From 51039c1eaeda083d0873b4883166cd121c8fdc96 Mon Sep 17 00:00:00 2001 From: japaveh Date: Tue, 6 Aug 2013 21:56:18 +0200 Subject: [PATCH 01/27] start of ideal 3.3.1 integration --- config/module.config.php | 6 +- config/services.config.php | 12 +- data/rabo.cer | 12 - data/rabo_ideal_v3.cer | 18 + data/xmlseclibs.php | 1876 +++++++++++++++++ src/SlmIdealPayment/Client/StandardClient.php | 599 ++++-- 6 files changed, 2295 insertions(+), 228 deletions(-) delete mode 100644 data/rabo.cer create mode 100644 data/rabo_ideal_v3.cer create mode 100644 data/xmlseclibs.php diff --git a/config/module.config.php b/config/module.config.php index 59f0fec..43beef1 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -63,9 +63,9 @@ 'certificate' => __DIR__ . '/../data/ing.cer', ), 'rabo' => array( - 'test' => 'https://idealtest.rabobank.nl/ideal/iDeal', - 'live' => 'https://ideal.rabobank.nl/ideal/iDeal', - 'certificate' => __DIR__ . '/../data/rabo.cer', + 'test' => 'https://idealtest.rabobank.nl/ideal/iDEALv3', + 'live' => 'https://ideal.rabobank.nl/ideal/iDEALv3', + 'certificate' => __DIR__ . '/../data/rabo_ideal_v3.cer', ), ), ); diff --git a/config/services.config.php b/config/services.config.php index 4e906b5..235ac85 100644 --- a/config/services.config.php +++ b/config/services.config.php @@ -41,9 +41,11 @@ use SlmIdealPayment\Client\StandardClient; use Zend\Http\Client as HttpClient; +include __DIR__ . '/../data/xmlseclibs.php'; + return array( 'factories' => array( - 'SlmIdealPayment\Client\StandardClient' => function($sm) { + 'SlmIdealPayment\Client\StandardClient' => function ($sm) { $config = $sm->get('config'); $config = $config['ideal']; @@ -51,6 +53,8 @@ $client->setPrivateCertificate($config['certificate']); $client->setKeyFile($config['key_file']); $client->setKeyPassword($config['key_password']); + $client->setMerchantId($config['merchant_id']); + $client->setSubId($config['sub_id']); $httpClient = new HttpClient; $httpClient->setAdapter('Zend\Http\Client\Adapter\Socket'); @@ -59,7 +63,7 @@ return $client; }, - 'ideal-abn' => function($sm) { + 'ideal-abn' => function ($sm) { $config = $sm->get('config'); $config = $config['ideal']; $client = $sm->get('SlmIdealPayment\Client\StandardClient'); @@ -72,7 +76,7 @@ return $client; }, - 'ideal-ing' => function($sm) { + 'ideal-ing' => function ($sm) { $config = $sm->get('config'); $config = $config['ideal']; $client = $sm->get('SlmIdealPayment\Client\StandardClient'); @@ -85,7 +89,7 @@ return $client; }, - 'ideal-rabo' => function($sm) { + 'ideal-rabo' => function ($sm) { $config = $sm->get('config'); $config = $config['ideal']; $client = $sm->get('SlmIdealPayment\Client\StandardClient'); diff --git a/data/rabo.cer b/data/rabo.cer deleted file mode 100644 index c1dfadd..0000000 --- a/data/rabo.cer +++ /dev/null @@ -1,12 +0,0 @@ ------BEGIN CERTIFICATE----- -MIICNjCCAZ8CBE1JPS0wDQYJKoZIhvcNAQEEBQAwYjELMAkGA1UEBhMCREUxDzANBgNVBAgTBkhl -c3NlbjESMBAGA1UEBxMJRnJhbmtmdXJ0MQ4wDAYDVQQKEwVpREVBTDEOMAwGA1UECxMFaURFQUwx -DjAMBgNVBAMTBWlERUFMMB4XDTExMDIwMjExMTcwMVoXDTE2MDIwMTExMTcwMVowYjELMAkGA1UE -BhMCREUxDzANBgNVBAgTBkhlc3NlbjESMBAGA1UEBxMJRnJhbmtmdXJ0MQ4wDAYDVQQKEwVpREVB -TDEOMAwGA1UECxMFaURFQUwxDjAMBgNVBAMTBWlERUFMMIGfMA0GCSqGSIb3DQEBAQUAA4GNADCB -iQKBgQC9P8lM2eJmd9Mm9J3nTygbtvZGFoxshQW1bQeMCDiCSAiYVj0v6UNBzKS3Rg3L5Hs51eOn -6nomwj9VO0PRFZF/gH+ENKzDbic/dWahvBNPt4lMsS3ivMfItxLdfuETG/CDmU95G4+Hy8adYP9V -mywFuo0+HTCmNvVRmsHeZOA7gQIDAQABMA0GCSqGSIb3DQEBBAUAA4GBAFD/hGrz7+m6c3XCt2Uu -W2QyPEGq8NH4sQUbgnKKMVQOml0fHuzrFYvR1DKOTBuM7WBPo0vadwgj5S8hd+8tMgH3f3I/iXp7 -ihpvvVxVBry6//Lw7s7d2CUKnMvsdnmhA9H8MuGqftMY/dClYHNCbiK+FnGFgCSoa9J6aP0b0btd ------END CERTIFICATE----- diff --git a/data/rabo_ideal_v3.cer b/data/rabo_ideal_v3.cer new file mode 100644 index 0000000..3ba7b08 --- /dev/null +++ b/data/rabo_ideal_v3.cer @@ -0,0 +1,18 @@ +-----BEGIN CERTIFICATE----- +MIIDhzCCAm8CBgE4cUAd7DANBgkqhkiG9w0BAQsFADCBhjELMAkGA1UEBhMCREUxDzANBgNVBAgT +Bkhlc3NlbjEaMBgGA1UEBxMRRnJhbmtmdXJ0IGFtIE1haW4xDTALBgNVBAoTBFJBQk8xHTAbBgNV +BAsTFE1lcmNoYW50IFhNTCBTaWduaW5nMRwwGgYDVQQDExNBdG9zIFdvcmxkbGluZSBHbWJIMB4X +DTEyMDYzMDIyMDAwMFoXDTE3MDcwMTEwMDAwMFowgYYxCzAJBgNVBAYTAkRFMQ8wDQYDVQQIEwZI +ZXNzZW4xGjAYBgNVBAcTEUZyYW5rZnVydCBhbSBNYWluMQ0wCwYDVQQKEwRSQUJPMR0wGwYDVQQL +ExRNZXJjaGFudCBYTUwgU2lnbmluZzEcMBoGA1UEAxMTQXRvcyBXb3JsZGxpbmUgR21iSDCCASIw +DQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAL+hIul8xb811QmqLg9mzkoHh+0BdnQJZCqyHyFM +eY7tkFOW8vwi13OxSMGLFzNnehjitMievsx2s5yQW/3NrjG3xLw/18PJrSIgulngs6Cjw9mmIRSn +FZp3ViZR5aEmjP3aHGxIT7MTqt/AzU6TVaOYur55WmiOFJSA15AN+Onf3U+H06y/kbZlj9+QwKxe +6jEZnaMlfSyhct5elswqEKjencUUU6qdRmsH8nSXmyrFJstXKlZsygDBJQSWxHqNE4r6lnYmpflC +76KMNcW1xsp58Qa8axlpZ3UjL+nVtBKw4t+R2ebQcz12N+vv/8TBJd8ckZ+YwW4Cm2fGGcc2CcEC +AwEAATANBgkqhkiG9w0BAQsFAAOCAQEAaLXoT+EezVl4YFcFTWI58Zg7C7WujRn+pTSFGN8MLFtx +MKfujLAMRh3YPVzXr2yE1kdMiMbq7IKtsKpb3PPgD3rb6YrP7zcwzDRxkXs802BgVxCPmdYrsa1i +PdJReq2VVTKoBHXSiKWowwBQFPOOc1XjFHcJ3Nq5WgssGEjk+puRW+i8GLaIv1KdwVlWLyHNArTs +W5JCcdtBhnMDz/g3/fRMu4EAnVFVmM75KNztVvgqkt+mZVuXfHfTCSv2RVFbrJvm/xrCmGk1VxDE +t4zSMdFEi98xh8DOC1oIhMf6JDImL1JyHqTljOIjBCo2uE5TFqQ/QZiOk0IC8Rb9y3lb2g== +-----END CERTIFICATE----- \ No newline at end of file diff --git a/data/xmlseclibs.php b/data/xmlseclibs.php new file mode 100644 index 0000000..7d38981 --- /dev/null +++ b/data/xmlseclibs.php @@ -0,0 +1,1876 @@ +. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in + * the documentation and/or other materials provided with the + * distribution. + * + * * Neither the name of Robert Richards nor the names of his + * contributors may be used to endorse or promote products derived + * from this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS + * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE + * COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, + * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; + * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT + * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN + * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE + * POSSIBILITY OF SUCH DAMAGE. + * + * @author Robert Richards + * @copyright 2007-2011 Robert Richards + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @version 1.3.0 + */ +/* + Functions to generate simple cases of Exclusive Canonical XML - Callable function is C14NGeneral() + i.e.: $canonical = C14NGeneral($domelement, TRUE); + */ + +/* helper function */ +function sortAndAddAttrs($element, $arAtts) +{ + $newAtts = array(); + foreach ($arAtts AS $attnode) { + $newAtts[$attnode->nodeName] = $attnode; + } + ksort($newAtts); + foreach ($newAtts as $attnode) { + $element->setAttribute($attnode->nodeName, $attnode->nodeValue); + } +} + +/* helper function */ + +function canonical($tree, $element, $withcomments) +{ + if ($tree->nodeType != XML_DOCUMENT_NODE) { + $dom = $tree->ownerDocument; + } else { + $dom = $tree; + } + if ($element->nodeType != XML_ELEMENT_NODE) { + if ($element->nodeType == XML_DOCUMENT_NODE) { + foreach ($element->childNodes AS $node) { + canonical($dom, $node, $withcomments); + } + return; + } + if ($element->nodeType == XML_COMMENT_NODE && !$withcomments) { + return; + } + $tree->appendChild($dom->importNode($element, true)); + return; + } + $arNS = array(); + if ($element->namespaceURI != "") { + if ($element->prefix == "") { + $elCopy = $dom->createElementNS($element->namespaceURI, $element->nodeName); + } else { + $prefix = $tree->lookupPrefix($element->namespaceURI); + if ($prefix == $element->prefix) { + $elCopy = $dom->createElementNS($element->namespaceURI, $element->nodeName); + } else { + $elCopy = $dom->createElement($element->nodeName); + $arNS[$element->namespaceURI] = $element->prefix; + } + } + } else { + $elCopy = $dom->createElement($element->nodeName); + } + $tree->appendChild($elCopy); + + /* Create DOMXPath based on original document */ + $xPath = new DOMXPath($element->ownerDocument); + + /* Get namespaced attributes */ + $arAtts = $xPath->query('attribute::*[namespace-uri(.) != ""]', $element); + + /* Create an array with namespace URIs as keys, and sort them */ + foreach ($arAtts AS $attnode) { + if (array_key_exists($attnode->namespaceURI, $arNS) && + ($arNS[$attnode->namespaceURI] == $attnode->prefix) + ) { + continue; + } + $prefix = $tree->lookupPrefix($attnode->namespaceURI); + if ($prefix != $attnode->prefix) { + $arNS[$attnode->namespaceURI] = $attnode->prefix; + } else { + $arNS[$attnode->namespaceURI] = null; + } + } + if (count($arNS) > 0) { + asort($arNS); + } + + /* Add namespace nodes */ + foreach ($arNS AS $namespaceURI => $prefix) { + if ($prefix != null) { + $elCopy->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:" . $prefix, $namespaceURI); + } + } + if (count($arNS) > 0) { + ksort($arNS); + } + + /* Get attributes not in a namespace, and then sort and add them */ + $arAtts = $xPath->query('attribute::*[namespace-uri(.) = ""]', $element); + sortAndAddAttrs($elCopy, $arAtts); + + /* Loop through the URIs, and then sort and add attributes within that namespace */ + foreach ($arNS as $nsURI => $prefix) { + $arAtts = $xPath->query('attribute::*[namespace-uri(.) = "' . $nsURI . '"]', $element); + sortAndAddAttrs($elCopy, $arAtts); + } + + foreach ($element->childNodes AS $node) { + canonical($elCopy, $node, $withcomments); + } +} + +/* + $element - DOMElement for which to produce the canonical version of + $exclusive - boolean to indicate exclusive canonicalization (must pass TRUE) + $withcomments - boolean indicating wether or not to include comments in canonicalized form + */ + +function C14NGeneral($element, $exclusive = false, $withcomments = false) +{ + /* IF PHP 5.2+ then use built in canonical functionality */ + $php_version = explode('.', PHP_VERSION); + if (($php_version[0] > 5) || ($php_version[0] == 5 && $php_version[1] >= 2)) { + return $element->C14N($exclusive, $withcomments); + } + + /* Must be element or document */ + if (!$element instanceof DOMElement && !$element instanceof DOMDocument) { + return null; + } + /* Currently only exclusive XML is supported */ + if ($exclusive == false) { + throw new Exception("Only exclusive canonicalization is supported in this version of PHP"); + } + + $copyDoc = new DOMDocument(); + canonical($copyDoc, $element, $withcomments); + return $copyDoc->saveXML($copyDoc->documentElement, LIBXML_NOEMPTYTAG); +} + +class XMLSecurityKey +{ + const TRIPLEDES_CBC = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; + const AES128_CBC = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; + const AES192_CBC = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; + const AES256_CBC = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; + const RSA_1_5 = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; + const RSA_OAEP_MGF1P = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; + const DSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#dsa-sha1'; + const RSA_SHA1 = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + const RSA_SHA256 = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + + private $cryptParams = array(); + public $type = 0; + public $key = null; + public $passphrase = ""; + public $iv = null; + public $name = null; + public $keyChain = null; + public $isEncrypted = false; + public $encryptedCtx = null; + public $guid = null; + + /** + * This variable contains the certificate as a string if this key represents an X509-certificate. + * If this key doesn't represent a certificate, this will be NULL. + */ + private $x509Certificate = null; + + /* This variable contains the certificate thunbprint if we have loaded an X509-certificate. */ + private $X509Thumbprint = null; + + public function __construct($type, $params = null) + { + srand(); + switch ($type) { + case (XMLSecurityKey::TRIPLEDES_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_TRIPLEDES; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#tripledes-cbc'; + $this->cryptParams['keysize'] = 24; + break; + case (XMLSecurityKey::AES128_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes128-cbc'; + $this->cryptParams['keysize'] = 16; + break; + case (XMLSecurityKey::AES192_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes192-cbc'; + $this->cryptParams['keysize'] = 24; + break; + case (XMLSecurityKey::AES256_CBC): + $this->cryptParams['library'] = 'mcrypt'; + $this->cryptParams['cipher'] = MCRYPT_RIJNDAEL_128; + $this->cryptParams['mode'] = MCRYPT_MODE_CBC; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#aes256-cbc'; + $this->cryptParams['keysize'] = 32; + break; + case (XMLSecurityKey::RSA_1_5): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-1_5'; + if (is_array($params) && !empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + return; + case (XMLSecurityKey::RSA_OAEP_MGF1P): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_OAEP_PADDING; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmlenc#rsa-oaep-mgf1p'; + $this->cryptParams['hash'] = null; + if (is_array($params) && !empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + return; + case (XMLSecurityKey::RSA_SHA1): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2000/09/xmldsig#rsa-sha1'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + if (is_array($params) && !empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + break; + case (XMLSecurityKey::RSA_SHA256): + $this->cryptParams['library'] = 'openssl'; + $this->cryptParams['method'] = 'http://www.w3.org/2001/04/xmldsig-more#rsa-sha256'; + $this->cryptParams['padding'] = OPENSSL_PKCS1_PADDING; + $this->cryptParams['digest'] = 'SHA256'; + if (is_array($params) && !empty($params['type'])) { + if ($params['type'] == 'public' || $params['type'] == 'private') { + $this->cryptParams['type'] = $params['type']; + break; + } + } + throw new Exception('Certificate "type" (private/public) must be passed via parameters'); + break; + default: + throw new Exception('Invalid Key Type'); + return; + } + $this->type = $type; + } + + /** + * Retrieve the key size for the symmetric encryption algorithm.. + * + * If the key size is unknown, or this isn't a symmetric encryption algorithm, + * NULL is returned. + * + * @return int|NULL The number of bytes in the key. + */ + public function getSymmetricKeySize() + { + if (!isset($this->cryptParams['keysize'])) { + return null; + } + return $this->cryptParams['keysize']; + } + + public function generateSessionKey() + { + if (!isset($this->cryptParams['keysize'])) { + throw new Exception('Unknown key size for type "' . $this->type . '".'); + } + $keysize = $this->cryptParams['keysize']; + + if (function_exists('openssl_random_pseudo_bytes')) { + /* We have PHP >= 5.3 - use openssl to generate session key. */ + $key = openssl_random_pseudo_bytes($keysize); + } else { + /* Generating random key using iv generation routines */ + $key = mcrypt_create_iv($keysize, MCRYPT_RAND); + } + + if ($this->type === XMLSecurityKey::TRIPLEDES_CBC) { + /* Make sure that the generated key has the proper parity bits set. + * Mcrypt doesn't care about the parity bits, but others may care. + */ + for ($i = 0; $i < strlen($key); $i++) { + $byte = ord($key[$i]) & 0xfe; + $parity = 1; + for ($j = 1; $j < 8; $j++) { + $parity ^= ($byte >> $j) & 1; + } + $byte |= $parity; + $key[$i] = chr($byte); + } + } + + $this->key = $key; + return $key; + } + + public static function getRawThumbprint($cert) + { + + $arCert = explode("\n", $cert); + $data = ''; + $inData = false; + + foreach ($arCert AS $curData) { + if (!$inData) { + if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { + $inData = true; + } + } else { + if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { + $inData = false; + break; + } + $data .= trim($curData); + } + } + + if (!empty($data)) { + return strtolower(sha1(base64_decode($data))); + } + + return null; + } + + public function loadKey($key, $isFile = false, $isCert = false) + { + if ($isFile) { + $this->key = file_get_contents($key); + } else { + $this->key = $key; + } + if ($isCert) { + $this->key = openssl_x509_read($this->key); + openssl_x509_export($this->key, $str_cert); + $this->x509Certificate = $str_cert; + $this->key = $str_cert; + } else { + $this->x509Certificate = null; + } + if ($this->cryptParams['library'] == 'openssl') { + if ($this->cryptParams['type'] == 'public') { + if ($isCert) { + /* Load the thumbprint if this is an X509 certificate. */ + $this->X509Thumbprint = self::getRawThumbprint($this->key); + } + $this->key = openssl_get_publickey($this->key); + } else { + $this->key = openssl_get_privatekey($this->key, $this->passphrase); + } + } else { + if ($this->cryptParams['cipher'] == MCRYPT_RIJNDAEL_128) { + /* Check key length */ + switch ($this->type) { + case (XMLSecurityKey::AES256_CBC): + if (strlen($this->key) < 25) { + throw new Exception('Key must contain at least 25 characters for this cipher'); + } + break; + case (XMLSecurityKey::AES192_CBC): + if (strlen($this->key) < 17) { + throw new Exception('Key must contain at least 17 characters for this cipher'); + } + break; + } + } + } + } + + private function encryptMcrypt($data) + { + $td = mcrypt_module_open($this->cryptParams['cipher'], '', $this->cryptParams['mode'], ''); + $this->iv = mcrypt_create_iv(mcrypt_enc_get_iv_size($td), MCRYPT_RAND); + mcrypt_generic_init($td, $this->key, $this->iv); + if ($this->cryptParams['mode'] == MCRYPT_MODE_CBC) { + $bs = mcrypt_enc_get_block_size($td); + for ($datalen0 = $datalen = strlen($data); (($datalen % $bs) != ($bs - 1)); $datalen++) { + $data .= chr(rand(1, 127)); + } + $data .= chr($datalen - $datalen0 + 1); + } + $encrypted_data = $this->iv . mcrypt_generic($td, $data); + mcrypt_generic_deinit($td); + mcrypt_module_close($td); + return $encrypted_data; + } + + private function decryptMcrypt($data) + { + $td = mcrypt_module_open($this->cryptParams['cipher'], '', $this->cryptParams['mode'], ''); + $iv_length = mcrypt_enc_get_iv_size($td); + + $this->iv = substr($data, 0, $iv_length); + $data = substr($data, $iv_length); + + mcrypt_generic_init($td, $this->key, $this->iv); + $decrypted_data = mdecrypt_generic($td, $data); + mcrypt_generic_deinit($td); + mcrypt_module_close($td); + if ($this->cryptParams['mode'] == MCRYPT_MODE_CBC) { + $dataLen = strlen($decrypted_data); + $paddingLength = substr($decrypted_data, $dataLen - 1, 1); + $decrypted_data = substr($decrypted_data, 0, $dataLen - ord($paddingLength)); + } + return $decrypted_data; + } + + private function encryptOpenSSL($data) + { + if ($this->cryptParams['type'] == 'public') { + if (!openssl_public_encrypt($data, $encrypted_data, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure encrypting Data'); + return; + } + } else { + if (!openssl_private_encrypt($data, $encrypted_data, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure encrypting Data'); + return; + } + } + return $encrypted_data; + } + + private function decryptOpenSSL($data) + { + if ($this->cryptParams['type'] == 'public') { + if (!openssl_public_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure decrypting Data'); + return; + } + } else { + if (!openssl_private_decrypt($data, $decrypted, $this->key, $this->cryptParams['padding'])) { + throw new Exception('Failure decrypting Data'); + return; + } + } + return $decrypted; + } + + private function signOpenSSL($data) + { + $algo = OPENSSL_ALGO_SHA1; + if (!empty($this->cryptParams['digest'])) { + $algo = $this->cryptParams['digest']; + } + if (!openssl_sign($data, $signature, $this->key, $algo)) { + throw new Exception('Failure Signing Data: ' . openssl_error_string() . ' - ' . $algo); + return; + } + return $signature; + } + + private function verifyOpenSSL($data, $signature) + { + $algo = OPENSSL_ALGO_SHA1; + if (!empty($this->cryptParams['digest'])) { + $algo = $this->cryptParams['digest']; + } + return openssl_verify($data, $signature, $this->key, $algo); + } + + public function encryptData($data) + { + switch ($this->cryptParams['library']) { + case 'mcrypt': + return $this->encryptMcrypt($data); + break; + case 'openssl': + return $this->encryptOpenSSL($data); + break; + } + } + + public function decryptData($data) + { + switch ($this->cryptParams['library']) { + case 'mcrypt': + return $this->decryptMcrypt($data); + break; + case 'openssl': + return $this->decryptOpenSSL($data); + break; + } + } + + public function signData($data) + { + switch ($this->cryptParams['library']) { + case 'openssl': + return $this->signOpenSSL($data); + break; + } + } + + public function verifySignature($data, $signature) + { + switch ($this->cryptParams['library']) { + case 'openssl': + return $this->verifyOpenSSL($data, $signature); + break; + } + } + + public function getAlgorith() + { + return $this->cryptParams['method']; + } + + static function makeAsnSegment($type, $string) + { + switch ($type) { + case 0x02: + if (ord($string) > 0x7f) { + $string = chr(0) . $string; + } + break; + case 0x03: + $string = chr(0) . $string; + break; + } + + $length = strlen($string); + + if ($length < 128) { + $output = sprintf("%c%c%s", $type, $length, $string); + } else { + if ($length < 0x0100) { + $output = sprintf("%c%c%c%s", $type, 0x81, $length, $string); + } else { + if ($length < 0x010000) { + $output = sprintf("%c%c%c%c%s", $type, 0x82, $length / 0x0100, $length % 0x0100, $string); + } else { + $output = null; + } + } + } + return ($output); + } + + /* Modulus and Exponent must already be base64 decoded */ + + static function convertRSA($modulus, $exponent) + { + /* make an ASN publicKeyInfo */ + $exponentEncoding = XMLSecurityKey::makeAsnSegment(0x02, $exponent); + $modulusEncoding = XMLSecurityKey::makeAsnSegment(0x02, $modulus); + $sequenceEncoding = XMLSecurityKey:: makeAsnSegment(0x30, $modulusEncoding . $exponentEncoding); + $bitstringEncoding = XMLSecurityKey::makeAsnSegment(0x03, $sequenceEncoding); + $rsaAlgorithmIdentifier = pack("H*", "300D06092A864886F70D0101010500"); + $publicKeyInfo = XMLSecurityKey::makeAsnSegment(0x30, $rsaAlgorithmIdentifier . $bitstringEncoding); + + /* encode the publicKeyInfo in base64 and add PEM brackets */ + $publicKeyInfoBase64 = base64_encode($publicKeyInfo); + $encoding = "-----BEGIN PUBLIC KEY-----\n"; + $offset = 0; + while ($segment = substr($publicKeyInfoBase64, $offset, 64)) { + $encoding = $encoding . $segment . "\n"; + $offset += 64; + } + return $encoding . "-----END PUBLIC KEY-----\n"; + } + + public function serializeKey($parent) + { + } + + /** + * Retrieve the X509 certificate this key represents. + * + * Will return the X509 certificate in PEM-format if this key represents + * an X509 certificate. + * + * @return The X509 certificate or NULL if this key doesn't represent an X509-certificate. + */ + public function getX509Certificate() + { + return $this->x509Certificate; + } + + /* Get the thumbprint of this X509 certificate. + * + * Returns: + * The thumbprint as a lowercase 40-character hexadecimal number, or NULL + * if this isn't a X509 certificate. + */ + + public function getX509Thumbprint() + { + return $this->X509Thumbprint; + } + + /** + * Create key from an EncryptedKey-element. + * + * @param DOMElement $element The EncryptedKey-element. + * @return XMLSecurityKey The new key. + */ + public static function fromEncryptedKeyElement(DOMElement $element) + { + + $objenc = new XMLSecEnc(); + $objenc->setNode($element); + if (!$objKey = $objenc->locateKey()) { + throw new Exception("Unable to locate algorithm for this Encrypted Key"); + } + $objKey->isEncrypted = true; + $objKey->encryptedCtx = $objenc; + XMLSecEnc::staticLocateKeyInfo($objKey, $element); + return $objKey; + } +} + +class XMLSecurityDSig +{ + const XMLDSIGNS = 'http://www.w3.org/2000/09/xmldsig#'; + const SHA1 = 'http://www.w3.org/2000/09/xmldsig#sha1'; + const SHA256 = 'http://www.w3.org/2001/04/xmlenc#sha256'; + const SHA512 = 'http://www.w3.org/2001/04/xmlenc#sha512'; + const RIPEMD160 = 'http://www.w3.org/2001/04/xmlenc#ripemd160'; + + const C14N = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + const C14N_COMMENTS = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments'; + const EXC_C14N = 'http://www.w3.org/2001/10/xml-exc-c14n#'; + const EXC_C14N_COMMENTS = 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments'; + + const template = ' + + + +'; + + public $sigNode = null; + public $idKeys = array(); + public $idNS = array(); + private $signedInfo = null; + private $xPathCtx = null; + private $canonicalMethod = null; + private $prefix = null; + private $searchpfx = 'secdsig'; + + /* This variable contains an associative array of validated nodes. */ + private $validatedNodes = null; + + public function __construct() + { + $sigdoc = new DOMDocument(); + $sigdoc->loadXML(XMLSecurityDSig::template); + $this->sigNode = $sigdoc->documentElement; + } + + private function resetXPathObj() + { + $this->xPathCtx = null; + } + + private function getXPathObj() + { + if (empty($this->xPathCtx) && !empty($this->sigNode)) { + $xpath = new DOMXPath($this->sigNode->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $this->xPathCtx = $xpath; + } + return $this->xPathCtx; + } + + static function generate_GUID($prefix = 'pfx') + { + $uuid = md5(uniqid(rand(), true)); + $guid = $prefix . substr($uuid, 0, 8) . "-" . + substr($uuid, 8, 4) . "-" . + substr($uuid, 12, 4) . "-" . + substr($uuid, 16, 4) . "-" . + substr($uuid, 20, 12); + return $guid; + } + + public function locateSignature($objDoc) + { + if ($objDoc instanceof DOMDocument) { + $doc = $objDoc; + } else { + $doc = $objDoc->ownerDocument; + } + if ($doc) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = ".//secdsig:Signature"; + $nodeset = $xpath->query($query, $objDoc); + $this->sigNode = $nodeset->item(0); + return $this->sigNode; + } + return null; + } + + public function createNewSignNode($name, $value = null) + { + $doc = $this->sigNode->ownerDocument; + if ($this->prefix != null) { + $name = $this->prefix . ':' . $name; + } + if (!is_null($value)) { + $node = $doc->createElementNS(XMLSecurityDSig::XMLDSIGNS, $name, $value); + } else { + $node = $doc->createElementNS(XMLSecurityDSig::XMLDSIGNS, $name); + } + return $node; + } + + public function setCanonicalMethod($method) + { + switch ($method) { + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $this->canonicalMethod = $method; + break; + default: + throw new Exception('Invalid Canonical Method'); + } + if ($xpath = $this->getXPathObj()) { + $query = './' . $this->searchpfx . ':SignedInfo'; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sinfo = $nodeset->item(0)) { + $query = './' . $this->searchpfx . 'CanonicalizationMethod'; + $nodeset = $xpath->query($query, $sinfo); + if (!($canonNode = $nodeset->item(0))) { + $canonNode = $this->createNewSignNode('CanonicalizationMethod'); + $sinfo->insertBefore($canonNode, $sinfo->firstChild); + } + $canonNode->setAttribute('Algorithm', $this->canonicalMethod); + } + } + } + + private function canonicalizeData($node, $canonicalmethod, $arXPath = null, $prefixList = null) + { + $exclusive = false; + $withComments = false; + switch ($canonicalmethod) { + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + $exclusive = false; + $withComments = false; + break; + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + $withComments = true; + break; + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + $exclusive = true; + break; + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + $exclusive = true; + $withComments = true; + break; + } + /* Support PHP versions < 5.2 not containing C14N methods in DOM extension */ + $php_version = explode('.', PHP_VERSION); + if (($php_version[0] < 5) || ($php_version[0] == 5 && $php_version[1] < 2)) { + if (!is_null($arXPath)) { + throw new Exception("PHP 5.2.0 or higher is required to perform XPath Transformations"); + } + return C14NGeneral($node, $exclusive, $withComments); + } + return $node->C14N($exclusive, $withComments, $arXPath, $prefixList); + } + + public function canonicalizeSignedInfo() + { + + $doc = $this->sigNode->ownerDocument; + $canonicalmethod = null; + if ($doc) { + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($signInfoNode = $nodeset->item(0)) { + $query = "./secdsig:CanonicalizationMethod"; + $nodeset = $xpath->query($query, $signInfoNode); + if ($canonNode = $nodeset->item(0)) { + $canonicalmethod = $canonNode->getAttribute('Algorithm'); + } + $this->signedInfo = $this->canonicalizeData($signInfoNode, $canonicalmethod); + return $this->signedInfo; + } + } + return null; + } + + public function calculateDigest($digestAlgorithm, $data) + { + switch ($digestAlgorithm) { + case XMLSecurityDSig::SHA1: + $alg = 'sha1'; + break; + case XMLSecurityDSig::SHA256: + $alg = 'sha256'; + break; + case XMLSecurityDSig::SHA512: + $alg = 'sha512'; + break; + case XMLSecurityDSig::RIPEMD160: + $alg = 'ripemd160'; + break; + default: + throw new Exception("Cannot validate digest: Unsupported Algorith <$digestAlgorithm>"); + } + if (function_exists('hash')) { + return base64_encode(hash($alg, $data, true)); + } elseif (function_exists('mhash')) { + $alg = "MHASH_" . strtoupper($alg); + return base64_encode(mhash(constant($alg), $data)); + } elseif ($alg === 'sha1') { + return base64_encode(sha1($data, true)); + } else { + throw new Exception('xmlseclibs is unable to calculate a digest. Maybe you need the mhash library?'); + } + } + + public function validateDigest($refNode, $data) + { + $xpath = new DOMXPath($refNode->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = 'string(./secdsig:DigestMethod/@Algorithm)'; + $digestAlgorithm = $xpath->evaluate($query, $refNode); + $digValue = $this->calculateDigest($digestAlgorithm, $data); + $query = 'string(./secdsig:DigestValue)'; + $digestValue = $xpath->evaluate($query, $refNode); + return ($digValue == $digestValue); + } + + public function processTransforms($refNode, $objData, $includeCommentNodes = true) + { + $data = $objData; + $xpath = new DOMXPath($refNode->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = './secdsig:Transforms/secdsig:Transform'; + $nodelist = $xpath->query($query, $refNode); + $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + $arXPath = null; + $prefixList = null; + foreach ($nodelist AS $transform) { + $algorithm = $transform->getAttribute("Algorithm"); + switch ($algorithm) { + case 'http://www.w3.org/2001/10/xml-exc-c14n#': + case 'http://www.w3.org/2001/10/xml-exc-c14n#WithComments': + + if (!$includeCommentNodes) { + /* We remove comment nodes by forcing it to use a canonicalization + * without comments. + */ + $canonicalMethod = 'http://www.w3.org/2001/10/xml-exc-c14n#'; + } else { + $canonicalMethod = $algorithm; + } + + $node = $transform->firstChild; + while ($node) { + if ($node->localName == 'InclusiveNamespaces') { + if ($pfx = $node->getAttribute('PrefixList')) { + $arpfx = array(); + $pfxlist = explode(" ", $pfx); + foreach ($pfxlist AS $pfx) { + $val = trim($pfx); + if (!empty($val)) { + $arpfx[] = $val; + } + } + if (count($arpfx) > 0) { + $prefixList = $arpfx; + } + } + break; + } + $node = $node->nextSibling; + } + break; + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315': + case 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments': + if (!$includeCommentNodes) { + /* We remove comment nodes by forcing it to use a canonicalization + * without comments. + */ + $canonicalMethod = 'http://www.w3.org/TR/2001/REC-xml-c14n-20010315'; + } else { + $canonicalMethod = $algorithm; + } + + break; + case 'http://www.w3.org/TR/1999/REC-xpath-19991116': + $node = $transform->firstChild; + while ($node) { + if ($node->localName == 'XPath') { + $arXPath = array(); + $arXPath['query'] = '(.//. | .//@* | .//namespace::*)[' . $node->nodeValue . ']'; + $arXpath['namespaces'] = array(); + $nslist = $xpath->query('./namespace::*', $node); + foreach ($nslist AS $nsnode) { + if ($nsnode->localName != "xml") { + $arXPath['namespaces'][$nsnode->localName] = $nsnode->nodeValue; + } + } + break; + } + $node = $node->nextSibling; + } + break; + } + } + if ($data instanceof DOMNode) { + $data = $this->canonicalizeData($objData, $canonicalMethod, $arXPath, $prefixList); + } + return $data; + } + + public function processRefNode($refNode) + { + $dataObject = null; + + /* + * Depending on the URI, we may not want to include comments in the result + * See: http://www.w3.org/TR/xmldsig-core/#sec-ReferenceProcessingModel + */ + $includeCommentNodes = true; + + if ($uri = $refNode->getAttribute("URI")) { + $arUrl = parse_url($uri); + if (empty($arUrl['path'])) { + if ($identifier = $arUrl['fragment']) { + + /* This reference identifies a node with the given id by using + * a URI on the form "#identifier". This should not include comments. + */ + $includeCommentNodes = false; + + $xPath = new DOMXPath($refNode->ownerDocument); + if ($this->idNS && is_array($this->idNS)) { + foreach ($this->idNS AS $nspf => $ns) { + $xPath->registerNamespace($nspf, $ns); + } + } + $iDlist = '@Id="' . $identifier . '"'; + if (is_array($this->idKeys)) { + foreach ($this->idKeys AS $idKey) { + $iDlist .= " or @$idKey='$identifier'"; + } + } + $query = '//*[' . $iDlist . ']'; + $dataObject = $xPath->query($query)->item(0); + } else { + $dataObject = $refNode->ownerDocument; + } + } else { + $dataObject = file_get_contents($arUrl); + } + } else { + /* This reference identifies the root node with an empty URI. This should + * not include comments. + */ + $includeCommentNodes = false; + + $dataObject = $refNode->ownerDocument; + } + $data = $this->processTransforms($refNode, $dataObject, $includeCommentNodes); + if (!$this->validateDigest($refNode, $data)) { + return false; + } + + if ($dataObject instanceof DOMNode) { + /* Add this node to the list of validated nodes. */ + if (!empty($identifier)) { + $this->validatedNodes[$identifier] = $dataObject; + } else { + $this->validatedNodes[] = $dataObject; + } + } + + return true; + } + + public function getRefNodeID($refNode) + { + if ($uri = $refNode->getAttribute("URI")) { + $arUrl = parse_url($uri); + if (empty($arUrl['path'])) { + if ($identifier = $arUrl['fragment']) { + return $identifier; + } + } + } + return null; + } + + public function getRefIDs() + { + $refids = array(); + $doc = $this->sigNode->ownerDocument; + + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo/secdsig:Reference"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length == 0) { + throw new Exception("Reference nodes not found"); + } + foreach ($nodeset AS $refNode) { + $refids[] = $this->getRefNodeID($refNode); + } + return $refids; + } + + public function validateReference() + { + $doc = $this->sigNode->ownerDocument; + if (!$doc->isSameNode($this->sigNode)) { + $this->sigNode->parentNode->removeChild($this->sigNode); + } + $xpath = $this->getXPathObj(); + $query = "./secdsig:SignedInfo/secdsig:Reference"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($nodeset->length == 0) { + throw new Exception("Reference nodes not found"); + } + + /* Initialize/reset the list of validated nodes. */ + $this->validatedNodes = array(); + + foreach ($nodeset AS $refNode) { + if (!$this->processRefNode($refNode)) { + /* Clear the list of validated nodes. */ + $this->validatedNodes = null; + throw new Exception("Reference validation failed"); + } + } + return true; + } + + private function addRefInternal($sinfoNode, $node, $algorithm, $arTransforms = null, $options = null) + { + $prefix = null; + $prefix_ns = null; + $id_name = 'Id'; + $overwrite_id = true; + $force_uri = false; + + if (is_array($options)) { + $prefix = empty($options['prefix']) ? null : $options['prefix']; + $prefix_ns = empty($options['prefix_ns']) ? null : $options['prefix_ns']; + $id_name = empty($options['id_name']) ? 'Id' : $options['id_name']; + $overwrite_id = !isset($options['overwrite']) ? true : (bool)$options['overwrite']; + $force_uri = !isset($options['force_uri']) ? false : (bool)$options['force_uri']; + } + + $attname = $id_name; + if (!empty($prefix)) { + $attname = $prefix . ':' . $attname; + } + + $refNode = $this->createNewSignNode('Reference'); + $sinfoNode->appendChild($refNode); + + if (!$node instanceof DOMDocument) { + $uri = null; + if (!$overwrite_id) { + $uri = $node->getAttributeNS($prefix_ns, $attname); + } + if (empty($uri)) { + $uri = XMLSecurityDSig::generate_GUID(); + $node->setAttributeNS($prefix_ns, $attname, $uri); + } + $refNode->setAttribute("URI", '#' . $uri); + } elseif ($force_uri) { + $refNode->setAttribute("URI", ''); + } + + $transNodes = $this->createNewSignNode('Transforms'); + $refNode->appendChild($transNodes); + + if (is_array($arTransforms)) { + foreach ($arTransforms AS $transform) { + $transNode = $this->createNewSignNode('Transform'); + $transNodes->appendChild($transNode); + if (is_array($transform) && + (!empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116'])) && + (!empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query'])) + ) { + $transNode->setAttribute('Algorithm', 'http://www.w3.org/TR/1999/REC-xpath-19991116'); + $XPathNode = $this->createNewSignNode( + 'XPath', + $transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['query'] + ); + $transNode->appendChild($XPathNode); + if (!empty($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'])) { + foreach ($transform['http://www.w3.org/TR/1999/REC-xpath-19991116']['namespaces'] AS $prefix => $namespace) { + $XPathNode->setAttributeNS("http://www.w3.org/2000/xmlns/", "xmlns:$prefix", $namespace); + } + } + } else { + $transNode->setAttribute('Algorithm', $transform); + } + } + } elseif (!empty($this->canonicalMethod)) { + $transNode = $this->createNewSignNode('Transform'); + $transNodes->appendChild($transNode); + $transNode->setAttribute('Algorithm', $this->canonicalMethod); + } + + $canonicalData = $this->processTransforms($refNode, $node); + $digValue = $this->calculateDigest($algorithm, $canonicalData); + + $digestMethod = $this->createNewSignNode('DigestMethod'); + $refNode->appendChild($digestMethod); + $digestMethod->setAttribute('Algorithm', $algorithm); + + $digestValue = $this->createNewSignNode('DigestValue', $digValue); + $refNode->appendChild($digestValue); + } + + public function addReference($node, $algorithm, $arTransforms = null, $options = null) + { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); + } + } + } + + public function addReferenceList($arNodes, $algorithm, $arTransforms = null, $options = null) + { + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + foreach ($arNodes AS $node) { + $this->addRefInternal($sInfo, $node, $algorithm, $arTransforms, $options); + } + } + } + } + + public function addObject($data, $mimetype = null, $encoding = null) + { + $objNode = $this->createNewSignNode('Object'); + $this->sigNode->appendChild($objNode); + if (!empty($mimetype)) { + $objNode->setAtribute('MimeType', $mimetype); + } + if (!empty($encoding)) { + $objNode->setAttribute('Encoding', $encoding); + } + + if ($data instanceof DOMElement) { + $newData = $this->sigNode->ownerDocument->importNode($data, true); + } else { + $newData = $this->sigNode->ownerDocument->createTextNode($data); + } + $objNode->appendChild($newData); + + return $objNode; + } + + public function locateKey($node = null) + { + if (empty($node)) { + $node = $this->sigNode; + } + if (!$node instanceof DOMNode) { + return null; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "string(./secdsig:SignedInfo/secdsig:SignatureMethod/@Algorithm)"; + $algorithm = $xpath->evaluate($query, $node); + if ($algorithm) { + try { + $objKey = new XMLSecurityKey($algorithm, array('type' => 'public')); + } catch (Exception $e) { + return null; + } + return $objKey; + } + } + return null; + } + + public function verify($objKey) + { + $doc = $this->sigNode->ownerDocument; + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "string(./secdsig:SignatureValue)"; + $sigValue = $xpath->evaluate($query, $this->sigNode); + if (empty($sigValue)) { + throw new Exception("Unable to locate SignatureValue"); + } + return $objKey->verifySignature($this->signedInfo, base64_decode($sigValue)); + } + + public function signData($objKey, $data) + { + return $objKey->signData($data); + } + + public function sign($objKey, $appendToNode = null) + { + // If we have a parent node append it now so C14N properly works + if ($appendToNode != null) { + $this->resetXPathObj(); + $this->appendSignature($appendToNode); + $this->sigNode = $appendToNode->lastChild; + } + if ($xpath = $this->getXPathObj()) { + $query = "./secdsig:SignedInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sInfo = $nodeset->item(0)) { + $query = "./secdsig:SignatureMethod"; + $nodeset = $xpath->query($query, $sInfo); + $sMethod = $nodeset->item(0); + $sMethod->setAttribute('Algorithm', $objKey->type); + $data = $this->canonicalizeData($sInfo, $this->canonicalMethod); + $sigValue = base64_encode($this->signData($objKey, $data)); + $sigValueNode = $this->createNewSignNode('SignatureValue', $sigValue); + if ($infoSibling = $sInfo->nextSibling) { + $infoSibling->parentNode->insertBefore($sigValueNode, $infoSibling); + } else { + $this->sigNode->appendChild($sigValueNode); + } + } + } + } + + public function appendCert() + { + } + + public function appendKey($objKey, $parent = null) + { + $objKey->serializeKey($parent); + } + + /** + * This function inserts the signature element. + * + * The signature element will be appended to the element, unless $beforeNode is specified. If $beforeNode + * is specified, the signature element will be inserted as the last element before $beforeNode. + * + * @param $node The node the signature element should be inserted into. + * @param $beforeNode The node the signature element should be located before. + * + * @return DOMNode The signature element node + */ + public function insertSignature($node, $beforeNode = null) + { + + $document = $node->ownerDocument; + $signatureElement = $document->importNode($this->sigNode, true); + + if ($beforeNode == null) { + return $node->insertBefore($signatureElement); + } else { + return $node->insertBefore($signatureElement, $beforeNode); + } + } + + public function appendSignature($parentNode, $insertBefore = false) + { + $beforeNode = $insertBefore ? $parentNode->firstChild : null; + return $this->insertSignature($parentNode, $beforeNode); + } + + static function get509XCert($cert, $isPEMFormat = true) + { + $certs = XMLSecurityDSig::staticGet509XCerts($cert, $isPEMFormat); + if (!empty($certs)) { + return $certs[0]; + } + return ''; + } + + static function staticGet509XCerts($certs, $isPEMFormat = true) + { + if ($isPEMFormat) { + $data = ''; + $certlist = array(); + $arCert = explode("\n", $certs); + $inData = false; + foreach ($arCert AS $curData) { + if (!$inData) { + if (strncmp($curData, '-----BEGIN CERTIFICATE', 22) == 0) { + $inData = true; + } + } else { + if (strncmp($curData, '-----END CERTIFICATE', 20) == 0) { + $inData = false; + $certlist[] = $data; + $data = ''; + continue; + } + $data .= trim($curData); + } + } + return $certlist; + } else { + return array($certs); + } + } + + static function staticAdd509Cert($parentRef, $cert, $isPEMFormat = true, $isURL = false, $xpath = null) + { + if ($isURL) { + $cert = file_get_contents($cert); + } + if (!$parentRef instanceof DOMElement) { + throw new Exception('Invalid parent Node parameter'); + } + $baseDoc = $parentRef->ownerDocument; + + if (empty($xpath)) { + $xpath = new DOMXPath($parentRef->ownerDocument); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + } + + $query = "./secdsig:KeyInfo"; + $nodeset = $xpath->query($query, $parentRef); + $keyInfo = $nodeset->item(0); + if (!$keyInfo) { + $inserted = false; + $keyInfo = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:KeyInfo'); + + $query = "./secdsig:Object"; + $nodeset = $xpath->query($query, $parentRef); + if ($sObject = $nodeset->item(0)) { + $sObject->parentNode->insertBefore($keyInfo, $sObject); + $inserted = true; + } + + if (!$inserted) { + $parentRef->appendChild($keyInfo); + } + } + + // Add all certs if there are more than one + $certs = XMLSecurityDSig::staticGet509XCerts($cert, $isPEMFormat); + + // Atach X509 data node + $x509DataNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509Data'); + $keyInfo->appendChild($x509DataNode); + + // Atach all certificate nodes + foreach ($certs as $X509Cert) { + $x509CertNode = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'ds:X509Certificate', $X509Cert); + $x509DataNode->appendChild($x509CertNode); + } + } + + public function add509Cert($cert, $isPEMFormat = true, $isURL = false) + { + if ($xpath = $this->getXPathObj()) { + self::staticAdd509Cert($this->sigNode, $cert, $isPEMFormat, $isURL, $xpath); + } + } + + function addKeyInfoAndName($keyName, $xpath = null) + { + + $baseDoc = $this->sigNode->ownerDocument; + + if (empty($xpath)) { + $xpath = new DOMXPath($baseDoc); + $xpath->registerNamespace('secdsig', XMLSecurityDSig::XMLDSIGNS); + } + + $query = "./secdsig:KeyInfo"; + $nodeset = $xpath->query($query, $this->sigNode); + $keyInfo = $nodeset->item(0); + if (!$keyInfo) { + $inserted = false; + $keyInfo = $baseDoc->createElementNS(XMLSecurityDSig::XMLDSIGNS, 'KeyInfo'); + + $query = "./secdsig:Object"; + $nodeset = $xpath->query($query, $this->sigNode); + if ($sObject = $nodeset->item(0)) { + $sObject->parentNode->insertBefore($keyInfo, $sObject); + $inserted = true; + } + if (!$inserted) { + $this->sigNode->appendChild($keyInfo); + } + } + $keyInfo->appendChild($baseDoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'KeyName', $keyName)); + } + + /* This function retrieves an associative array of the validated nodes. + * + * The array will contain the id of the referenced node as the key and the node itself + * as the value. + * + * Returns: + * An associative array of validated nodes or NULL if no nodes have been validated. + */ + + public function getValidatedNodes() + { + return $this->validatedNodes; + } +} + +class XMLSecEnc +{ + const template = " + + + +"; + + const Element = 'http://www.w3.org/2001/04/xmlenc#Element'; + const Content = 'http://www.w3.org/2001/04/xmlenc#Content'; + const URI = 3; + const XMLENCNS = 'http://www.w3.org/2001/04/xmlenc#'; + + private $encdoc = null; + private $rawNode = null; + public $type = null; + public $encKey = null; + private $references = array(); + + public function __construct() + { + $this->_resetTemplate(); + } + + private function _resetTemplate() + { + $this->encdoc = new DOMDocument(); + $this->encdoc->loadXML(XMLSecEnc::template); + } + + public function addReference($name, $node, $type) + { + if (!$node instanceOf DOMNode) { + throw new Exception('$node is not of type DOMNode'); + } + $curencdoc = $this->encdoc; + $this->_resetTemplate(); + $encdoc = $this->encdoc; + $this->encdoc = $curencdoc; + $refuri = XMLSecurityDSig::generate_GUID(); + $element = $encdoc->documentElement; + $element->setAttribute("Id", $refuri); + $this->references[$name] = array("node" => $node, "type" => $type, "encnode" => $encdoc, "refuri" => $refuri); + } + + public function setNode($node) + { + $this->rawNode = $node; + } + + public function encryptNode($objKey, $replace = true) + { + $data = ''; + if (empty($this->rawNode)) { + throw new Exception('Node to encrypt has not been set'); + } + if (!$objKey instanceof XMLSecurityKey) { + throw new Exception('Invalid Key'); + } + $doc = $this->rawNode->ownerDocument; + $xPath = new DOMXPath($this->encdoc); + $objList = $xPath->query('/xenc:EncryptedData/xenc:CipherData/xenc:CipherValue'); + $cipherValue = $objList->item(0); + if ($cipherValue == null) { + throw new Exception('Error locating CipherValue element within template'); + } + switch ($this->type) { + case (XMLSecEnc::Element): + $data = $doc->saveXML($this->rawNode); + $this->encdoc->documentElement->setAttribute('Type', XMLSecEnc::Element); + break; + case (XMLSecEnc::Content): + $children = $this->rawNode->childNodes; + foreach ($children AS $child) { + $data .= $doc->saveXML($child); + } + $this->encdoc->documentElement->setAttribute('Type', XMLSecEnc::Content); + break; + default: + throw new Exception('Type is currently not supported'); + return; + } + + $encMethod = $this->encdoc->documentElement->appendChild( + $this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptionMethod') + ); + $encMethod->setAttribute('Algorithm', $objKey->getAlgorith()); + $cipherValue->parentNode->parentNode->insertBefore( + $encMethod, + $cipherValue->parentNode->parentNode->firstChild + ); + + $strEncrypt = base64_encode($objKey->encryptData($data)); + $value = $this->encdoc->createTextNode($strEncrypt); + $cipherValue->appendChild($value); + + if ($replace) { + switch ($this->type) { + case (XMLSecEnc::Element): + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + return $this->encdoc; + } + $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true); + $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); + return $importEnc; + break; + case (XMLSecEnc::Content): + $importEnc = $this->rawNode->ownerDocument->importNode($this->encdoc->documentElement, true); + while ($this->rawNode->firstChild) { + $this->rawNode->removeChild($this->rawNode->firstChild); + } + $this->rawNode->appendChild($importEnc); + return $importEnc; + break; + } + } + } + + public function encryptReferences($objKey) + { + $curRawNode = $this->rawNode; + $curType = $this->type; + foreach ($this->references AS $name => $reference) { + $this->encdoc = $reference["encnode"]; + $this->rawNode = $reference["node"]; + $this->type = $reference["type"]; + try { + $encNode = $this->encryptNode($objKey); + $this->references[$name]["encnode"] = $encNode; + } catch (Exception $e) { + $this->rawNode = $curRawNode; + $this->type = $curType; + throw $e; + } + } + $this->rawNode = $curRawNode; + $this->type = $curType; + } + + /** + * Retrieve the CipherValue text from this encrypted node. + * + * @return string|NULL The Ciphervalue text, or NULL if no CipherValue is found. + */ + public function getCipherValue() + { + if (empty($this->rawNode)) { + throw new Exception('Node to decrypt has not been set'); + } + + $doc = $this->rawNode->ownerDocument; + $xPath = new DOMXPath($doc); + $xPath->registerNamespace('xmlencr', XMLSecEnc::XMLENCNS); + /* Only handles embedded content right now and not a reference */ + $query = "./xmlencr:CipherData/xmlencr:CipherValue"; + $nodeset = $xPath->query($query, $this->rawNode); + $node = $nodeset->item(0); + + if (!$node) { + return null; + } + + return base64_decode($node->nodeValue); + } + + /** + * Decrypt this encrypted node. + * + * The behaviour of this function depends on the value of $replace. + * If $replace is FALSE, we will return the decrypted data as a string. + * If $replace is TRUE, we will insert the decrypted element(s) into the + * document, and return the decrypted element(s). + * + * @params XMLSecurityKey $objKey The decryption key that should be used when decrypting the node. + * @params boolean $replace Whether we should replace the encrypted node in the XML document with the decrypted data. The default is TRUE. + * @return string|DOMElement The decrypted data. + */ + public function decryptNode($objKey, $replace = true) + { + if (!$objKey instanceof XMLSecurityKey) { + throw new Exception('Invalid Key'); + } + + $encryptedData = $this->getCipherValue(); + if ($encryptedData) { + $decrypted = $objKey->decryptData($encryptedData); + if ($replace) { + switch ($this->type) { + case (XMLSecEnc::Element): + $newdoc = new DOMDocument(); + $newdoc->loadXML($decrypted); + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + return $newdoc; + } + $importEnc = $this->rawNode->ownerDocument->importNode($newdoc->documentElement, true); + $this->rawNode->parentNode->replaceChild($importEnc, $this->rawNode); + return $importEnc; + break; + case (XMLSecEnc::Content): + if ($this->rawNode->nodeType == XML_DOCUMENT_NODE) { + $doc = $this->rawNode; + } else { + $doc = $this->rawNode->ownerDocument; + } + $newFrag = $doc->createDocumentFragment(); + $newFrag->appendXML($decrypted); + $parent = $this->rawNode->parentNode; + $parent->replaceChild($newFrag, $this->rawNode); + return $parent; + break; + default: + return $decrypted; + } + } else { + return $decrypted; + } + } else { + throw new Exception("Cannot locate encrypted data"); + } + } + + public function encryptKey($srcKey, $rawKey, $append = true) + { + if ((!$srcKey instanceof XMLSecurityKey) || (!$rawKey instanceof XMLSecurityKey)) { + throw new Exception('Invalid Key'); + } + $strEncKey = base64_encode($srcKey->encryptData($rawKey->key)); + $root = $this->encdoc->documentElement; + $encKey = $this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptedKey'); + if ($append) { + $keyInfo = $root->insertBefore( + $this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo'), + $root->firstChild + ); + $keyInfo->appendChild($encKey); + } else { + $this->encKey = $encKey; + } + $encMethod = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:EncryptionMethod')); + $encMethod->setAttribute('Algorithm', $srcKey->getAlgorith()); + if (!empty($srcKey->name)) { + $keyInfo = $encKey->appendChild( + $this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyInfo') + ); + $keyInfo->appendChild( + $this->encdoc->createElementNS('http://www.w3.org/2000/09/xmldsig#', 'dsig:KeyName', $srcKey->name) + ); + } + $cipherData = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:CipherData')); + $cipherData->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:CipherValue', $strEncKey)); + if (is_array($this->references) && count($this->references) > 0) { + $refList = $encKey->appendChild($this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:ReferenceList')); + foreach ($this->references AS $name => $reference) { + $refuri = $reference["refuri"]; + $dataRef = $refList->appendChild( + $this->encdoc->createElementNS(XMLSecEnc::XMLENCNS, 'xenc:DataReference') + ); + $dataRef->setAttribute("URI", '#' . $refuri); + } + } + return; + } + + public function decryptKey($encKey) + { + if (!$encKey->isEncrypted) { + throw new Exception("Key is not Encrypted"); + } + if (empty($encKey->key)) { + throw new Exception("Key is missing data to perform the decryption"); + } + return $this->decryptNode($encKey, false); + } + + public function locateEncryptedData($element) + { + if ($element instanceof DOMDocument) { + $doc = $element; + } else { + $doc = $element->ownerDocument; + } + if ($doc) { + $xpath = new DOMXPath($doc); + $query = "//*[local-name()='EncryptedData' and namespace-uri()='" . XMLSecEnc::XMLENCNS . "']"; + $nodeset = $xpath->query($query); + return $nodeset->item(0); + } + return null; + } + + public function locateKey($node = null) + { + if (empty($node)) { + $node = $this->rawNode; + } + if (!$node instanceof DOMNode) { + return null; + } + if ($doc = $node->ownerDocument) { + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('xmlsecenc', XMLSecEnc::XMLENCNS); + $query = ".//xmlsecenc:EncryptionMethod"; + $nodeset = $xpath->query($query, $node); + if ($encmeth = $nodeset->item(0)) { + $attrAlgorithm = $encmeth->getAttribute("Algorithm"); + try { + $objKey = new XMLSecurityKey($attrAlgorithm, array('type' => 'private')); + } catch (Exception $e) { + return null; + } + return $objKey; + } + } + return null; + } + + static function staticLocateKeyInfo($objBaseKey = null, $node = null) + { + if (empty($node) || (!$node instanceof DOMNode)) { + return null; + } + $doc = $node->ownerDocument; + if (!$doc) { + return null; + } + + $xpath = new DOMXPath($doc); + $xpath->registerNamespace('xmlsecenc', XMLSecEnc::XMLENCNS); + $xpath->registerNamespace('xmlsecdsig', XMLSecurityDSig::XMLDSIGNS); + $query = "./xmlsecdsig:KeyInfo"; + $nodeset = $xpath->query($query, $node); + $encmeth = $nodeset->item(0); + if (!$encmeth) { + /* No KeyInfo in EncryptedData / EncryptedKey. */ + return $objBaseKey; + } + + foreach ($encmeth->childNodes AS $child) { + switch ($child->localName) { + case 'KeyName': + if (!empty($objBaseKey)) { + $objBaseKey->name = $child->nodeValue; + } + break; + case 'KeyValue': + foreach ($child->childNodes AS $keyval) { + switch ($keyval->localName) { + case 'DSAKeyValue': + throw new Exception("DSAKeyValue currently not supported"); + break; + case 'RSAKeyValue': + $modulus = null; + $exponent = null; + if ($modulusNode = $keyval->getElementsByTagName('Modulus')->item(0)) { + $modulus = base64_decode($modulusNode->nodeValue); + } + if ($exponentNode = $keyval->getElementsByTagName('Exponent')->item(0)) { + $exponent = base64_decode($exponentNode->nodeValue); + } + if (empty($modulus) || empty($exponent)) { + throw new Exception("Missing Modulus or Exponent"); + } + $publicKey = XMLSecurityKey::convertRSA($modulus, $exponent); + $objBaseKey->loadKey($publicKey); + break; + } + } + break; + case 'RetrievalMethod': + $type = $child->getAttribute('Type'); + if ($type !== 'http://www.w3.org/2001/04/xmlenc#EncryptedKey') { + /* Unsupported key type. */ + break; + } + $uri = $child->getAttribute('URI'); + if ($uri[0] !== '#') { + /* URI not a reference - unsupported. */ + break; + } + $id = substr($uri, 1); + + $query = "//xmlsecenc:EncryptedKey[@Id='$id']"; + $keyElement = $xpath->query($query)->item(0); + if (!$keyElement) { + throw new Exception("Unable to locate EncryptedKey with @Id='$id'."); + } + + return XMLSecurityKey::fromEncryptedKeyElement($keyElement); + case 'EncryptedKey': + return XMLSecurityKey::fromEncryptedKeyElement($child); + case 'X509Data': + if ($x509certNodes = $child->getElementsByTagName('X509Certificate')) { + if ($x509certNodes->length > 0) { + $x509cert = $x509certNodes->item(0)->textContent; + $x509cert = str_replace(array("\r", "\n"), "", $x509cert); + $x509cert = "-----BEGIN CERTIFICATE-----\n" . chunk_split( + $x509cert, + 64, + "\n" + ) . "-----END CERTIFICATE-----\n"; + $objBaseKey->loadKey($x509cert, false, true); + } + } + break; + } + } + return $objBaseKey; + } + + public function locateKeyInfo($objBaseKey = null, $node = null) + { + if (empty($node)) { + $node = $this->rawNode; + } + return XMLSecEnc::staticLocateKeyInfo($objBaseKey, $node); + } +} \ No newline at end of file diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 8ebbf85..4bf724b 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -43,20 +43,24 @@ namespace SlmIdealPayment\Client; use SimpleXMLElement; +use DOMDocument; use DateTime; +use RunTimeException; use SlmIdealPayment\Request; use SlmIdealPayment\Response; use SlmIdealPayment\Model; -use Zend\Http\Client as HttpClient; +use Zend\Http\Client as HttpClient; use Zend\Http\Response as HttpResponse; use SlmIdealPayment\Exception; -class StandardClient implements ClientInterface +class StandardClient { protected $requestUrl; + protected $merchantId; + protected $subId; protected $publicCertificate; protected $privateCertificate; protected $keyFile; @@ -64,6 +68,12 @@ class StandardClient implements ClientInterface protected $httpClient; + /** + * @var string + */ + const EXPIRATION = 'PT1H'; + const CURRENCY = 'EUR'; + public function getRequestUrl() { return $this->requestUrl; @@ -135,187 +145,243 @@ public function setHttpClient(HttpClient $httpClient) } /** - * {@inheritdoc} + * @param mixed $merchantId */ - public function sendDirectoryRequest(Request\DirectoryRequest $request) + public function setMerchantId($merchantId) { - // Prepare request - $message = $this->createMessage($request, array( - $request->getMerchantId(), - $request->getSubId() - )); - - // Grab result and parse to XML object - $response = $this->send($message); - $xml = $this->extractResponse($response); - - // Create response object - $response = new Response\DirectoryResponse; - $acquirer = (string) $xml->Acquirer->acquirerID; - $response->setAcquirer($acquirer); - - $collection = new Model\IssuerCollection; - foreach ($xml->Directory->children() as $item) { - if ($item->getName() != 'Issuer') { - continue; - } + $this->merchantId = $merchantId; + } - $issuer = new Model\Issuer(); - $issuer->setId((string) $item->issuerID); - $issuer->setName((string) $item->issuerName); - $issuer->setType((string) $item->issuerList); - $collection->append($issuer); - } - $response->setIssuers($collection); + /** + * @return mixed + */ + public function getMerchantId() + { + return $this->merchantId; + } - return $response; + /** + * @param mixed $subId + */ + public function setSubId($subId) + { + $this->subId = $subId; } /** - * {@inheritdoc} + * @return mixed */ - public function sendTransactionRequest(Request\TransactionRequest $request) + public function getSubId() { - // Prepare request - $message = $this->createMessage($request, array( - $request->getIssuer()->getId(), - $request->getMerchantId(), - $request->getSubId(), - $request->getReturnUrl(), - $request->getTransaction()->getPurchaseId(), - $request->getTransaction()->getAmount(), - $request->getTransaction()->getCurrency(), - $request->getTransaction()->getLanguage(), - $request->getTransaction()->getDescription(), - $request->getTransaction()->getEntranceCode() - )); - - $message->Merchant->addChild('merchantReturnURL', $request->getReturnUrl()); - - $issuer = $message->addChild('Issuer'); - $issuer->addChild('issuerID', $request->getIssuer()->getId()); - - $transaction = $message->addChild('Transaction'); - $transaction->addChild('purchaseID', $request->getTransaction()->getPurchaseId()); - $transaction->addChild('amount', $request->getTransaction()->getAmount()); - $transaction->addChild('currency', $request->getTransaction()->getCurrency()); - $transaction->addChild('expirationPeriod', $request->getTransaction()->getExpirationPeriod()); - $transaction->addChild('language', $request->getTransaction()->getLanguage()); - $transaction->addChild('description', $request->getTransaction()->getDescription()); - $transaction->addChild('entranceCode', $request->getTransaction()->getEntranceCode()); - - // Grab result and parse to XML object - $response = $this->send($message); - $xml = $this->extractResponse($response); - - // Create response object - $response = new Response\TransactionResponse; - $acquirer = (string) $xml->Acquirer->acquirerID; - $url = (string) $xml->Issuer->issuerAuthenticationURL; - $response->setAcquirer($acquirer); - $response->setAuthenticationUrl($url); - - $transaction = new Model\Transaction; - $transactionId = (string) $xml->Transaction->transactionID; - $purchaseId = (string) $xml->Transaction->purchaseID; - $transaction->setTransactionId($transactionId); - $transaction->setPurchaseId($purchaseId); - - $response->setTransaction($transaction); - - return $response; + return $this->subId; } + /** - * {@inheritdoc} + * Create request for a list of available issuers + * + * Return array is a key=>value for a shortlist and a longlist of + * issuers available with keys respectively 'short' and 'long'. + * + * @return array */ - public function sendStatusRequest(Request\StatusRequest $request) + public function sendDirectoryRequest() { - // Prepare request - $message = $this->createMessage($request, array( - $request->getMerchantId(), - $request->getSubId(), - $request->getTransaction()->getTransactionId() - )); - - $transaction = $message->addChild('Transaction'); - $transaction->addChild('transactionID', $request->getTransaction()->getTransactionId()); - - // Grab result and parse to XML object - $response = $this->send($message); - $xml = $this->extractResponse($response); - - // Create response object - $response = new Response\StatusResponse; - $acquirer = (string) $xml->Acquirer->acquirerID; - $signature = (string) $xml->Signature->signatureValue; - $fingerprint = (string) $xml->Signature->fingerprint; - $response->setAcquirer($acquirer); - $response->setSignatureValue($signature); - $response->setFingerprint($fingerprint); - - $transaction = new Model\Transaction; - $transactionId = (string) $xml->Transaction->transactionID; - $status = (string) $xml->Transaction->status; - $transaction->setTransactionId($transactionId); - $transaction->setStatus($status); - - $consumer = new Model\Consumer; - $name = (string) $xml->Transaction->consumerName; - $account = (string) $xml->Transaction->consumerAccountNumber; - $city = (string) $xml->Transaction->consumerCity; - $consumer->setName($name); - $consumer->setAccountNumber($account); - $consumer->setCity($city); - $transaction->setConsumer($consumer); - - $response->setTransaction($transaction); - - /** - * @todo Check signature and fingerprint if message is valid - */ - - return $response; + $list = $this->_requestIssuers(); + return $list; } - protected function createMessage(Request\RequestInterface $request, array $signedFields = array()) + /** + * Create a request to start a transaction + * + * Based on the order information, a request is send to the iDeal server + * to request for a url. The url will be used to send the user to, to + * perform the actual transaction. + * + * @param $order Order for which transaction is requested + * @param string $issuerId Issuer to which transaction is requested + * @param string $returnUrl Location to return client after payment + * @param string $language Language of the interface (nl|en is accepted) + * @param string $description Description to be shown in interface (maxlength 32 characters) + * @return string + */ + public function requestTransaction($order, $issuerId, $returnUrl, $language, $description) { - $class = get_class($request); - $class = substr($class, strrpos($class, '\\') + 1); - - switch ($class) { - case 'DirectoryRequest': - $type = 'DirectoryReq'; - break; - case 'TransactionRequest': - $type = 'AcquirerTrxReq'; - break; - case 'StatusRequest': - $type = 'AcquirerStatusReq'; - break; + return true; + $xml = $this->_createXmlForRequestTransaction( + array( + 'issuerId' => $issuerId, + 'merchantId' => $this->_merchantId, + 'subId' => $this->_subId, + 'returnUrl' => $returnUrl, + 'purchaseId' => $order->id, + 'amount' => round($order->amount / 100, 2), + 'expiration' => self::EXPIRATION, + 'currency' => self::CURRENCY, + 'language' => $language, + 'description' => $description, + 'entrance' => $this->getEntranceCode($order) + ) + ); + + $response = $this->_postMessageXml($xml); + + $authenticationUrl = ''; + $transactionId = ''; + $purchaseId = ''; + foreach ($response->children() as $child) { + if ('Issuer' === $child->getName()) { + foreach ($child->children() as $property) { + if ('issuerAuthenticationURL' === $property->getName()) { + $authenticationUrl = (string)$property; + } + } + } elseif ('Transaction' === $child->getName()) { + foreach ($child->children() as $property) { + if ('transactionID' === $property->getName()) { + $transactionId = (string)$property; + } elseif ('purchaseID' === $property->getName()) { + $purchaseId = (string)$property; + } + } + } } - // Start to create message - $xml = new SimpleXMLElement(sprintf('<%1$s>', $type)); - $xml->addAttribute('xmlns', 'http://www.idealdesk.com/Message'); - $xml->addAttribute('version', '1.1.0'); + return array( + 'authenticationUrl' => $authenticationUrl, + 'transactionId' => $transactionId, + 'purchaseId' => $purchaseId + ); + } + + /** + * Create a request to validate the transaction + * + * Every transaction must be validated if the payment is completed. The + * request is made to the iDeal server, based on the given transactionId + * which was a return value for the request for transaction. + * + * In case of a failed transaction, false will be returned. In case of a + * succeeded transaction, true will be returned. + * + * @param string $transactionId + * @return bool + */ + public function requestStatus($transactionId) + { + $xml = $this->_createXmlForRequestStatus( + array( + 'merchantId' => $this->_merchantId, + 'subId' => $this->_subId, + 'transactionId' => $transactionId + ) + ); + + $response = $this->_postMessageXml($xml); + + $transaction = array(); + foreach ($response->children() as $child) { + if ('Transaction' === $child->getName()) { + foreach ($child->children() as $property) { + $transaction[$property->getName()] = (string)$property; + } + } + } + + return array( + 'transaction' => $transaction, + ); + } + + protected function _requestIssuers() + { + $xml = $this->createXmlForRequestIssuers( + array( + 'merchantId' => $this->getMerchantId(), + 'subId' => $this->getSubId(), + ) + ); + + $response = $this->_postMessageXml($xml->saveXML()); + + $xml = new \DOMDocument(); + $xml->loadXML($response); + +// if ('DirectoryRes' !== $xml->getName()) { +// throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); +// } + + $countries = array(); + foreach ($xml->Directory->children() as $child) { + if ('Country' !== $child->getName()) { + continue; + } + $country = (string)$child->countryNames; + + + $list = array(); + foreach ($child->children() as $issuer) { + if ('Issuer' !== $issuer->getName()) { + continue; + } + + $id = (string)$issuer->issuerID; + $name = (string)$issuer->issuerName; + + $list[$id] = $name; + } + + $countries[$country] = $list; + } + + return $countries; + } + + protected function _postMessage(DOMDocument $document) + { + $xml = $document->saveXML(); + + $response = $this->_postMessageXml($xml); + return $this->_parseResponse($response); + } + + protected function _postMessageXml($xml) + { + $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->getRequestUrl()); + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); + curl_setopt($ch, CURLOPT_POST, 1); + curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml')); + curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + + $output = curl_exec($ch); + curl_close($ch); + + return $output; + } - // Every message needs a time stamp - $date = gmdate('Y-m-d\TH:i:s.000\Z'); - $xml->addChild('createDateTimeStamp', $date); + protected function _parseResponse($response) + { + if (!$this->_verify($response)) { + throw new RuntimeException('Response from server is invalid!'); + } - // Set standard fields - $merchant = $xml->addChild('Merchant'); - $merchant->addChild('merchantID', $request->getMerchantId()); - $merchant->addChild('subID', $request->getSubId()); - $merchant->addChild('authentication', 'SHA1_RSA'); + $xml = new SimpleXMLElement($response); - // Set cryptographic fields - $fingerprint = $this->getFingerprint(); - $signature = $this->getMessageSignature($date, $signedFields); + if (isset($xml->Error)) { + $error = $xml->Error; - $merchant->addChild('token', $fingerprint); - $merchant->addChild('tokenCode', $signature); + $message = sprintf( + '%s (%s): %s', + $error->errorMessage, + $error->errorCode, + $error->errorDetail + ); + + throw new RuntimeException($message); + } return $xml; } @@ -325,9 +391,7 @@ protected function getFingerprint($public = false) $certificate = ($public) ? $this->getPublicCertificate() : $this->getPrivateCertificate(); if (false === ($fp = fopen($certificate, 'r'))) { - throw new Exception\CertificateNotFoundException( - sprintf('Cannot find the certificate at %s', $certificate) - ); + throw new RuntimeException('Cannot open certificate file'); } $rawData = fread($fp, 8192); @@ -335,81 +399,198 @@ protected function getFingerprint($public = false) fclose($fp); if (!openssl_x509_export($data, $data)) { - throw new Exception\CertificateNotValidException( - sprintf('Certificate %s cannot be read due to errors in the file', $certificate) - ); + throw new RuntimeException('Error in certificate'); } $data = str_replace(array('-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'), '', $data); return strtoupper(sha1(base64_decode($data))); } - protected function getMessageSignature($timestamp, array $signedFields) + protected function _getSignature($message) { - $message = $timestamp; - foreach ($signedFields as $value) { - $message .= $value; + if (false === ($fp = fopen($this->_keyFile, 'r'))) { + throw new RuntimeException('Cannot open key file'); } - $message = str_replace(array(" ", "\t", "\n"), array('', '', ''), $message); - $keyFile = $this->getKeyFile(); - $keyPwd = $this->getKeyPassword(); - - - if(false === ($fp = @fopen($this->keyFile, 'r'))) { - throw new Exception\CertificateNotFoundException( - sprintf('Cannot find the keyfile at %s', $keyFile) - ); - } - - $key = fread($fp, 8192); + $keyFile = fread($fp, 8192); fclose($fp); - if (!$privateKey = openssl_pkey_get_private($key, $keyPwd)) { - throw new Exception\CertificateNotValidException( - sprintf('Certificate %s cannot be opened with the provided password', $keyFile) - ); + if (!$privateKey = openssl_pkey_get_private($keyFile, $this->_keyPassword)) { + throw new RuntimeException('Invalid password for key file'); } $signature = ''; - if (!openssl_sign($message, $signature, $privateKey)) { - throw new Exception\CertificateNotValidException( - sprintf('Message cannot be signed with certificate %s due to errors in the file', $keyFile) - ); + if (!opensslsign($message, $signature, $privateKey)) { + throw new RuntimeException('Cannot sign message with private key'); } openssl_free_key($privateKey); return base64_encode($signature); } - protected function send(SimpleXMLElement $xml) + public function getEntranceCode(Deal_Model_Order $order) { - $data = $xml->asXml(); + $filter = new Zend_Filter_Alnum; + return $filter->filter($order->id . $order->created_at); + } - $client = $this->getHttpClient(); - $client->setUri($this->getRequestUrl()); - $client->setRawBody($data); - return $client->send(); + protected function sign(DOMDocument $document) + { + $objDSig = new \XMLSecurityDSig(); + $objDSig->setCanonicalMethod(\XMLSecurityDSig::EXC_C14N); + $objDSig->addReference( + $document, + \XMLSecurityDSig::SHA256, + array('http://www.w3.org/2000/09/xmldsig#enveloped-signature'), + array('force_uri' => true) + ); + + $objKey = new \XMLSecurityKey(\XMLSecurityKey::RSA_SHA256, array('type' => 'private')); + $objKey->passphrase = $this->getKeyPassword(); + $objKey->loadKey($this->getKeyFile(), true); + $objDSig->sign($objKey); + + $objDSig->addKeyInfoAndName($this->getFingerprint()); + $objDSig->appendSignature($document->documentElement); + + return $document; } - protected function extractResponse(HttpResponse $response) + protected function _verify($response) { - if (!$response->isOk()) { - throw new Exception\HttpRequestException( - 'Request is not successfully executed' - ); + $document = new DOMDocument(); + $document->loadXML($response); + + $objXMLSecDSig = new XMLSecurityDSig(); + $objDSig = $objXMLSecDSig->locateSignature($document); + + if (!$objDSig) { + throw new Exception("Cannot locate Signature Node"); } + $objXMLSecDSig->canonicalizeSignedInfo(); + $retVal = $objXMLSecDSig->validateReference(); - $body = $response->getBody(); - $xml = simplexml_load_string($body); + if (!$retVal) { + throw new Exception("Reference Validation Failed"); + } - if (isset($xml->Error)) { - $error = $xml->Error; - throw new Exception\IdealRequestException( - sprintf('%s (%s): "%s"', $error->errorMessage, $error->errorCode, $error->errorDetail) - ); + $objKey = $objXMLSecDSig->locateKey(); + if (!$objKey) { + throw new Exception("We have no idea about the key"); } - return $xml; + $objKey->loadKey($this->_publicCertificate, true); + + if ($objXMLSecDSig->verify($objKey)) { + return true; + } + return false; + } + + /** + * Create signed XML to request issuer listing + * + * @param array $data + * @return DOMDocument + */ + protected function createXmlForRequestIssuers(array $data) + { + $timestamp = utf8_encode(gmdate('Y-m-d\TH:i:s.000\Z')); + $merchant = $data['merchantId']; + $subid = $data['subId']; + + $xml = << + + $timestamp + + $merchant + $subid + + +EOT; + $document = new \DOMDocument(); + $document->loadXML($xml); + + // Sign document and return + return $this->sign($document); + } + + protected function _createXmlForRequestTransaction(array $data) + { + $timestamp = utf8_encode(gmdate('Y-m-d\TH:i:s.000\Z')); + $issuer = $data['issuerId']; + + $merchant = $data['merchantId']; + $subid = $data['subId']; + $returnUrl = $data['returnUrl']; + + $purchaseId = $data['purchaseId']; + $amount = $data['amount']; + $currency = $data['currency']; + $expiration = $data['expiration']; + $language = $data['language']; + $description = $data['description']; + $entrance = $data['entrance']; + + $xml = << + + $timestamp + + $issuer + + + $merchant + $subid + $returnUrl + + + $purchaseId + $amount + $currency + $expiration + $language + $description + $entrance + + +EOT; + + $document = new DOMDocument(); + $document->loadXML($xml); + + // Sign document and return + return $this->sign($document); + } + + protected function _createXmlForRequestStatus(array $data) + { + $timestamp = utf8_encode(gmdate('Y-m-d\TH:i:s.000\Z')); + + $merchant = $data['merchantId']; + $subid = $data['subId']; + + $transactionId = $data['transactionId']; + + $xml = << + + $timestamp + + $merchant + $subid + + + $transactionId + + +EOT; + + $document = new DOMDocument(); + $document->loadXML($xml); + + // Sign document and return + return $this->sign($document); } } \ No newline at end of file From 157983422b5723dca52f28c9876dda2b047d07be Mon Sep 17 00:00:00 2001 From: japaveh Date: Sat, 10 Aug 2013 00:17:12 +0200 Subject: [PATCH 02/27] Upgrade to version 3.3.1 --- src/SlmIdealPayment/Client/StandardClient.php | 148 +++++++++++------- src/SlmIdealPayment/Model/Consumer.php | 45 ++++-- src/SlmIdealPayment/Model/Transaction.php | 26 ++- src/SlmIdealPayment/Request/StatusRequest.php | 10 ++ .../Request/TransactionRequest.php | 30 ++++ .../Response/TransactionResponse.php | 20 +++ 6 files changed, 209 insertions(+), 70 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 4bf724b..341d7b7 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -44,7 +44,7 @@ use SimpleXMLElement; use DOMDocument; -use DateTime; + use RunTimeException; use SlmIdealPayment\Request; @@ -53,11 +53,15 @@ use Zend\Http\Client as HttpClient; use Zend\Http\Response as HttpResponse; +use Zend\Math\Rand; use SlmIdealPayment\Exception; class StandardClient { + /** + * @var string + */ protected $requestUrl; protected $merchantId; protected $subId; @@ -198,33 +202,28 @@ public function sendDirectoryRequest() * to request for a url. The url will be used to send the user to, to * perform the actual transaction. * - * @param $order Order for which transaction is requested - * @param string $issuerId Issuer to which transaction is requested - * @param string $returnUrl Location to return client after payment - * @param string $language Language of the interface (nl|en is accepted) - * @param string $description Description to be shown in interface (maxlength 32 characters) - * @return string + * @param Request\TransactionRequest $transactionRequest + * @return array */ - public function requestTransaction($order, $issuerId, $returnUrl, $language, $description) + public function requestTransaction(Request\TransactionRequest $transactionRequest) { - return true; $xml = $this->_createXmlForRequestTransaction( array( - 'issuerId' => $issuerId, - 'merchantId' => $this->_merchantId, - 'subId' => $this->_subId, - 'returnUrl' => $returnUrl, - 'purchaseId' => $order->id, - 'amount' => round($order->amount / 100, 2), + 'issuerId' => $transactionRequest->getIssuer()->getId(), + 'merchantId' => $this->getMerchantId(), + 'subId' => $this->getSubId(), + 'returnUrl' => $transactionRequest->getReturnUrl(), + 'purchaseId' => $transactionRequest->getTransaction()->getPurchaseId(), + 'amount' => round($transactionRequest->getTransaction()->getAmount() / 100, 2), 'expiration' => self::EXPIRATION, 'currency' => self::CURRENCY, - 'language' => $language, - 'description' => $description, - 'entrance' => $this->getEntranceCode($order) + 'language' => $transactionRequest->getTransaction()->getLanguage(), + 'description' => $transactionRequest->getTransaction()->getDescription(), + 'entrance' => $transactionRequest->getTransaction()->getEntranceCode() ) ); - $response = $this->_postMessageXml($xml); + $response = $this->_postMessageXml($xml->saveXML()); $authenticationUrl = ''; $transactionId = ''; @@ -247,11 +246,14 @@ public function requestTransaction($order, $issuerId, $returnUrl, $language, $de } } - return array( - 'authenticationUrl' => $authenticationUrl, - 'transactionId' => $transactionId, - 'purchaseId' => $purchaseId - ); + $response = new Response\TransactionResponse(); + $response->setAuthenticationUrl($authenticationUrl); + $transaction = new Model\Transaction(); + $transaction->setTransactionId($transactionId); + $transaction->setPurchaseId($purchaseId); + $response->setTransaction($transaction); + + return $response; } /** @@ -264,20 +266,20 @@ public function requestTransaction($order, $issuerId, $returnUrl, $language, $de * In case of a failed transaction, false will be returned. In case of a * succeeded transaction, true will be returned. * - * @param string $transactionId - * @return bool + * @param Request\StatusRequest $statusRequest + * @return array */ - public function requestStatus($transactionId) + public function requestStatus(Request\StatusRequest $statusRequest) { $xml = $this->_createXmlForRequestStatus( array( - 'merchantId' => $this->_merchantId, - 'subId' => $this->_subId, - 'transactionId' => $transactionId + 'merchantId' => $this->getMerchantId(), + 'subId' => $this->getSubId(), + 'transactionId' => $statusRequest->getTransaction()->getTransactionId() ) ); - $response = $this->_postMessageXml($xml); + $response = $this->_postMessageXml($xml->saveXML()); $transaction = array(); foreach ($response->children() as $child) { @@ -288,11 +290,29 @@ public function requestStatus($transactionId) } } - return array( - 'transaction' => $transaction, - ); + $transactionModel = new Model\Transaction(); + $transactionModel->setTransactionId($transaction['transactionID']); + $transactionModel->setStatus($transaction['status']); + + $consumer = new Model\Consumer(); + $consumer->setName($transaction['consumerName']); + $consumer->setAccountIBAN($transaction['consumerIBAN']); + $consumer->setAccountBIC($transaction['consumerBIC']); + + $transactionModel->setAmount($transaction['amount']); + $transactionModel->setCurrency($transaction['currency']); + $transactionModel->setConsumer($consumer); + + $transactionResponse = new Response\TransactionResponse(); + $transactionResponse->setTransaction($transactionModel); + + return $transactionResponse; } + /** + * @return Model\Issuer[] + * @throws \RuntimeException + */ protected function _requestIssuers() { $xml = $this->createXmlForRequestIssuers( @@ -302,17 +322,16 @@ protected function _requestIssuers() ) ); + $response = $this->_postMessageXml($xml->saveXML()); - $xml = new \DOMDocument(); - $xml->loadXML($response); -// if ('DirectoryRes' !== $xml->getName()) { -// throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); -// } + if ('DirectoryRes' !== $response->getName()) { + throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); + } $countries = array(); - foreach ($xml->Directory->children() as $child) { + foreach ($response->Directory->children() as $child) { if ('Country' !== $child->getName()) { continue; } @@ -325,15 +344,17 @@ protected function _requestIssuers() continue; } - $id = (string)$issuer->issuerID; - $name = (string)$issuer->issuerName; + $issuerModel = new Model\Issuer(); + $issuerModel->setId((string)$issuer->issuerID); + $issuerModel->setName((string)$issuer->issuerName); - $list[$id] = $name; + $list[] = $issuerModel; } $countries[$country] = $list; } + return $countries; } @@ -345,9 +366,15 @@ protected function _postMessage(DOMDocument $document) return $this->_parseResponse($response); } + /** + * @param $xml + * @return SimpleXMLElement + * @throws \Exception + */ protected function _postMessageXml($xml) { $ch = curl_init(); + curl_setopt($ch, CURLOPT_URL, $this->getRequestUrl()); curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); @@ -359,7 +386,19 @@ protected function _postMessageXml($xml) $output = curl_exec($ch); curl_close($ch); - return $output; + $prev = libxml_use_internal_errors(true); + + $xml = simplexml_load_string($output); + if ($xml === false) { + $errors = ""; + foreach (libxml_get_errors() as $error) { + $errors .= sprintf("%s\n", $error->message); + } + throw new \Exception(sprintf("The response packet could not be successfully parsed: %s", $errors)); + } + libxml_use_internal_errors($prev); + + return $xml; } protected function _parseResponse($response) @@ -430,8 +469,7 @@ protected function _getSignature($message) public function getEntranceCode(Deal_Model_Order $order) { - $filter = new Zend_Filter_Alnum; - return $filter->filter($order->id . $order->created_at); + return Rand::getString(40, implode(array_merge(range('a', 'z'), range('A', 'Z'), range(1, 9)))); } protected function sign(DOMDocument $document) @@ -461,25 +499,25 @@ protected function _verify($response) $document = new DOMDocument(); $document->loadXML($response); - $objXMLSecDSig = new XMLSecurityDSig(); + $objXMLSecDSig = new \XMLSecurityDSig(); $objDSig = $objXMLSecDSig->locateSignature($document); if (!$objDSig) { - throw new Exception("Cannot locate Signature Node"); + throw new \Exception("Cannot locate Signature Node"); } $objXMLSecDSig->canonicalizeSignedInfo(); $retVal = $objXMLSecDSig->validateReference(); if (!$retVal) { - throw new Exception("Reference Validation Failed"); + throw new \Exception("Reference Validation Failed"); } $objKey = $objXMLSecDSig->locateKey(); if (!$objKey) { - throw new Exception("We have no idea about the key"); + throw new \Exception("We have no idea about the key"); } - $objKey->loadKey($this->_publicCertificate, true); + $objKey->loadKey($this->getPublicCertificate(), true); if ($objXMLSecDSig->verify($objKey)) { return true; @@ -511,7 +549,6 @@ protected function createXmlForRequestIssuers(array $data) EOT; $document = new \DOMDocument(); $document->loadXML($xml); - // Sign document and return return $this->sign($document); } @@ -519,7 +556,8 @@ protected function createXmlForRequestIssuers(array $data) protected function _createXmlForRequestTransaction(array $data) { $timestamp = utf8_encode(gmdate('Y-m-d\TH:i:s.000\Z')); - $issuer = $data['issuerId']; + + $issuer = $data['issuerId']; $merchant = $data['merchantId']; $subid = $data['subId']; @@ -533,7 +571,7 @@ protected function _createXmlForRequestTransaction(array $data) $description = $data['description']; $entrance = $data['entrance']; - $xml = << $timestamp @@ -556,11 +594,9 @@ protected function _createXmlForRequestTransaction(array $data) EOT; - $document = new DOMDocument(); $document->loadXML($xml); - // Sign document and return return $this->sign($document); } diff --git a/src/SlmIdealPayment/Model/Consumer.php b/src/SlmIdealPayment/Model/Consumer.php index c39cf12..0d71ea8 100644 --- a/src/SlmIdealPayment/Model/Consumer.php +++ b/src/SlmIdealPayment/Model/Consumer.php @@ -45,7 +45,8 @@ class Consumer { protected $name; - protected $accountNumber; + protected $accountIBAN; + protected $accountBIC; protected $city; public function getName() @@ -59,25 +60,51 @@ public function setName($name) return $this; } - public function getAccountNumber() + /** + * @param mixed $accountBIC + */ + public function setAccountBIC($accountBIC) { - return $this->accountNumber; + $this->accountBIC = $accountBIC; } - public function setAccountNumber($accountNumber) + /** + * @return mixed + */ + public function getAccountBIC() { - $this->accountNumber = $accountNumber; - return $this; + return $this->accountBIC; } - public function getCity() + /** + * @param mixed $accountIBAN + */ + public function setAccountIBAN($accountIBAN) { - return $this->city; + $this->accountIBAN = $accountIBAN; + } + + /** + * @return mixed + */ + public function getAccountIBAN() + { + return $this->accountIBAN; } + /** + * @param mixed $city + */ public function setCity($city) { $this->city = $city; - return $this; + } + + /** + * @return mixed + */ + public function getCity() + { + return $this->city; } } \ No newline at end of file diff --git a/src/SlmIdealPayment/Model/Transaction.php b/src/SlmIdealPayment/Model/Transaction.php index 43f4aa1..2edb7a2 100644 --- a/src/SlmIdealPayment/Model/Transaction.php +++ b/src/SlmIdealPayment/Model/Transaction.php @@ -59,6 +59,9 @@ class Transaction protected $transactionId; protected $status = self::STATUS_UNKNOWN; + /** + * @var Consumer; + */ protected $consumer; protected $language = 'nl'; @@ -137,10 +140,19 @@ public function getStatus() public function setStatus($status) { - if (!in_array($status, array( - self::STATUS_UNKNOWN, self::STATUS_OPEN, self::STATUS_SUCCESS, self::STATUS_FAILURE, self::STATUS_CANCELLED, self::STATUS_EXPIRED - ))) { - throw new Exception\InvalidArgumentException( + if (!in_array( + $status, + array( + self::STATUS_UNKNOWN, + self::STATUS_OPEN, + self::STATUS_SUCCESS, + self::STATUS_FAILURE, + self::STATUS_CANCELLED, + self::STATUS_EXPIRED + ) + ) + ) { + throw new Exception( 'Cannot set status, "%s" is an invalid status', $status ); } @@ -149,10 +161,14 @@ public function setStatus($status) return $this; } + /** + * @return Consumer + * @throws RuntimeException + */ public function getConsumer() { if (self::STATUS_UNKNOWN === $this->getStatus()) { - throw new Exception\RuntimeException( + throw new RuntimeException( 'Cannot get consumer, status of transaction is unkown' ); } diff --git a/src/SlmIdealPayment/Request/StatusRequest.php b/src/SlmIdealPayment/Request/StatusRequest.php index 1c50e91..eae294d 100644 --- a/src/SlmIdealPayment/Request/StatusRequest.php +++ b/src/SlmIdealPayment/Request/StatusRequest.php @@ -46,13 +46,23 @@ class StatusRequest extends AbstractRequest { + /** + * @var Transaction + */ protected $transaction; + /** + * @return Transaction + */ public function getTransaction() { return $this->transaction; } + /** + * @param Transaction $transaction + * @return Transaction + */ public function setTransaction(Transaction $transaction) { $this->transaction = $transaction; diff --git a/src/SlmIdealPayment/Request/TransactionRequest.php b/src/SlmIdealPayment/Request/TransactionRequest.php index c64e68e..b530b55 100644 --- a/src/SlmIdealPayment/Request/TransactionRequest.php +++ b/src/SlmIdealPayment/Request/TransactionRequest.php @@ -47,37 +47,67 @@ class TransactionRequest extends AbstractRequest { + /** + * @var Issuer + */ protected $issuer; + /** + * @var Transaction + */ protected $transaction; + /** + * @var string + */ protected $returnUrl; + /** + * @return Issuer + */ public function getIssuer() { return $this->issuer; } + /** + * @param Issuer $issuer + * @return $this + */ public function setIssuer(Issuer $issuer) { $this->issuer = $issuer; return $this; } + /** + * @return Transaction + */ public function getTransaction() { return $this->transaction; } + /** + * @param Transaction $transaction + * @return TransactionRequest + */ public function setTransaction(Transaction $transaction) { $this->transaction = $transaction; return $this; } + /** + * @return string + */ public function getReturnUrl() { return $this->returnUrl; } + /** + * @param $returnUrl + * @return TransactionRequest + */ public function setReturnUrl($returnUrl) { $this->returnUrl = $returnUrl; diff --git a/src/SlmIdealPayment/Response/TransactionResponse.php b/src/SlmIdealPayment/Response/TransactionResponse.php index 12fe2f1..6079d76 100644 --- a/src/SlmIdealPayment/Response/TransactionResponse.php +++ b/src/SlmIdealPayment/Response/TransactionResponse.php @@ -46,25 +46,45 @@ class TransactionResponse extends AbstractResponse { + /** + * @var string + */ protected $authenticationUrl; + /** + * @var Transaction + */ protected $transaction; + /** + * @return string + */ public function getAuthenticationUrl() { return $this->authenticationUrl; } + /** + * @param $authenticationUrl + * @return TransactionResponse + */ public function setAuthenticationUrl($authenticationUrl) { $this->authenticationUrl = $authenticationUrl; return $this; } + /** + * @return Transaction + */ public function getTransaction() { return $this->transaction; } + /** + * @param Transaction $transaction + * @return TransactionResponse + */ public function setTransaction(Transaction $transaction) { $this->transaction = $transaction; From 5f75ae008a340b3293c35d46ba18947bd6e511c6 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 17:23:04 +0200 Subject: [PATCH 03/27] Implement ClientInterface for StandardClient Enable the interface for the client again and adjust the methods to reflect the required naming and variable types. --- src/SlmIdealPayment/Client/StandardClient.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 341d7b7..b697cd3 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -57,7 +57,7 @@ use SlmIdealPayment\Exception; -class StandardClient +class StandardClient implements ClientInterface { /** * @var string @@ -189,7 +189,7 @@ public function getSubId() * * @return array */ - public function sendDirectoryRequest() + public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) { $list = $this->_requestIssuers(); return $list; @@ -205,7 +205,7 @@ public function sendDirectoryRequest() * @param Request\TransactionRequest $transactionRequest * @return array */ - public function requestTransaction(Request\TransactionRequest $transactionRequest) + public function sendTransactionRequest(Request\TransactionRequest $transactionRequest) { $xml = $this->_createXmlForRequestTransaction( array( @@ -269,7 +269,7 @@ public function requestTransaction(Request\TransactionRequest $transactionReques * @param Request\StatusRequest $statusRequest * @return array */ - public function requestStatus(Request\StatusRequest $statusRequest) + public function sendStatusRequest(Request\StatusRequest $statusRequest) { $xml = $this->_createXmlForRequestStatus( array( From 040fb0e5c5ed134c0458b46bfbb7176eaa9502ea Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 17:24:52 +0200 Subject: [PATCH 04/27] Use @inheritdoc as method documentation --- src/SlmIdealPayment/Client/StandardClient.php | 28 ++----------------- 1 file changed, 3 insertions(+), 25 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index b697cd3..450a9f5 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -182,12 +182,7 @@ public function getSubId() /** - * Create request for a list of available issuers - * - * Return array is a key=>value for a shortlist and a longlist of - * issuers available with keys respectively 'short' and 'long'. - * - * @return array + * {@inheritdoc} */ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) { @@ -196,14 +191,7 @@ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) } /** - * Create a request to start a transaction - * - * Based on the order information, a request is send to the iDeal server - * to request for a url. The url will be used to send the user to, to - * perform the actual transaction. - * - * @param Request\TransactionRequest $transactionRequest - * @return array + * {@inheritdoc} */ public function sendTransactionRequest(Request\TransactionRequest $transactionRequest) { @@ -257,17 +245,7 @@ public function sendTransactionRequest(Request\TransactionRequest $transactionRe } /** - * Create a request to validate the transaction - * - * Every transaction must be validated if the payment is completed. The - * request is made to the iDeal server, based on the given transactionId - * which was a return value for the request for transaction. - * - * In case of a failed transaction, false will be returned. In case of a - * succeeded transaction, true will be returned. - * - * @param Request\StatusRequest $statusRequest - * @return array + * {@inheritdoc} */ public function sendStatusRequest(Request\StatusRequest $statusRequest) { From c792253f4c3f11b27e6f55b6b869c18e706f0b1a Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 17:26:11 +0200 Subject: [PATCH 05/27] Remove _requestIssuers() method --- src/SlmIdealPayment/Client/StandardClient.php | 92 +++++++++---------- 1 file changed, 41 insertions(+), 51 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 450a9f5..9511942 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -186,8 +186,47 @@ public function getSubId() */ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) { - $list = $this->_requestIssuers(); - return $list; + $xml = $this->createXmlForRequestIssuers( + array( + 'merchantId' => $this->getMerchantId(), + 'subId' => $this->getSubId(), + ) + ); + + + $response = $this->_postMessageXml($xml->saveXML()); + + + if ('DirectoryRes' !== $response->getName()) { + throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); + } + + $countries = array(); + foreach ($response->Directory->children() as $child) { + if ('Country' !== $child->getName()) { + continue; + } + $country = (string)$child->countryNames; + + + $list = array(); + foreach ($child->children() as $issuer) { + if ('Issuer' !== $issuer->getName()) { + continue; + } + + $issuerModel = new Model\Issuer(); + $issuerModel->setId((string)$issuer->issuerID); + $issuerModel->setName((string)$issuer->issuerName); + + $list[] = $issuerModel; + } + + $countries[$country] = $list; + } + + + return $countries; } /** @@ -287,55 +326,6 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) return $transactionResponse; } - /** - * @return Model\Issuer[] - * @throws \RuntimeException - */ - protected function _requestIssuers() - { - $xml = $this->createXmlForRequestIssuers( - array( - 'merchantId' => $this->getMerchantId(), - 'subId' => $this->getSubId(), - ) - ); - - - $response = $this->_postMessageXml($xml->saveXML()); - - - if ('DirectoryRes' !== $response->getName()) { - throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); - } - - $countries = array(); - foreach ($response->Directory->children() as $child) { - if ('Country' !== $child->getName()) { - continue; - } - $country = (string)$child->countryNames; - - - $list = array(); - foreach ($child->children() as $issuer) { - if ('Issuer' !== $issuer->getName()) { - continue; - } - - $issuerModel = new Model\Issuer(); - $issuerModel->setId((string)$issuer->issuerID); - $issuerModel->setName((string)$issuer->issuerName); - - $list[] = $issuerModel; - } - - $countries[$country] = $list; - } - - - return $countries; - } - protected function _postMessage(DOMDocument $document) { $xml = $document->saveXML(); From 9b1616d8c28b1bfae09e052b9a20a9d75ae98d1f Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 17:28:05 +0200 Subject: [PATCH 06/27] Remove unused methods --- src/SlmIdealPayment/Client/StandardClient.php | 27 ------------------- 1 file changed, 27 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 9511942..5175128 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -413,33 +413,6 @@ protected function getFingerprint($public = false) return strtoupper(sha1(base64_decode($data))); } - protected function _getSignature($message) - { - if (false === ($fp = fopen($this->_keyFile, 'r'))) { - throw new RuntimeException('Cannot open key file'); - } - - $keyFile = fread($fp, 8192); - fclose($fp); - - if (!$privateKey = openssl_pkey_get_private($keyFile, $this->_keyPassword)) { - throw new RuntimeException('Invalid password for key file'); - } - - $signature = ''; - if (!opensslsign($message, $signature, $privateKey)) { - throw new RuntimeException('Cannot sign message with private key'); - } - - openssl_free_key($privateKey); - return base64_encode($signature); - } - - public function getEntranceCode(Deal_Model_Order $order) - { - return Rand::getString(40, implode(array_merge(range('a', 'z'), range('A', 'Z'), range(1, 9)))); - } - protected function sign(DOMDocument $document) { $objDSig = new \XMLSecurityDSig(); From ded7ca72f1d9a079a055a7d1fa99c9597bc7f247 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 17:40:37 +0200 Subject: [PATCH 07/27] Use Zend\Http\Client for http calls --- src/SlmIdealPayment/Client/StandardClient.php | 75 ++++++------------- 1 file changed, 21 insertions(+), 54 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 5175128..13e5e8f 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -193,8 +193,8 @@ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) ) ); - - $response = $this->_postMessageXml($xml->saveXML()); + $response = $this->send($xml); + $response = $this->extractResponse($response); if ('DirectoryRes' !== $response->getName()) { @@ -250,7 +250,8 @@ public function sendTransactionRequest(Request\TransactionRequest $transactionRe ) ); - $response = $this->_postMessageXml($xml->saveXML()); + $response = $this->send($xml); + $response = $this->extractResponse($response); $authenticationUrl = ''; $transactionId = ''; @@ -296,7 +297,8 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) ) ); - $response = $this->_postMessageXml($xml->saveXML()); + $response = $this->send($xml); + $response = $this->extractResponse($response); $transaction = array(); foreach ($response->children() as $child) { @@ -326,68 +328,33 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) return $transactionResponse; } - protected function _postMessage(DOMDocument $document) + protected function send(DOMDocument $document) { - $xml = $document->saveXML(); - - $response = $this->_postMessageXml($xml); - return $this->_parseResponse($response); - } + $data = $document->saveXML(); - /** - * @param $xml - * @return SimpleXMLElement - * @throws \Exception - */ - protected function _postMessageXml($xml) - { - $ch = curl_init(); - - curl_setopt($ch, CURLOPT_URL, $this->getRequestUrl()); - curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 0); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0); - curl_setopt($ch, CURLOPT_POST, 1); - curl_setopt($ch, CURLOPT_HTTPHEADER, array('Content-Type: text/xml')); - curl_setopt($ch, CURLOPT_POSTFIELDS, $xml); - curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); - - $output = curl_exec($ch); - curl_close($ch); - - $prev = libxml_use_internal_errors(true); - - $xml = simplexml_load_string($output); - if ($xml === false) { - $errors = ""; - foreach (libxml_get_errors() as $error) { - $errors .= sprintf("%s\n", $error->message); - } - throw new \Exception(sprintf("The response packet could not be successfully parsed: %s", $errors)); - } - libxml_use_internal_errors($prev); + $client = $this->getHttpClient(); + $client->setUri($this->getRequestUrl()); + $client->setRawBody($data); - return $xml; + return $client->send(); } - protected function _parseResponse($response) + protected function extractResponse(HttpResponse $response) { - if (!$this->_verify($response)) { - throw new RuntimeException('Response from server is invalid!'); + if (!$response->isOk()) { + throw new Exception\HttpRequestException( + 'Request is not successfully executed' + ); } - $xml = new SimpleXMLElement($response); + $body = $response->getBody(); + $xml = simplexml_load_string($body); if (isset($xml->Error)) { $error = $xml->Error; - - $message = sprintf( - '%s (%s): %s', - $error->errorMessage, - $error->errorCode, - $error->errorDetail + throw new Exception\IdealRequestException( + sprintf('%s (%s): "%s"', $error->errorMessage, $error->errorCode, $error->errorDetail) ); - - throw new RuntimeException($message); } return $xml; From 2dbe9993fd1620781c3e427f51b92a2a9233b2af Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 17:54:27 +0200 Subject: [PATCH 08/27] Mark currency and language as read-only properties According to the iDEAL documentation, the only supported currency is euro and the default and recommended language is Dutch. Therefore, to protect users from changing it remove the setters. --- src/SlmIdealPayment/Model/Transaction.php | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/SlmIdealPayment/Model/Transaction.php b/src/SlmIdealPayment/Model/Transaction.php index 2edb7a2..e598716 100644 --- a/src/SlmIdealPayment/Model/Transaction.php +++ b/src/SlmIdealPayment/Model/Transaction.php @@ -187,20 +187,8 @@ public function getLanguage() return $this->language; } - public function setLanguage($language) - { - $this->language = $language; - return $this; - } - public function getCurrency() { return $this->currency; } - - public function setCurrency($currency) - { - $this->currency = $currency; - return $this; - } } \ No newline at end of file From 128e2630c9318cfb53905bf74ef8716470b4cd4c Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 18:00:29 +0200 Subject: [PATCH 09/27] Load expiration period and currency from transaction The transaction will contain default values to ease usage. --- src/SlmIdealPayment/Client/StandardClient.php | 10 ++------ src/SlmIdealPayment/Model/Transaction.php | 24 ++++++++++++------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 13e5e8f..9b731ec 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -72,12 +72,6 @@ class StandardClient implements ClientInterface protected $httpClient; - /** - * @var string - */ - const EXPIRATION = 'PT1H'; - const CURRENCY = 'EUR'; - public function getRequestUrl() { return $this->requestUrl; @@ -242,8 +236,8 @@ public function sendTransactionRequest(Request\TransactionRequest $transactionRe 'returnUrl' => $transactionRequest->getReturnUrl(), 'purchaseId' => $transactionRequest->getTransaction()->getPurchaseId(), 'amount' => round($transactionRequest->getTransaction()->getAmount() / 100, 2), - 'expiration' => self::EXPIRATION, - 'currency' => self::CURRENCY, + 'expiration' => $transactionRequest->getTransaction()->getExpirationPeriod(), + 'currency' => $transactionRequest->getTransaction()->getCurrency(), 'language' => $transactionRequest->getTransaction()->getLanguage(), 'description' => $transactionRequest->getTransaction()->getDescription(), 'entrance' => $transactionRequest->getTransaction()->getEntranceCode() diff --git a/src/SlmIdealPayment/Model/Transaction.php b/src/SlmIdealPayment/Model/Transaction.php index e598716..224c555 100644 --- a/src/SlmIdealPayment/Model/Transaction.php +++ b/src/SlmIdealPayment/Model/Transaction.php @@ -44,26 +44,34 @@ class Transaction { - const STATUS_UNKNOWN = 'Unknown'; - const STATUS_OPEN = 'Open'; - const STATUS_SUCCESS = 'Success'; - const STATUS_FAILURE = 'Failure'; - const STATUS_CANCELLED = 'Cancelled'; - const STATUS_EXPIRED = 'Expired'; + const STATUS_UNKNOWN = 'Unknown'; + const STATUS_OPEN = 'Open'; + const STATUS_SUCCESS = 'Success'; + const STATUS_FAILURE = 'Failure'; + const STATUS_CANCELLED = 'Cancelled'; + const STATUS_EXPIRED = 'Expired'; + const DEFAULT_EXPIRATION = 'PT15M'; + + /**#@+ + * @var string + */ protected $purchaseId; protected $amount; - protected $expirationPeriod; + protected $expirationPeriod = self::DEFAULT_EXPIRATION; protected $description; protected $entranceCode; protected $transactionId; - protected $status = self::STATUS_UNKNOWN; + /** * @var Consumer; */ protected $consumer; + /**#@+ + * @var string + */ protected $language = 'nl'; protected $currency = 'EUR'; From 51e78ac5bf6fbd8ae1304e60f77b3a6c79bf6156 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 18:02:27 +0200 Subject: [PATCH 10/27] Place setCurrency back for status requests --- src/SlmIdealPayment/Model/Transaction.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/SlmIdealPayment/Model/Transaction.php b/src/SlmIdealPayment/Model/Transaction.php index 224c555..c2c6c02 100644 --- a/src/SlmIdealPayment/Model/Transaction.php +++ b/src/SlmIdealPayment/Model/Transaction.php @@ -199,4 +199,10 @@ public function getCurrency() { return $this->currency; } + + public function setCurrency($currency) + { + $this->currency = $currency; + return $this; + } } \ No newline at end of file From 90999e5fc5c4d8de8827795e98ac5cef0eb75b17 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 18:13:56 +0200 Subject: [PATCH 11/27] Validate response in send() method --- src/SlmIdealPayment/Client/StandardClient.php | 25 +++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 9b731ec..3965719 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -59,7 +59,7 @@ class StandardClient implements ClientInterface { - /** + /**#@+ * @var string */ protected $requestUrl; @@ -70,6 +70,9 @@ class StandardClient implements ClientInterface protected $keyFile; protected $keyPassword; + /** + * @var HttpClient + */ protected $httpClient; public function getRequestUrl() @@ -188,8 +191,6 @@ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) ); $response = $this->send($xml); - $response = $this->extractResponse($response); - if ('DirectoryRes' !== $response->getName()) { throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); @@ -245,7 +246,6 @@ public function sendTransactionRequest(Request\TransactionRequest $transactionRe ); $response = $this->send($xml); - $response = $this->extractResponse($response); $authenticationUrl = ''; $transactionId = ''; @@ -292,7 +292,6 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) ); $response = $this->send($xml); - $response = $this->extractResponse($response); $transaction = array(); foreach ($response->children() as $child) { @@ -324,24 +323,24 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) protected function send(DOMDocument $document) { - $data = $document->saveXML(); - $client = $this->getHttpClient(); $client->setUri($this->getRequestUrl()); - $client->setRawBody($data); + $client->setRawBody($document->saveXML()); - return $client->send(); - } + $response = $client->send(); - protected function extractResponse(HttpResponse $response) - { if (!$response->isOk()) { + // @todo supply status code + message throw new Exception\HttpRequestException( 'Request is not successfully executed' ); } $body = $response->getBody(); + if (!$this->isValid($body)) { + throw new Exception\IdealRequestException('iDEAL response is invalid'); + } + $xml = simplexml_load_string($body); if (isset($xml->Error)) { @@ -396,7 +395,7 @@ protected function sign(DOMDocument $document) return $document; } - protected function _verify($response) + protected function isValid($response) { $document = new DOMDocument(); $document->loadXML($response); From d31d5a74d406d803e34fc5168635e8321a56c9af Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 19:10:45 +0200 Subject: [PATCH 12/27] Use only DOMDocument for XML manipulation Remove all SimpleXML syntax to only support DOMDocument, DOMElement and DOMNode syntax. This is an in-progress commit to use only DOM manipulation. All HEREDOC strings must be converted into DOMNodes as well. --- src/SlmIdealPayment/Client/StandardClient.php | 164 ++++++++---------- 1 file changed, 71 insertions(+), 93 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 3965719..3acd73a 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -42,10 +42,8 @@ namespace SlmIdealPayment\Client; -use SimpleXMLElement; use DOMDocument; - -use RunTimeException; +use DOMNode; use SlmIdealPayment\Request; use SlmIdealPayment\Response; @@ -53,7 +51,6 @@ use Zend\Http\Client as HttpClient; use Zend\Http\Response as HttpResponse; -use Zend\Math\Rand; use SlmIdealPayment\Exception; @@ -177,42 +174,31 @@ public function getSubId() return $this->subId; } - /** * {@inheritdoc} */ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) { - $xml = $this->createXmlForRequestIssuers( - array( - 'merchantId' => $this->getMerchantId(), - 'subId' => $this->getSubId(), - ) - ); + $xml = $this->createXmlForRequestIssuers(array( + 'merchantId' => $this->getMerchantId(), + 'subId' => $this->getSubId(), + )); $response = $this->send($xml); - if ('DirectoryRes' !== $response->getName()) { - throw new \RuntimeException('iDeal error: expects DirectoryRes as root element'); + if ('DirectoryRes' !== $response->firstChild->nodeName) { + throw new Exception\IdealRequestException('Expecting DirectoryRes as root element in response'); } $countries = array(); - foreach ($response->Directory->children() as $child) { - if ('Country' !== $child->getName()) { - continue; - } - $country = (string)$child->countryNames; - + foreach ($response->getElementsByTagName('Country') as $child) { + $country = $child->getElementsByTagName('countryNames')->item(0)->textContent; $list = array(); - foreach ($child->children() as $issuer) { - if ('Issuer' !== $issuer->getName()) { - continue; - } - + foreach ($child->getElementsByTagName('Issuer') as $issuer) { $issuerModel = new Model\Issuer(); - $issuerModel->setId((string)$issuer->issuerID); - $issuerModel->setName((string)$issuer->issuerName); + $issuerModel->setId($this->getTag($issuer, 'issuerID')); + $issuerModel->setName($this->getTag($issuer, 'issuerName')); $list[] = $issuerModel; } @@ -220,7 +206,7 @@ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) $countries[$country] = $list; } - + // @todo create a DirectoryResponse and insert all issuers there return $countries; } @@ -229,50 +215,34 @@ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) */ public function sendTransactionRequest(Request\TransactionRequest $transactionRequest) { - $xml = $this->_createXmlForRequestTransaction( - array( - 'issuerId' => $transactionRequest->getIssuer()->getId(), - 'merchantId' => $this->getMerchantId(), - 'subId' => $this->getSubId(), - 'returnUrl' => $transactionRequest->getReturnUrl(), - 'purchaseId' => $transactionRequest->getTransaction()->getPurchaseId(), - 'amount' => round($transactionRequest->getTransaction()->getAmount() / 100, 2), - 'expiration' => $transactionRequest->getTransaction()->getExpirationPeriod(), - 'currency' => $transactionRequest->getTransaction()->getCurrency(), - 'language' => $transactionRequest->getTransaction()->getLanguage(), - 'description' => $transactionRequest->getTransaction()->getDescription(), - 'entrance' => $transactionRequest->getTransaction()->getEntranceCode() - ) - ); + $xml = $this->_createXmlForRequestTransaction(array( + 'issuerId' => $transactionRequest->getIssuer()->getId(), + 'merchantId' => $this->getMerchantId(), + 'subId' => $this->getSubId(), + 'returnUrl' => $transactionRequest->getReturnUrl(), + 'purchaseId' => $transactionRequest->getTransaction()->getPurchaseId(), + 'amount' => $transactionRequest->getTransaction()->getAmount(), + 'expiration' => $transactionRequest->getTransaction()->getExpirationPeriod(), + 'currency' => $transactionRequest->getTransaction()->getCurrency(), + 'language' => $transactionRequest->getTransaction()->getLanguage(), + 'description' => $transactionRequest->getTransaction()->getDescription(), + 'entrance' => $transactionRequest->getTransaction()->getEntranceCode() + )); $response = $this->send($xml); - $authenticationUrl = ''; - $transactionId = ''; - $purchaseId = ''; - foreach ($response->children() as $child) { - if ('Issuer' === $child->getName()) { - foreach ($child->children() as $property) { - if ('issuerAuthenticationURL' === $property->getName()) { - $authenticationUrl = (string)$property; - } - } - } elseif ('Transaction' === $child->getName()) { - foreach ($child->children() as $property) { - if ('transactionID' === $property->getName()) { - $transactionId = (string)$property; - } elseif ('purchaseID' === $property->getName()) { - $purchaseId = (string)$property; - } - } - } + if ('AcquirerTrxRes' !== $response->firstChild->nodeName) { + throw new Exception\IdealRequestException('Expecting AcquirerTrxRes as root element in response'); } - $response = new Response\TransactionResponse(); - $response->setAuthenticationUrl($authenticationUrl); + $url = $this->getTag($response, 'issuerAuthenticationURL'); + $transaction = new Model\Transaction(); - $transaction->setTransactionId($transactionId); - $transaction->setPurchaseId($purchaseId); + $transaction->setTransactionId($this->getTag($response, 'transactionID')); + $transaction->setPurchaseId($this->getTag($response, 'purchaseID')); + + $response = new Response\TransactionResponse(); + $response->setAuthenticationUrl($url); $response->setTransaction($transaction); return $response; @@ -293,32 +263,28 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) $response = $this->send($xml); - $transaction = array(); - foreach ($response->children() as $child) { - if ('Transaction' === $child->getName()) { - foreach ($child->children() as $property) { - $transaction[$property->getName()] = (string)$property; - } - } + if ('AcquirerStatusRes' !== $response->firstChild->nodeName) { + throw new Exception\IdealRequestException('Expecting AcquirerStatusRes as root element in response'); } - $transactionModel = new Model\Transaction(); - $transactionModel->setTransactionId($transaction['transactionID']); - $transactionModel->setStatus($transaction['status']); + $transaction = new Model\Transaction(); + $transaction->setTransactionId($this->getTag($response, 'transactionID')); + $transaction->setStatus($this->getTag($response, 'status')); + // statusDateTimestamp $consumer = new Model\Consumer(); - $consumer->setName($transaction['consumerName']); - $consumer->setAccountIBAN($transaction['consumerIBAN']); - $consumer->setAccountBIC($transaction['consumerBIC']); + $consumer->setName($this->getTag($response, 'consumerName')); + $consumer->setAccountIBAN($this->getTag($response, 'consumerIBAN')); + $consumer->setAccountBIC($this->getTag($response, 'consumerBIC')); - $transactionModel->setAmount($transaction['amount']); - $transactionModel->setCurrency($transaction['currency']); - $transactionModel->setConsumer($consumer); + $transaction->setAmount($this->getTag($response, 'amount')); + $transaction->setCurrency($this->getTag($response, 'currency')); + $transaction->setConsumer($consumer); - $transactionResponse = new Response\TransactionResponse(); - $transactionResponse->setTransaction($transactionModel); + $response = new Response\TransactionResponse(); + $response->setTransaction($transaction); - return $transactionResponse; + return $response; } protected function send(DOMDocument $document) @@ -341,16 +307,23 @@ protected function send(DOMDocument $document) throw new Exception\IdealRequestException('iDEAL response is invalid'); } - $xml = simplexml_load_string($body); + $document = new DOMDocument; + $document->loadXML($body); + + $errors = $document->getElementsByTagName('Error'); + if ($errors->length !== 0) { + $error = $errors->item(0); + + $code = $this->getTag($error, 'errorCode'); + $message = $this->getTag($error, 'errorMessage'); + $detail = $this->getTag($error, 'errorDetail'); - if (isset($xml->Error)) { - $error = $xml->Error; throw new Exception\IdealRequestException( - sprintf('%s (%s): "%s"', $error->errorMessage, $error->errorCode, $error->errorDetail) + sprintf('%s (%s): "%s"', $message, $code, $detail) ); } - return $xml; + return $document; } protected function getFingerprint($public = false) @@ -358,7 +331,7 @@ protected function getFingerprint($public = false) $certificate = ($public) ? $this->getPublicCertificate() : $this->getPrivateCertificate(); if (false === ($fp = fopen($certificate, 'r'))) { - throw new RuntimeException('Cannot open certificate file'); + throw new Exception\CertificateNotFoundException('Cannot open certificate file'); } $rawData = fread($fp, 8192); @@ -366,7 +339,7 @@ protected function getFingerprint($public = false) fclose($fp); if (!openssl_x509_export($data, $data)) { - throw new RuntimeException('Error in certificate'); + throw new Exception\CertificateNotValidException('Error in certificate'); } $data = str_replace(array('-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'), '', $data); @@ -426,6 +399,11 @@ protected function isValid($response) return false; } + protected function getTag(DOMNode $element, $tag) + { + return $element->getElementsByTagName($tag)->item(0)->textContent; + } + /** * Create signed XML to request issuer listing * @@ -448,9 +426,9 @@ protected function createXmlForRequestIssuers(array $data) EOT; - $document = new \DOMDocument(); + $document = new DOMDocument(); $document->loadXML($xml); - // Sign document and return + return $this->sign($document); } From ba4a7ffafaf501a5e58eb6c1db557d9be3c201e0 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 21:08:21 +0200 Subject: [PATCH 13/27] Use Signature object to sign and validate the XML The sign and validation process is abstracted to ease the testing of the client. --- config/services.config.php | 3 +- src/SlmIdealPayment/Client/StandardClient.php | 69 +++++------------- .../Client/StandardClient/Signature.php | 70 +++++++++++++++++++ 3 files changed, 87 insertions(+), 55 deletions(-) create mode 100644 src/SlmIdealPayment/Client/StandardClient/Signature.php diff --git a/config/services.config.php b/config/services.config.php index 235ac85..8ecbae7 100644 --- a/config/services.config.php +++ b/config/services.config.php @@ -38,11 +38,10 @@ * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @link http://japaveh.nl */ + use SlmIdealPayment\Client\StandardClient; use Zend\Http\Client as HttpClient; -include __DIR__ . '/../data/xmlseclibs.php'; - return array( 'factories' => array( 'SlmIdealPayment\Client\StandardClient' => function ($sm) { diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 3acd73a..f8066a6 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -48,6 +48,7 @@ use SlmIdealPayment\Request; use SlmIdealPayment\Response; use SlmIdealPayment\Model; +use SlmIdealPayment\Client\StandardClient\Signature; use Zend\Http\Client as HttpClient; use Zend\Http\Response as HttpResponse; @@ -303,13 +304,14 @@ protected function send(DOMDocument $document) } $body = $response->getBody(); - if (!$this->isValid($body)) { - throw new Exception\IdealRequestException('iDEAL response is invalid'); - } $document = new DOMDocument; $document->loadXML($body); + if (!$this->isValid($document)) { + throw new Exception\IdealRequestException('iDEAL response is invalid'); + } + $errors = $document->getElementsByTagName('Error'); if ($errors->length !== 0) { $error = $errors->item(0); @@ -348,55 +350,14 @@ protected function getFingerprint($public = false) protected function sign(DOMDocument $document) { - $objDSig = new \XMLSecurityDSig(); - $objDSig->setCanonicalMethod(\XMLSecurityDSig::EXC_C14N); - $objDSig->addReference( - $document, - \XMLSecurityDSig::SHA256, - array('http://www.w3.org/2000/09/xmldsig#enveloped-signature'), - array('force_uri' => true) - ); - - $objKey = new \XMLSecurityKey(\XMLSecurityKey::RSA_SHA256, array('type' => 'private')); - $objKey->passphrase = $this->getKeyPassword(); - $objKey->loadKey($this->getKeyFile(), true); - $objDSig->sign($objKey); - - $objDSig->addKeyInfoAndName($this->getFingerprint()); - $objDSig->appendSignature($document->documentElement); - - return $document; + $signature = new Signature; + $signature->sign($document, $this->getFingerprint(), $this->getKeyFile(), $this->getKeyPassword()); } - protected function isValid($response) + protected function isValid(DOMDocument $document) { - $document = new DOMDocument(); - $document->loadXML($response); - - $objXMLSecDSig = new \XMLSecurityDSig(); - $objDSig = $objXMLSecDSig->locateSignature($document); - - if (!$objDSig) { - throw new \Exception("Cannot locate Signature Node"); - } - $objXMLSecDSig->canonicalizeSignedInfo(); - $retVal = $objXMLSecDSig->validateReference(); - - if (!$retVal) { - throw new \Exception("Reference Validation Failed"); - } - - $objKey = $objXMLSecDSig->locateKey(); - if (!$objKey) { - throw new \Exception("We have no idea about the key"); - } - - $objKey->loadKey($this->getPublicCertificate(), true); - - if ($objXMLSecDSig->verify($objKey)) { - return true; - } - return false; + $signature = new Signature; + return $signature->verify($document, $this->getPublicCertificate()); } protected function getTag(DOMNode $element, $tag) @@ -429,7 +390,8 @@ protected function createXmlForRequestIssuers(array $data) $document = new DOMDocument(); $document->loadXML($xml); - return $this->sign($document); + $this->sign($document); + return $document; } protected function _createXmlForRequestTransaction(array $data) @@ -476,7 +438,8 @@ protected function _createXmlForRequestTransaction(array $data) $document = new DOMDocument(); $document->loadXML($xml); - return $this->sign($document); + $this->sign($document); + return $document; } protected function _createXmlForRequestStatus(array $data) @@ -505,7 +468,7 @@ protected function _createXmlForRequestStatus(array $data) $document = new DOMDocument(); $document->loadXML($xml); - // Sign document and return - return $this->sign($document); + $this->sign($document); + return $document; } } \ No newline at end of file diff --git a/src/SlmIdealPayment/Client/StandardClient/Signature.php b/src/SlmIdealPayment/Client/StandardClient/Signature.php new file mode 100644 index 0000000..8afb440 --- /dev/null +++ b/src/SlmIdealPayment/Client/StandardClient/Signature.php @@ -0,0 +1,70 @@ +setCanonicalMethod(XMLSecurityDSig::EXC_C14N); + $dsig->addReference($document, XMLSecurityDSig::SHA256, + array('http://www.w3.org/2000/09/xmldsig#enveloped-signature'), + array('force_uri' => true) + ); + + $key = new XMLSecurityKey(XMLSecurityKey::RSA_SHA256, array('type' => 'private')); + if ($passphrase !== null) { + $key->passphrase = $passphrase; + } + + $key->loadKey($keyfile, true); + $dsig->sign($key); + + $dsig->addKeyInfoAndName($fingerprint); + $dsig->appendSignature($document->documentElement); + } + + public function verify(DOMDocument $document, $certificate) + { + $dsig = new XMLSecurityDSig(); + + if (!$dsig->locateSignature($document)) { + throw new \Exception("Cannot locate Signature Node"); + } + + $dsig->canonicalizeSignedInfo(); + + if (!$dsig->validateReference()) { + throw new \Exception("Reference Validation Failed"); + } + + $key = $dsig->locateKey(); + if (!$key) { + throw new \Exception("We have no idea about the key"); + } + + $key->loadKey($certificate, true); + + return (bool) $dsig->verify($key); + } +} \ No newline at end of file From 237128e7d6e1d40031ecd1073e2c5cc7e3be4bc0 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 22:16:29 +0200 Subject: [PATCH 14/27] Remove autoloading files As of common practice, remove autoloading files and only rely on autoloading inside the Module class or autoloading via composer. --- Module.php | 3 --- autoload_classmap.php | 25 ------------------------- autoload_function.php | 12 ------------ autoload_register.php | 2 -- 4 files changed, 42 deletions(-) delete mode 100644 autoload_classmap.php delete mode 100644 autoload_function.php delete mode 100644 autoload_register.php diff --git a/Module.php b/Module.php index 87e4c49..f8c5300 100644 --- a/Module.php +++ b/Module.php @@ -51,9 +51,6 @@ class Module implements public function getAutoloaderConfig() { return array( - 'Zend\Loader\ClassMapAutoloader' => array( - __DIR__ . '/autoload_classmap.php', - ), 'Zend\Loader\StandardAutoloader' => array( 'namespaces' => array( __NAMESPACE__ => __DIR__ . '/src/' . __NAMESPACE__ diff --git a/autoload_classmap.php b/autoload_classmap.php deleted file mode 100644 index 307cea2..0000000 --- a/autoload_classmap.php +++ /dev/null @@ -1,25 +0,0 @@ - __DIR__ . '/src/SlmIdealPayment/Client/ClientInterface.php', - 'SlmIdealPayment\Client\StandardClient' => __DIR__ . '/src/SlmIdealPayment/Client/StandardClient.php', - 'SlmIdealPayment\Exception\CertificateNotFoundException' => __DIR__ . '/src/SlmIdealPayment/Exception/CertificateNotFoundException.php', - 'SlmIdealPayment\Exception\CertificateNotValidException' => __DIR__ . '/src/SlmIdealPayment/Exception/CertificateNotValidException.php', - 'SlmIdealPayment\Exception\HttpRequestException' => __DIR__ . '/src/SlmIdealPayment/Exception/HttpRequestException.php', - 'SlmIdealPayment\Exception\IdealRequestException' => __DIR__ . '/src/SlmIdealPayment/Exception/IdealRequestException.php', - 'SlmIdealPayment\Exception\InvalidArgumentException' => __DIR__ . '/src/SlmIdealPayment/Exception/InvalidArgumentException.php', - 'SlmIdealPayment\Exception\RuntimeException' => __DIR__ . '/src/SlmIdealPayment/Exception/RuntimeException.php', - 'SlmIdealPayment\Model\Consumer' => __DIR__ . '/src/SlmIdealPayment/Model/Consumer.php', - 'SlmIdealPayment\Model\Issuer' => __DIR__ . '/src/SlmIdealPayment/Model/Issuer.php', - 'SlmIdealPayment\Model\IssuerCollection' => __DIR__ . '/src/SlmIdealPayment/Model/IssuerCollection.php', - 'SlmIdealPayment\Model\Transaction' => __DIR__ . '/src/SlmIdealPayment/Model/Transaction.php', - 'SlmIdealPayment\Request\AbstractRequest' => __DIR__ . '/src/SlmIdealPayment/Request/AbstractRequest.php', - 'SlmIdealPayment\Request\DirectoryRequest' => __DIR__ . '/src/SlmIdealPayment/Request/DirectoryRequest.php', - 'SlmIdealPayment\Request\RequestInterface' => __DIR__ . '/src/SlmIdealPayment/Request/RequestInterface.php', - 'SlmIdealPayment\Request\StatusRequest' => __DIR__ . '/src/SlmIdealPayment/Request/StatusRequest.php', - 'SlmIdealPayment\Request\TransactionRequest' => __DIR__ . '/src/SlmIdealPayment/Request/TransactionRequest.php', - 'SlmIdealPayment\Response\AbstractResponse' => __DIR__ . '/src/SlmIdealPayment/Response/AbstractResponse.php', - 'SlmIdealPayment\Response\DirectoryResponse' => __DIR__ . '/src/SlmIdealPayment/Response/DirectoryResponse.php', - 'SlmIdealPayment\Response\StatusResponse' => __DIR__ . '/src/SlmIdealPayment/Response/StatusResponse.php', - 'SlmIdealPayment\Response\TransactionResponse' => __DIR__ . '/src/SlmIdealPayment/Response/TransactionResponse.php', -); diff --git a/autoload_function.php b/autoload_function.php deleted file mode 100644 index 3ea81c4..0000000 --- a/autoload_function.php +++ /dev/null @@ -1,12 +0,0 @@ - Date: Sat, 10 Aug 2013 23:12:08 +0200 Subject: [PATCH 15/27] Write documentation about the client usage --- README.md | 154 +++++++++++++++++++++++++- config/slmidealpayment.local.php.dist | 45 ++++++++ 2 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 config/slmidealpayment.local.php.dist diff --git a/README.md b/README.md index 3592a82..a6279f2 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,156 @@ SlmIdealPayment =============== +Created by Jurian Sluiman -This module is a Zend Framework 2 module to provide iDEAL payments (a Dutch payment system). \ No newline at end of file +Introduction +--- +SlmIdealPayment is a Zend Framework 2 module to provide payments via the iDEAL +system. iDEAL is a payment service for the Dutch market and allows integration +of payments for almost all Dutch banks. + +The module provides integration for the so-called iDEAL professional / iDEAL +advanced method. Integration via iDEAL basic / iDEAL lite is not included. + +### Standalone usage +This module is developed for Zend Framework 2, but can be used without the +framework in your own application. There is no need to understand Zend Framework +2 to use this module. + +Requirements +--- +The module only requires a HTTP client where it utilizes the `Zend\Http` +component. Furthermore php 5.3+ is required to work with. + +Installation +--- +The module can be loaded via composer. Require `slm/ideal-payment` in your +composer.json file. If you do not have a composer.json file in the root of your +project, copy the contents below and put that into a file called composer.json +and save it in the root of your project: + +``` +{ + "require": { + "slm/ideal-payment": "@dev" + } +} +``` + +Then execute the following commands in a CLI: + +``` +curl -s http://getcomposer.org/installer | php +php composer.phar install +``` + +Now you should have a vendor directory, including a slm/ideal-payment. In your +bootstrap code, make sure you include the vendor/autoload.php file to properly +load the SlmIdealPayment module. + +Configuration +--- +The file `slmidealpayment.local.php.dist` eases the iDEAL configuration. Copy +the file from `vendor/slm/ideal-payment/config/slmidealpayment.local.php.dist` +to your autoload folder and remove the `.dist` extension. + +Open the file and update the values to your needs. If you do not have a SSL +certificate, you can use a self-signed certificate and upload that one to the +iDEAL dashboard of your acquirer. Generate a key with 2048 bits encryption with +the following command: + +``` +openssl genrsa –aes128 –out priv.pem –passout pass:[privateKeyPass] 2048 +``` + +Then create a certificate valid for 5 years: + +``` +openssl req –x509 –sha256 –new –key priv.pem –passin pass:[privateKeyPass] +-days 1825 –out cert.cer +``` + +Then use the `priv.pem` and `cert.cer` files for the iDEAL signatures. + +Supported acquirers +--- +SlmIdealPayment works with the following banks (or acquirers in iDEAL terms): + + 1. Rabobank + 2. ING Bank + 3. ABN Amro + +Usage +--- +SlmIdealPayments will setup the client with the correct properties. Use the +following names to request an instance from the service manager: + + 1. Rabobank: `SlmIdealPayment\Client\Standard\Rabobank` + 2. ING Bank: `SlmIdealPayment\Client\Standard\Ing` + 3. ABN Amro: `SlmIdealPayment\Client\Standard\AbnAmro` + +### Directory request +Then use the client to perform the request. A directory request gives a list of +supported issuers. The result is a `SlmIdealPayment\Response\DirectoryResponse`: + +```php +$client = $sl->get('SlmIdealPayment\Client\Standard\Rabobank'); + +$request = new DirectoryRequest; +$response = $client->send($request); + +foreach ($response as $issuer) { + echo sprintf("%s: %s\n", $issuer->getId(), $issuer->getName()); +} +``` + +### Transaction request +A transaction request needs a Transaction object. The result is a +`SlmIdealPayment\Response\TransactionResponse` object: + +```php +use SlmIdealPayment\Model; + +$client = $sl->get('SlmIdealPayment\Client\Standard\Rabobank'); + +// Set up the issuer +$issuer = new Model\Issuer; +$issuer->setId($issuer); // set selected issuer here + +// Set up the transaction +$transaction = new Model\Transaction; +$transaction->setPurchaseId($purchaseId); +$transaction->setAmount($amount); +$transaction->setDescription($description); +$transaction->setEntranceCode($ec); + +$request = new TransactionRequest; +$request->setIssuer($issuer); +$request->setTransaction($transaction); + +$response = $client->send($request); + +echo $response->getTransaction()->getTransactionId(); + +// Then perform redirect: +// Redirect to $response->getAuthenticationUrl(); +``` + +### Status request +A status request also needs a transaction object, but then for its transaction +id. The result is a `SlmIdealPayment\Response\StatusRequest`: + +```php +use SlmIdealPayment\Model; + +$client = $sl->get('SlmIdealPayment\Client\Standard\Rabobank'); + +$transaction = new Model\Transaction; +$transaction->setTransactionId($transactionId); + +$request = new StatusRequest; +$request->setTransaction($transaction); + +$response = $client->send($request); + +echo $response->getTransaction()->getStatus(); +``` \ No newline at end of file diff --git a/config/slmidealpayment.local.php.dist b/config/slmidealpayment.local.php.dist new file mode 100644 index 0000000..ed095ee --- /dev/null +++ b/config/slmidealpayment.local.php.dist @@ -0,0 +1,45 @@ + '', + + /** + * Sub ID if the merchant uses multiple shops + * + * By default, you can leave this value to zero + */ + 'sub_id' => '0', + + /** + * Flag for production mode + * + * Set to false, it uses all testing urls for + * the different acquirers + */ + 'production' => true, + + /** + * Location of the key file + */ + 'key_file' => '', + + /** + * Password to open the key file + */ + 'key_password' => '', +); + +/** + * You do not need to edit below this line + */ +return array('slm_ideal_paymemt' => $idealPayment); \ No newline at end of file From 9b1364c7584c5eea2f78bce15bf9fc103502c99b Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 23:13:21 +0200 Subject: [PATCH 16/27] Make standalone less explicit --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a6279f2..9c37cdb 100644 --- a/README.md +++ b/README.md @@ -11,10 +11,10 @@ of payments for almost all Dutch banks. The module provides integration for the so-called iDEAL professional / iDEAL advanced method. Integration via iDEAL basic / iDEAL lite is not included. -### Standalone usage -This module is developed for Zend Framework 2, but can be used without the -framework in your own application. There is no need to understand Zend Framework -2 to use this module. +> **Standalone usage** +> This module is developed for Zend Framework 2, but can be used without the +> framework in your own application. There is no need to understand Zend +> Framework 2 to use this module. Requirements --- From e22c59dc2ae0af054e42e8c9524d00146219d79e Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 23:18:49 +0200 Subject: [PATCH 17/27] Update issuer listing example code --- README.md | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 9c37cdb..fa73b78 100644 --- a/README.md +++ b/README.md @@ -98,8 +98,12 @@ $client = $sl->get('SlmIdealPayment\Client\Standard\Rabobank'); $request = new DirectoryRequest; $response = $client->send($request); -foreach ($response as $issuer) { - echo sprintf("%s: %s\n", $issuer->getId(), $issuer->getName()); +foreach ($response->getContries() as $country) { + echo sprintf("Country: %s\n", $country->getName()); + + foreach ($country->getIssuers as $issuer) { + echo sprintf("%s: %s\n", $issuer->getId(), $issuer->getName()); + } } ``` @@ -114,7 +118,7 @@ $client = $sl->get('SlmIdealPayment\Client\Standard\Rabobank'); // Set up the issuer $issuer = new Model\Issuer; -$issuer->setId($issuer); // set selected issuer here +$issuer->setId($issuerId); // set selected issuer here // Set up the transaction $transaction = new Model\Transaction; From 27d30ee046da8f03cb851d23ccb7f5c2a3e65714 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 23:23:25 +0200 Subject: [PATCH 18/27] Add missing parentheses for method getIssuers() --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index fa73b78..1e20023 100644 --- a/README.md +++ b/README.md @@ -101,7 +101,7 @@ $response = $client->send($request); foreach ($response->getContries() as $country) { echo sprintf("Country: %s\n", $country->getName()); - foreach ($country->getIssuers as $issuer) { + foreach ($country->getIssuers() as $issuer) { echo sprintf("%s: %s\n", $issuer->getId(), $issuer->getName()); } } From 9cb1d95ae89958241a8d98b4cab1defa3cebe48e Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 23:32:13 +0200 Subject: [PATCH 19/27] Add badges --- README.md | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 1e20023..7564056 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,8 @@ SlmIdealPayment =============== +[![Build Status](https://travis-ci.org/juriansluiman/SlmIdealPayment.png)](https://travis-ci.org/juriansluiman/SlmIdealPayment) +[![Latest Stable Version](https://poser.pugx.org/slm/ideal-payment/v/stable.png)](https://packagist.org/packages/slm/ideal-payment) + Created by Jurian Sluiman Introduction @@ -11,7 +14,7 @@ of payments for almost all Dutch banks. The module provides integration for the so-called iDEAL professional / iDEAL advanced method. Integration via iDEAL basic / iDEAL lite is not included. -> **Standalone usage** +> **Standalone usage:** > This module is developed for Zend Framework 2, but can be used without the > framework in your own application. There is no need to understand Zend > Framework 2 to use this module. From 388cf12c345d67af95d99cbff7ae815490cfaed4 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 23:49:33 +0200 Subject: [PATCH 20/27] Add section about general usage --- README.md | 44 +++++++++++++++++++++++++++++++------------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7564056..a2755cc 100644 --- a/README.md +++ b/README.md @@ -19,10 +19,13 @@ advanced method. Integration via iDEAL basic / iDEAL lite is not included. > framework in your own application. There is no need to understand Zend > Framework 2 to use this module. -Requirements ---- -The module only requires a HTTP client where it utilizes the `Zend\Http` -component. Furthermore php 5.3+ is required to work with. +### Supported acquirers + +SlmIdealPayment works with the following banks (or acquirers in iDEAL terms): + + 1. Rabobank + 2. ING Bank + 3. ABN Amro Installation --- @@ -74,14 +77,6 @@ openssl req –x509 –sha256 –new –key priv.pem –passin pass:[privateKeyP Then use the `priv.pem` and `cert.cer` files for the iDEAL signatures. -Supported acquirers ---- -SlmIdealPayment works with the following banks (or acquirers in iDEAL terms): - - 1. Rabobank - 2. ING Bank - 3. ABN Amro - Usage --- SlmIdealPayments will setup the client with the correct properties. Use the @@ -160,4 +155,27 @@ $request->setTransaction($transaction); $response = $client->send($request); echo $response->getTransaction()->getStatus(); -``` \ No newline at end of file +``` + +Using SlmIdealPayment outside Zend Framework 2 +=== +You can use the client without Zend Framework 2. Only the HTTP client is used +inside the client and it's a small dependency you can load in any project you +have. However, you need to configure all variables yourself. + +```php +use SlmIdealPayment\Client\StandardClient; + +$client = new StandardClient; +$client->setRequestUrl('https://ideal.rabobank.nl/ideal/iDEALv3'); +$client->setMerchantId('00X0XXXXX'); +$client->setSubId('0'); + +$client->setPublicCertificate('data/ssl/rabobank.cer'); +$client->setPrivateCertificate('data/ssl/cert.cer'); + +$client->setKeyFile('data/ssl/priv.pem'); +$client->setKeyPassword('h4x0r'); +``` + +Now `$client` is configured, use above methods to perform the various requests. \ No newline at end of file From 710d976f6b15dcc45c96d2d722568ccad14ca9fe Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sat, 10 Aug 2013 23:50:34 +0200 Subject: [PATCH 21/27] s/Zend Framework 2/ZF2 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index a2755cc..27486ed 100644 --- a/README.md +++ b/README.md @@ -157,7 +157,7 @@ $response = $client->send($request); echo $response->getTransaction()->getStatus(); ``` -Using SlmIdealPayment outside Zend Framework 2 +Using SlmIdealPayment outside ZF2 === You can use the client without Zend Framework 2. Only the HTTP client is used inside the client and it's a small dependency you can load in any project you From 44ee2184f9b9fa633c50623410c807887eedba4f Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sun, 11 Aug 2013 00:17:48 +0200 Subject: [PATCH 22/27] Validate generated XML messages for requests The provided XSD is used to validate all XML messages for all requests. To be done: XML validation for incoming responses. --- config/services.config.php | 2 + data/AcceptantAcquirer.xsd | 424 ++++++++++++++++++ data/dsigschema.xsd | 292 ++++++++++++ src/SlmIdealPayment/Client/StandardClient.php | 37 ++ .../Exception/XmlValidationException.php | 45 ++ 5 files changed, 800 insertions(+) create mode 100644 data/AcceptantAcquirer.xsd create mode 100644 data/dsigschema.xsd create mode 100644 src/SlmIdealPayment/Exception/XmlValidationException.php diff --git a/config/services.config.php b/config/services.config.php index 8ecbae7..9d03937 100644 --- a/config/services.config.php +++ b/config/services.config.php @@ -55,6 +55,8 @@ $client->setMerchantId($config['merchant_id']); $client->setSubId($config['sub_id']); + $client->setValidationSchema(__DIR__ . '/../data/AcceptantAcquirer.xsd'); + $httpClient = new HttpClient; $httpClient->setAdapter('Zend\Http\Client\Adapter\Socket'); $httpClient->getAdapter()->setOptions($config['ssl_options']); diff --git a/data/AcceptantAcquirer.xsd b/data/AcceptantAcquirer.xsd new file mode 100644 index 0000000..5b24484 --- /dev/null +++ b/data/AcceptantAcquirer.xsd @@ -0,0 +1,424 @@ + + + + + + + elements defined + + + + Directory Request (A) + + + + + + + + + + + + + + + + + + + + Directory Response (A') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Acquirer Transaction Request (B) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Acquirer Transaction Response (B') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Acquirer Status Request (F) + + + + + + + + + + + + + + + + + + + + + + + + + + + Acquirer Status Response (F') + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Acquirer Error Response (X') + + + + + + + + + + + + + + + + + + + + + + simpleTypes defined + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + basic simpleTypes defined + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + attributeGroups defined + + + + attributes of each iDEAL message + + + + \ No newline at end of file diff --git a/data/dsigschema.xsd b/data/dsigschema.xsd new file mode 100644 index 0000000..321a2d1 --- /dev/null +++ b/data/dsigschema.xsd @@ -0,0 +1,292 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index f8066a6..9466f91 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -128,6 +128,28 @@ public function setKeyPassword($keyPassword) return $this; } + /** + * Getter for validation schema + * + * @return mixed + */ + public function getValidationSchema() + { + return $this->validationSchema; + } + + /** + * Setter for validation schema + * + * @param mixed $validationSchema Value to set + * @return self + */ + public function setValidationSchema($validationSchema) + { + $this->validationSchema = $validationSchema; + return $this; + } + public function getHttpClient() { if (!$this->httpClient instanceof HttpClient) { @@ -391,6 +413,11 @@ protected function createXmlForRequestIssuers(array $data) $document->loadXML($xml); $this->sign($document); + + if (!$document->schemaValidate($this->getValidationSchema())) { + throw new Exception\XmlValidationException('Generated XML for directory request could not be validated'); + } + return $document; } @@ -439,6 +466,11 @@ protected function _createXmlForRequestTransaction(array $data) $document->loadXML($xml); $this->sign($document); + + if (!$document->schemaValidate($this->getValidationSchema())) { + throw new Exception\XmlValidationException('Generated XML for transaction request could not be validated'); + } + return $document; } @@ -469,6 +501,11 @@ protected function _createXmlForRequestStatus(array $data) $document->loadXML($xml); $this->sign($document); + + if (!$document->schemaValidate($this->getValidationSchema())) { + throw new Exception\XmlValidationException('Generated XML for status request could not be validated'); + } + return $document; } } \ No newline at end of file diff --git a/src/SlmIdealPayment/Exception/XmlValidationException.php b/src/SlmIdealPayment/Exception/XmlValidationException.php new file mode 100644 index 0000000..7fc7b3a --- /dev/null +++ b/src/SlmIdealPayment/Exception/XmlValidationException.php @@ -0,0 +1,45 @@ + + * @copyright 2012 Jurian Sluiman. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://juriansluiman.nl + */ + +namespace SlmIdealPayment\Exception; + +class XmlValidationException extends \RuntimeException +{ +} \ No newline at end of file From f46744e394c9db0d403bdfc3af52066454190f2c Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sun, 11 Aug 2013 11:32:33 +0200 Subject: [PATCH 23/27] Move validation to separate method --- src/SlmIdealPayment/Client/StandardClient.php | 54 ++++++++++++++----- 1 file changed, 40 insertions(+), 14 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 9466f91..a22c8d6 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -67,6 +67,7 @@ class StandardClient implements ClientInterface protected $privateCertificate; protected $keyFile; protected $keyPassword; + protected $validationSchema; /** * @var HttpClient @@ -329,10 +330,7 @@ protected function send(DOMDocument $document) $document = new DOMDocument; $document->loadXML($body); - - if (!$this->isValid($document)) { - throw new Exception\IdealRequestException('iDEAL response is invalid'); - } + $this->verify($document); $errors = $document->getElementsByTagName('Error'); if ($errors->length !== 0) { @@ -370,16 +368,50 @@ protected function getFingerprint($public = false) return strtoupper(sha1(base64_decode($data))); } + /** + * Sign document and append dom node + * + * @param DOMDocument $document + * @return void + */ protected function sign(DOMDocument $document) { $signature = new Signature; $signature->sign($document, $this->getFingerprint(), $this->getKeyFile(), $this->getKeyPassword()); } - protected function isValid(DOMDocument $document) + /** + * Verify provided document + * + * @param DOMDocument $document + * @throws Exception\IdealRequestException If the signature is not valid + * @return void + */ + protected function verify(DOMDocument $document) { $signature = new Signature; - return $signature->verify($document, $this->getPublicCertificate()); + + if ($signature->verify($document, $this->getPublicCertificate())) { + throw new Exception\IdealRequestException('iDEAL response could not be verified from acquirer'); + } + } + + /** + * Validate XML document with XSD schema + * + * @param DOMDocument $document + * @throws Exception\XmlValidationException If document is not valid + * @return void + */ + protected function validate(DOMDocument $document) + { + if (null === ($schema = $this->getValidationSchema())) { + return; + } + + if (!$document->schemaValidate($schema)) { + throw new Exception\XmlValidationException('Generated XML for directory request could not be validated'); + } } protected function getTag(DOMNode $element, $tag) @@ -413,10 +445,7 @@ protected function createXmlForRequestIssuers(array $data) $document->loadXML($xml); $this->sign($document); - - if (!$document->schemaValidate($this->getValidationSchema())) { - throw new Exception\XmlValidationException('Generated XML for directory request could not be validated'); - } + $this->validate($document); return $document; } @@ -466,10 +495,7 @@ protected function _createXmlForRequestTransaction(array $data) $document->loadXML($xml); $this->sign($document); - - if (!$document->schemaValidate($this->getValidationSchema())) { - throw new Exception\XmlValidationException('Generated XML for transaction request could not be validated'); - } + $this->validate($document); return $document; } From ee96a42e61a6924f2dd3f3b766187109f2d15950 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sun, 11 Aug 2013 11:33:35 +0200 Subject: [PATCH 24/27] Move all factory logic into service factory class --- Module.php | 11 -- config/module.config.php | 20 +++- config/services.config.php | 108 ------------------ config/slmidealpayment.local.php.dist | 17 ++- .../Service/StandardClientFactory.php | 98 ++++++++++++++++ 5 files changed, 130 insertions(+), 124 deletions(-) delete mode 100644 config/services.config.php create mode 100644 src/SlmIdealPayment/Service/StandardClientFactory.php diff --git a/Module.php b/Module.php index f8c5300..b2f2f11 100644 --- a/Module.php +++ b/Module.php @@ -45,7 +45,6 @@ class Module implements Feature\AutoloaderProviderInterface, - Feature\ServiceProviderInterface, Feature\ConfigProviderInterface { public function getAutoloaderConfig() @@ -63,14 +62,4 @@ public function getConfig() { return include __DIR__ . '/config/module.config.php'; } - - /** - * Go to the service configuration - * - * @return array - */ - public function getServiceConfig() - { - return include __DIR__ . '/config/services.config.php'; - } } diff --git a/config/module.config.php b/config/module.config.php index 43beef1..49d682d 100644 --- a/config/module.config.php +++ b/config/module.config.php @@ -39,16 +39,18 @@ * @link http://juriansluiman.nl */ -use Zend\Http\Client as HttpClient; - return array( - 'ideal' => array( + 'slm_ideal_payment' => array( 'production' => true, 'merchant_id' => '', 'sub_id' => '', 'certificate' => '', 'key_file' => '', 'key_password' => '', + + 'enable_validation' => true, + 'validation_scheme' => __DIR__ . '/../data/AcceptantAcquirer.xsd', + 'ssl_options' => array( 'sslcapath' => '/etc/ssl/certs', ), @@ -62,10 +64,20 @@ 'live' => 'https://ideal.secure-ing.com/ideal/iDeal', 'certificate' => __DIR__ . '/../data/ing.cer', ), - 'rabo' => array( + 'rabobank' => array( 'test' => 'https://idealtest.rabobank.nl/ideal/iDEALv3', 'live' => 'https://ideal.rabobank.nl/ideal/iDEALv3', 'certificate' => __DIR__ . '/../data/rabo_ideal_v3.cer', ), ), + + 'service_manager' => array( + 'factories' => array( + 'SlmIdealPayment\Client\StandardClient' => 'SlmIdealPayment\Service\StandardClientFactory', + + 'SlmIdealPayment\Client\Standard\Rabobank' => 'SlmIdealPayment\Service\StandardClientFactory', + 'SlmIdealPayment\Client\Standard\Ing' => 'SlmIdealPayment\Service\StandardClientFactory', + 'SlmIdealPayment\Client\Standard\AbnAmro' => 'SlmIdealPayment\Service\StandardClientFactory', + ), + ), ); diff --git a/config/services.config.php b/config/services.config.php deleted file mode 100644 index 9d03937..0000000 --- a/config/services.config.php +++ /dev/null @@ -1,108 +0,0 @@ - - * @copyright 2013 Johan van der Heide. - * @license http://www.opensource.org/licenses/bsd-license.php BSD License - * @link http://japaveh.nl - */ - -use SlmIdealPayment\Client\StandardClient; -use Zend\Http\Client as HttpClient; - -return array( - 'factories' => array( - 'SlmIdealPayment\Client\StandardClient' => function ($sm) { - $config = $sm->get('config'); - $config = $config['ideal']; - - $client = new StandardClient; - $client->setPrivateCertificate($config['certificate']); - $client->setKeyFile($config['key_file']); - $client->setKeyPassword($config['key_password']); - $client->setMerchantId($config['merchant_id']); - $client->setSubId($config['sub_id']); - - $client->setValidationSchema(__DIR__ . '/../data/AcceptantAcquirer.xsd'); - - $httpClient = new HttpClient; - $httpClient->setAdapter('Zend\Http\Client\Adapter\Socket'); - $httpClient->getAdapter()->setOptions($config['ssl_options']); - $client->setHttpClient($httpClient); - - return $client; - }, - 'ideal-abn' => function ($sm) { - $config = $sm->get('config'); - $config = $config['ideal']; - $client = $sm->get('SlmIdealPayment\Client\StandardClient'); - - $url = ($config['production']) ? $config['abn']['live'] : $config['abn']['test']; - $cert = $config['abn']['certificate']; - - $client->setRequestUrl($url); - $client->setPublicCertificate($cert); - - return $client; - }, - 'ideal-ing' => function ($sm) { - $config = $sm->get('config'); - $config = $config['ideal']; - $client = $sm->get('SlmIdealPayment\Client\StandardClient'); - - $url = ($config['production']) ? $config['ing']['live'] : $config['ing']['test']; - $cert = $config['ing']['certificate']; - - $client->setRequestUrl($url); - $client->setPublicCertificate($cert); - - return $client; - }, - 'ideal-rabo' => function ($sm) { - $config = $sm->get('config'); - $config = $config['ideal']; - $client = $sm->get('SlmIdealPayment\Client\StandardClient'); - - $url = ($config['production']) ? $config['rabo']['live'] : $config['rabo']['test']; - $cert = $config['rabo']['certificate']; - - $client->setRequestUrl($url); - $client->setPublicCertificate($cert); - - return $client; - }, - ), -); - diff --git a/config/slmidealpayment.local.php.dist b/config/slmidealpayment.local.php.dist index ed095ee..dba27e1 100644 --- a/config/slmidealpayment.local.php.dist +++ b/config/slmidealpayment.local.php.dist @@ -28,6 +28,11 @@ $idealPayment = array( */ 'production' => true, + /** + * Location of the certificate + */ + 'certificate' => '', + /** * Location of the key file */ @@ -37,9 +42,19 @@ $idealPayment = array( * Password to open the key file */ 'key_password' => '', + + /** + * Flag to enable/disable XML validation + * + * SlmIdealPayment can validate all XML messages + * from the requests and responses against a provided + * XSD file. This gives tight control over the XML + * but can slow down the process. + */ + 'enable_validation' => true, ); /** * You do not need to edit below this line */ -return array('slm_ideal_paymemt' => $idealPayment); \ No newline at end of file +return array('slm_ideal_payment' => $idealPayment); \ No newline at end of file diff --git a/src/SlmIdealPayment/Service/StandardClientFactory.php b/src/SlmIdealPayment/Service/StandardClientFactory.php new file mode 100644 index 0000000..033cb48 --- /dev/null +++ b/src/SlmIdealPayment/Service/StandardClientFactory.php @@ -0,0 +1,98 @@ + + * @copyright 2012-2013 Jurian Sluiman http://juriansluiman.nl. + * @license http://www.opensource.org/licenses/bsd-license.php BSD License + * @link http://juriansluiman.nl + */ + +namespace SlmIdealPayment\Service; + +use SlmIdealPayment\Client\StandardClient; +use Zend\ServiceManager\FactoryInterface; +use Zend\ServiceManager\ServiceLocatorInterface; + +class StandardClientFactory implements FactoryInterface +{ + /** + * @param ServiceLocatorInterface $serviceLocator + * @return StandardClient + */ + public function createService(ServiceLocatorInterface $serviceLocator, $canonical = null, $name = null) + { + $config = $serviceLocator->get('Config'); + $config = $config['slm_ideal_payment']; + + $client = new StandardClient; + $client->setPrivateCertificate($config['certificate']); + $client->setKeyFile($config['key_file']); + $client->setKeyPassword($config['key_password']); + $client->setMerchantId($config['merchant_id']); + $client->setSubId($config['sub_id']); + + if (true === $config['enable_validation']) { + $client->setValidationSchema($config['validation_scheme']); + } + + $httpClient = $client->getHttpClient(); + $httpClient->setAdapter('Zend\Http\Client\Adapter\Socket'); + $httpClient->getAdapter()->setOptions($config['ssl_options']); + $client->setHttpClient($httpClient); + + if ('SlmIdealPayment\Client\StandardClient' !== $name) { + $this->configureAcquirer($client, $config, $name); + } + + return $client; + } + + protected function configureAcquirer(StandardClient $client, array $config, $name) + { + /** + * Canonicalize name + * + * "SlmIdealPayment\Client\Standard\Rabobank" => "rabobank" + */ + $name = strtolower(substr($name, strrpos($name, '\\') + 1)); + + $url = ($config['production']) ? $config[$name]['live'] : $config[$name]['test']; + $cert = $config[$name]['certificate']; + + $client->setRequestUrl($url); + $client->setPublicCertificate($cert); + + return $client; + } +} \ No newline at end of file From 12d5cdfbe056a4ccccbce759829aae407be59b45 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sun, 11 Aug 2013 12:15:54 +0200 Subject: [PATCH 25/27] Ease interfaces for request and response --- .../Request/DirectoryRequest.php | 2 +- .../Request/RequestInterface.php | 4 --- src/SlmIdealPayment/Request/StatusRequest.php | 2 +- .../Request/TransactionRequest.php | 2 +- .../Response/AbstractResponse.php | 2 +- .../ResponseInterface.php} | 30 ++----------------- .../Response/StatusResponse.php | 24 --------------- 7 files changed, 6 insertions(+), 60 deletions(-) rename src/SlmIdealPayment/{Request/AbstractRequest.php => Response/ResponseInterface.php} (75%) diff --git a/src/SlmIdealPayment/Request/DirectoryRequest.php b/src/SlmIdealPayment/Request/DirectoryRequest.php index 7f42085..4c44c41 100644 --- a/src/SlmIdealPayment/Request/DirectoryRequest.php +++ b/src/SlmIdealPayment/Request/DirectoryRequest.php @@ -42,6 +42,6 @@ namespace SlmIdealPayment\Request; -class DirectoryRequest extends AbstractRequest +class DirectoryRequest implements RequestInterface { } \ No newline at end of file diff --git a/src/SlmIdealPayment/Request/RequestInterface.php b/src/SlmIdealPayment/Request/RequestInterface.php index e0ab817..017ae80 100644 --- a/src/SlmIdealPayment/Request/RequestInterface.php +++ b/src/SlmIdealPayment/Request/RequestInterface.php @@ -44,8 +44,4 @@ interface RequestInterface { - public function getMerchantId(); - public function setMerchantId($merchantId); - public function getSubId(); - public function setSubId($subId); } \ No newline at end of file diff --git a/src/SlmIdealPayment/Request/StatusRequest.php b/src/SlmIdealPayment/Request/StatusRequest.php index eae294d..cfa0b6d 100644 --- a/src/SlmIdealPayment/Request/StatusRequest.php +++ b/src/SlmIdealPayment/Request/StatusRequest.php @@ -44,7 +44,7 @@ use SlmIdealPayment\Model\Transaction; -class StatusRequest extends AbstractRequest +class StatusRequest implements RequestInterface { /** * @var Transaction diff --git a/src/SlmIdealPayment/Request/TransactionRequest.php b/src/SlmIdealPayment/Request/TransactionRequest.php index b530b55..04893a8 100644 --- a/src/SlmIdealPayment/Request/TransactionRequest.php +++ b/src/SlmIdealPayment/Request/TransactionRequest.php @@ -45,7 +45,7 @@ use SlmIdealPayment\Model\Issuer; use SlmIdealPayment\Model\Transaction; -class TransactionRequest extends AbstractRequest +class TransactionRequest implements RequestInterface { /** * @var Issuer diff --git a/src/SlmIdealPayment/Response/AbstractResponse.php b/src/SlmIdealPayment/Response/AbstractResponse.php index 6921192..cbab81c 100644 --- a/src/SlmIdealPayment/Response/AbstractResponse.php +++ b/src/SlmIdealPayment/Response/AbstractResponse.php @@ -42,7 +42,7 @@ namespace SlmIdealPayment\Response; -abstract class AbstractResponse +abstract class AbstractResponse implements ResponseInterface { protected $acquirer; diff --git a/src/SlmIdealPayment/Request/AbstractRequest.php b/src/SlmIdealPayment/Response/ResponseInterface.php similarity index 75% rename from src/SlmIdealPayment/Request/AbstractRequest.php rename to src/SlmIdealPayment/Response/ResponseInterface.php index bfd5674..2765e59 100644 --- a/src/SlmIdealPayment/Request/AbstractRequest.php +++ b/src/SlmIdealPayment/Response/ResponseInterface.php @@ -32,40 +32,14 @@ * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE * POSSIBILITY OF SUCH DAMAGE. * - * @package SlmIdealPayment - * @subpackage Request * @author Jurian Sluiman * @copyright 2012 Jurian Sluiman. * @license http://www.opensource.org/licenses/bsd-license.php BSD License * @link http://juriansluiman.nl */ -namespace SlmIdealPayment\Request; +namespace SlmIdealPayment\Response; -abstract class AbstractRequest implements RequestInterface +interface ResponseInterface { - protected $merchantId; - protected $subId; - - public function getMerchantId() - { - return $this->merchantId; - } - - public function setMerchantId($merchantId) - { - $this->merchantId = $merchantId; - return $this; - } - - public function getSubId() - { - return $this->subId; - } - - public function setSubId($subId) - { - $this->subId = $subId; - return $this; - } } \ No newline at end of file diff --git a/src/SlmIdealPayment/Response/StatusResponse.php b/src/SlmIdealPayment/Response/StatusResponse.php index e704a25..eed5f92 100644 --- a/src/SlmIdealPayment/Response/StatusResponse.php +++ b/src/SlmIdealPayment/Response/StatusResponse.php @@ -47,8 +47,6 @@ class StatusResponse extends AbstractResponse { protected $transaction; - protected $signatureValue; - protected $fingerprint; public function getTransaction() { @@ -60,26 +58,4 @@ public function setTransaction(Transaction $transaction) $this->transaction = $transaction; return $this; } - - public function getSignatureValue() - { - return $this->signatureValue; - } - - public function setSignatureValue($signatureValue) - { - $this->signatureValue = $signatureValue; - return $this; - } - - public function getFingerprint() - { - return $this->fingerprint; - } - - public function setFingerprint($fingerprint) - { - $this->fingerprint = $fingerprint; - return $this; - } } \ No newline at end of file From 10f8e9256a2d554eba785a29e17699e519c645d6 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sun, 11 Aug 2013 12:16:51 +0200 Subject: [PATCH 26/27] Create proxy method send() for client --- .../Client/ClientInterface.php | 11 ++++++ src/SlmIdealPayment/Client/StandardClient.php | 37 +++++++++++++++---- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/src/SlmIdealPayment/Client/ClientInterface.php b/src/SlmIdealPayment/Client/ClientInterface.php index f448ec7..97adb51 100644 --- a/src/SlmIdealPayment/Client/ClientInterface.php +++ b/src/SlmIdealPayment/Client/ClientInterface.php @@ -42,6 +42,9 @@ namespace SlmIdealPayment\Client; +use SlmIdealPayment\Request\RequestInterface; +use SlmIdealPayment\Response\ResponseInterface; + use SlmIdealPayment\Request\DirectoryRequest; use SlmIdealPayment\Response\DirectoryResponse; @@ -53,6 +56,14 @@ interface ClientInterface { + /** + * Proxy to different request methods + * + * @param RequestInterface $request + * @return ResponseInterface + */ + public function send(RequestInterface $request); + /** * Perform request to get directory of issuers * diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index a22c8d6..050920d 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -198,6 +198,26 @@ public function getSubId() return $this->subId; } + /** + * {@inheritdoc} + */ + public function send(Request\RequestInterface $request) + { + switch (get_class($request)) { + case 'SlmIdealPayment\Request\DirectoryRequest': + return $this->sendDirectoryRequest($request); + break; + case 'SlmIdealPayment\Request\TransactionRequest': + return $this->sendTransactionRequest($request); + break; + case 'SlmIdealPayment\Request\StatusRequest': + return $this->sendStatusRequest($request); + break; + default: + throw new Exception\InvalidArgumentException('Unknown class for send() proxy method'); + } + } + /** * {@inheritdoc} */ @@ -208,7 +228,7 @@ public function sendDirectoryRequest(Request\DirectoryRequest $directoryRequest) 'subId' => $this->getSubId(), )); - $response = $this->send($xml); + $response = $this->request($xml); if ('DirectoryRes' !== $response->firstChild->nodeName) { throw new Exception\IdealRequestException('Expecting DirectoryRes as root element in response'); @@ -253,7 +273,7 @@ public function sendTransactionRequest(Request\TransactionRequest $transactionRe 'entrance' => $transactionRequest->getTransaction()->getEntranceCode() )); - $response = $this->send($xml); + $response = $this->request($xml); if ('AcquirerTrxRes' !== $response->firstChild->nodeName) { throw new Exception\IdealRequestException('Expecting AcquirerTrxRes as root element in response'); @@ -285,7 +305,7 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) ) ); - $response = $this->send($xml); + $response = $this->request($xml); if ('AcquirerStatusRes' !== $response->firstChild->nodeName) { throw new Exception\IdealRequestException('Expecting AcquirerStatusRes as root element in response'); @@ -311,7 +331,7 @@ public function sendStatusRequest(Request\StatusRequest $statusRequest) return $response; } - protected function send(DOMDocument $document) + protected function request(DOMDocument $document) { $client = $this->getHttpClient(); $client->setUri($this->getRequestUrl()); @@ -330,6 +350,7 @@ protected function send(DOMDocument $document) $document = new DOMDocument; $document->loadXML($body); + $this->verify($document); $errors = $document->getElementsByTagName('Error'); @@ -391,7 +412,7 @@ protected function verify(DOMDocument $document) { $signature = new Signature; - if ($signature->verify($document, $this->getPublicCertificate())) { + if (!$signature->verify($document, $this->getPublicCertificate())) { throw new Exception\IdealRequestException('iDEAL response could not be verified from acquirer'); } } @@ -414,9 +435,11 @@ protected function validate(DOMDocument $document) } } - protected function getTag(DOMNode $element, $tag) + protected function getTag(DOMNode $node, $tag) { - return $element->getElementsByTagName($tag)->item(0)->textContent; + return $node->getElementsByTagName($tag) + ->item(0) + ->textContent; } /** From df0604249af75053833da881f2d362f4f0c55fc2 Mon Sep 17 00:00:00 2001 From: Jurian Sluiman Date: Sun, 11 Aug 2013 12:17:30 +0200 Subject: [PATCH 27/27] Move fingerprint generation to Signature class --- src/SlmIdealPayment/Client/StandardClient.php | 22 +------------------ .../Client/StandardClient/Signature.php | 22 +++++++++++++++++-- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/src/SlmIdealPayment/Client/StandardClient.php b/src/SlmIdealPayment/Client/StandardClient.php index 050920d..e34031f 100644 --- a/src/SlmIdealPayment/Client/StandardClient.php +++ b/src/SlmIdealPayment/Client/StandardClient.php @@ -369,26 +369,6 @@ protected function request(DOMDocument $document) return $document; } - protected function getFingerprint($public = false) - { - $certificate = ($public) ? $this->getPublicCertificate() : $this->getPrivateCertificate(); - - if (false === ($fp = fopen($certificate, 'r'))) { - throw new Exception\CertificateNotFoundException('Cannot open certificate file'); - } - - $rawData = fread($fp, 8192); - $data = openssl_x509_read($rawData); - fclose($fp); - - if (!openssl_x509_export($data, $data)) { - throw new Exception\CertificateNotValidException('Error in certificate'); - } - - $data = str_replace(array('-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'), '', $data); - return strtoupper(sha1(base64_decode($data))); - } - /** * Sign document and append dom node * @@ -398,7 +378,7 @@ protected function getFingerprint($public = false) protected function sign(DOMDocument $document) { $signature = new Signature; - $signature->sign($document, $this->getFingerprint(), $this->getKeyFile(), $this->getKeyPassword()); + $signature->sign($document, $this->getPrivateCertificate(), $this->getKeyFile(), $this->getKeyPassword()); } /** diff --git a/src/SlmIdealPayment/Client/StandardClient/Signature.php b/src/SlmIdealPayment/Client/StandardClient/Signature.php index 8afb440..5f9c80f 100644 --- a/src/SlmIdealPayment/Client/StandardClient/Signature.php +++ b/src/SlmIdealPayment/Client/StandardClient/Signature.php @@ -23,7 +23,7 @@ public function __construct() } } - public function sign(DOMDocument $document, $fingerprint, $keyfile, $passphrase = null) + public function sign(DOMDocument $document, $certificate, $keyfile, $passphrase = null) { $dsig = new XMLSecurityDSig(); $dsig->setCanonicalMethod(XMLSecurityDSig::EXC_C14N); @@ -40,7 +40,7 @@ public function sign(DOMDocument $document, $fingerprint, $keyfile, $passphrase $key->loadKey($keyfile, true); $dsig->sign($key); - $dsig->addKeyInfoAndName($fingerprint); + $dsig->addKeyInfoAndName($this->getFingerprint($certificate)); $dsig->appendSignature($document->documentElement); } @@ -67,4 +67,22 @@ public function verify(DOMDocument $document, $certificate) return (bool) $dsig->verify($key); } + + protected function getFingerprint($path) + { + if (false === ($fp = fopen($path, 'r'))) { + throw new Exception\CertificateNotFoundException('Cannot open certificate file'); + } + + $rawData = fread($fp, 8192); + $data = openssl_x509_read($rawData); + fclose($fp); + + if (!openssl_x509_export($data, $data)) { + throw new Exception\CertificateNotValidException('Error in certificate'); + } + + $data = str_replace(array('-----BEGIN CERTIFICATE-----', '-----END CERTIFICATE-----'), '', $data); + return strtoupper(sha1(base64_decode($data))); + } } \ No newline at end of file