Skip to content

Commit f01c40b

Browse files
committed
Add support for Webservice gateway (including unit tests); some refactoring
1 parent 1570a82 commit f01c40b

24 files changed

+1257
-121
lines changed

README.md

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,8 @@
1111

1212

1313
[Omnipay](https://github.com/thephpleague/omnipay) is a framework agnostic, multi-gateway payment
14-
processing library for PHP 5.3+. This package implements Redsys support for Omnipay.
14+
processing library for PHP 5.3+. This package implements Redsys support for Omnipay. It includes
15+
support for both redirect (3-party) and webservice (2-party) versions of the gateway.
1516

1617
## Installation
1718

@@ -35,7 +36,8 @@ And run composer to update your dependencies:
3536

3637
The following gateways are provided by this package:
3738

38-
* Redsys
39+
* Redsys_Redirect
40+
* Redsys_Webservice
3941

4042
For general usage instructions, please see the main [Omnipay](https://github.com/thephpleague/omnipay)
4143
repository.

composer.json

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,12 @@
99
"pay",
1010
"payment",
1111
"redsys",
12-
"servired"
12+
"servired",
13+
"sermepa",
14+
"redirect",
15+
"webservice",
16+
"web service",
17+
"soap"
1318
],
1419
"homepage": "https://github.com/PatronBase/omnipay-redsys",
1520
"license": "MIT",
@@ -36,7 +41,7 @@
3641
},
3742
"extra": {
3843
"branch-alias": {
39-
"dev-master": "2.0.x-dev"
44+
"dev-master": "2.1.x-dev"
4045
}
4146
}
4247
}

phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,9 @@
99
processIsolation="false"
1010
stopOnFailure="false"
1111
syntaxCheck="false">
12+
<php>
13+
<ini name="date.timezone" value="UTC" />
14+
</php>
1215
<testsuites>
1316
<testsuite name="Omnipay Test Suite">
1417
<directory>./tests/</directory>

src/Message/CompletePurchaseResponse.php

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
namespace Omnipay\Redsys\Message;
44

5+
use Omnipay\Common\Message\AbstractResponse;
56
use Omnipay\Common\Message\RequestInterface;
67
use Omnipay\Common\Exception\InvalidResponseException;
78

@@ -31,10 +32,12 @@ public function __construct(RequestInterface $request, $data)
3132
{
3233
parent::__construct($request, $data);
3334

35+
$security = new Security;
36+
3437
if (!empty($data['Ds_MerchantParameters'])) {
35-
$this->merchantParameters = $this->decodeMerchantParameters($data['Ds_MerchantParameters']);
38+
$this->merchantParameters = $security->decodeMerchantParameters($data['Ds_MerchantParameters']);
3639
} elseif (!empty($data['DS_MERCHANTPARAMETERS'])) {
37-
$this->merchantParameters = $this->decodeMerchantParameters($data['DS_MERCHANTPARAMETERS']);
40+
$this->merchantParameters = $security->decodeMerchantParameters($data['DS_MERCHANTPARAMETERS']);
3841
$this->usingUpcaseResponse = true;
3942
} else {
4043
throw new InvalidResponseException('Invalid response from payment gateway (no data)');
@@ -49,10 +52,10 @@ public function __construct(RequestInterface $request, $data)
4952
throw new InvalidResponseException();
5053
}
5154

52-
$this->returnSignature = $this->createReturnSignature(
55+
$this->returnSignature = $security->createReturnSignature(
5356
$data[$this->usingUpcaseResponse ? 'DS_MERCHANTPARAMETERS' : 'Ds_MerchantParameters'],
5457
$order,
55-
base64_decode($this->request->getHmacKey())
58+
$this->request->getHmacKey()
5659
);
5760

5861
if ($this->returnSignature != $data[$this->usingUpcaseResponse ? 'DS_SIGNATURE' : 'Ds_Signature']) {

src/Message/PurchaseRequest.php

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,21 @@ public function getData()
160160

161161
public function sendData($data)
162162
{
163-
return $this->response = new PurchaseResponse($this, $data);
163+
$security = new Security;
164+
165+
$encoded_data = $security->encodeMerchantParameters($data);
166+
167+
$response_data = array(
168+
'Ds_SignatureVersion' => Security::VERSION,
169+
'Ds_MerchantParameters' => $encoded_data,
170+
'Ds_Signature' => $security->createSignature(
171+
$encoded_data,
172+
$data['Ds_Merchant_Order'],
173+
$this->getHmacKey()
174+
),
175+
);
176+
177+
return $this->response = new PurchaseResponse($this, $response_data);
164178
}
165179

166180
public function getEndpoint()

src/Message/PurchaseResponse.php

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,13 @@
22

33
namespace Omnipay\Redsys\Message;
44

5-
use Omnipay\Common\Message\RedirectResponseInterface;
5+
use Omnipay\Common\Message\AbstractResponse;
66

77
/**
88
* Redsys Purchase Response
99
*/
10-
class PurchaseResponse extends AbstractResponse implements RedirectResponseInterface
10+
class PurchaseResponse extends AbstractResponse
1111
{
12-
protected $version = 'HMAC_SHA256_V1';
13-
1412
public function isSuccessful()
1513
{
1614
return false;
@@ -33,15 +31,6 @@ public function getRedirectMethod()
3331

3432
public function getRedirectData()
3533
{
36-
$redirect_data = array();
37-
$redirect_data['Ds_SignatureVersion'] = $this->version;
38-
$redirect_data['Ds_MerchantParameters'] = $this->encodeMerchantParameters($this->data);
39-
$redirect_data['Ds_Signature'] = $this->createSignature(
40-
$redirect_data['Ds_MerchantParameters'],
41-
$this->data['Ds_Merchant_Order'],
42-
base64_decode($this->request->getHmacKey())
43-
);
44-
45-
return $redirect_data;
34+
return $this->data;
4635
}
4736
}

src/Message/AbstractResponse.php renamed to src/Message/Security.php

Lines changed: 37 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,25 +2,30 @@
22

33
namespace Omnipay\Redsys\Message;
44

5-
use Omnipay\Common\Message\AbstractResponse as BaseAbstractResponse;
65
use Omnipay\Common\Exception\RuntimeException;
76

87
/**
9-
* Abstract Response
8+
* Security
109
*
11-
* This abstract class extends the base Omnipay AbstractResponse in order
12-
* to provide some common encoding and decoding functions.
10+
* This class provides common encoding, decoding and signing functions.
11+
* While all of this code could be called statically, it is left as a
12+
* regular class in order to faciliate unit testing. If alternate
13+
* encryption methods are provided later, the VERSION const can be
14+
* switched to a constructor option (and validated against a whitelist).
1315
*/
14-
abstract class AbstractResponse extends BaseAbstractResponse
16+
class Security
1517
{
18+
/** @var string */
19+
const VERSION = 'HMAC_SHA256_V1';
20+
1621
/**
1722
* Encode merchant parameters
1823
*
1924
* @param array $data The parameters to encode
2025
*
2126
* @return string Encoded data
2227
*/
23-
protected function encodeMerchantParameters($data)
28+
public function encodeMerchantParameters($data)
2429
{
2530
return base64_encode(json_encode($data));
2631
}
@@ -32,28 +37,27 @@ protected function encodeMerchantParameters($data)
3237
*
3338
* @return array Decoded data
3439
*/
35-
protected function decodeMerchantParameters($data)
40+
public function decodeMerchantParameters($data)
3641
{
3742
return (array)json_decode(base64_decode(strtr($data, '-_', '+/')));
3843
}
3944

4045
/**
4146
* Encrypt message with given key and default IV
4247
*
43-
* @todo function_exists() vs extension_loaded()?
44-
*
4548
* @param string $message The message to encrypt
46-
* @param string $key The key used to encrypt the message
49+
* @param string $key The base64-encoded key used to encrypt the message
4750
*
4851
* @return string Encrypted message
4952
*
5053
* @throws RuntimeException
5154
*/
5255
protected function encryptMessage($message, $key)
5356
{
57+
$key = base64_decode($key);
5458
$iv = implode(array_map('chr', array(0, 0, 0, 0, 0, 0, 0, 0)));
5559

56-
if (function_exists('mcrypt_encrypt')) {
60+
if ($this->hasValidEncryptionMethod()) {
5761
$ciphertext = mcrypt_encrypt(MCRYPT_3DES, $key, $message, MCRYPT_MODE_CBC, $iv);
5862
} else {
5963
throw new RuntimeException('No valid encryption extension installed');
@@ -62,24 +66,43 @@ protected function encryptMessage($message, $key)
6266
return $ciphertext;
6367
}
6468

69+
/**
70+
* Check if the system has a valid encryption method available
71+
*
72+
* @return bool
73+
*/
74+
public function hasValidEncryptionMethod()
75+
{
76+
return extension_loaded('mcrypt') && function_exists('mcrypt_encrypt');
77+
}
78+
6579
/**
6680
* Create signature hash used to verify messages
6781
*
6882
* @todo Add if-check on algorithm to match against signature version as new param?
6983
*
7084
* @param string $message The message to encrypt
7185
* @param string $salt Unique salt used to generate the ciphertext
72-
* @param string $key The key used to encrypt the message
86+
* @param string $key The base64-encoded key used to encrypt the message
7387
*
7488
* @return string Generated signature
7589
*/
76-
protected function createSignature($message, $salt, $key)
90+
public function createSignature($message, $salt, $key)
7791
{
7892
$ciphertext = $this->encryptMessage($salt, $key);
7993
return base64_encode(hash_hmac('sha256', $message, $ciphertext, true));
8094
}
8195

82-
protected function createReturnSignature($message, $salt, $key)
96+
/**
97+
* Create signature hash used to verify messages back for Redirect gateway
98+
*
99+
* @param string $message The message to encrypt
100+
* @param string $salt Unique salt used to generate the ciphertext
101+
* @param string $key The base64-encoded key used to encrypt the message
102+
*
103+
* @return string Generated signature
104+
*/
105+
public function createReturnSignature($message, $salt, $key)
83106
{
84107
return strtr($this->createSignature($message, $salt, $key), '+/', '-_');
85108
}
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
<?php
2+
3+
namespace Omnipay\Redsys\Message;
4+
5+
use Exception;
6+
use SimpleXMLElement;
7+
8+
/**
9+
* Redsys Webservice Purchase Request
10+
*/
11+
class WebservicePurchaseRequest extends PurchaseRequest
12+
{
13+
/** @var string */
14+
protected $liveEndpoint = "https://sis.redsys.es/sis/services/SerClsWSEntrada";
15+
/** @var string */
16+
protected $testEndpoint = "https://sis-t.redsys.es:25443/sis/services/SerClsWSEntrada";
17+
18+
public function getData()
19+
{
20+
$this->validate('merchantId', 'terminalId', 'amount', 'currency', 'card');
21+
$card = $this->getCard();
22+
// test cards aparently don't validate
23+
if (!$this->getTestMode()) {
24+
$card->validate();
25+
}
26+
27+
$data = array(
28+
'DS_MERCHANT_AMOUNT' => $this->getAmountInteger(),
29+
'DS_MERCHANT_ORDER' => $this->getTransactionId(),
30+
'DS_MERCHANT_MERCHANTCODE' => $this->getMerchantId(),
31+
'DS_MERCHANT_CURRENCY' => $this->getCurrencyNumeric(), // uses ISO-4217 codes
32+
'DS_MERCHANT_PAN' => $card->getNumber(),
33+
'DS_MERCHANT_CVV2' => $card->getCvv(),
34+
'DS_MERCHANT_TRANSACTIONTYPE' => 'A', // 'Traditional payment'
35+
'DS_MERCHANT_TERMINAL' => $this->getTerminalId(),
36+
'DS_MERCHANT_EXPIRYDATE' => $card->getExpiryDate('ym'),
37+
// undocumented fields
38+
'DS_MERCHANT_MERCHANTDATA' => $this->getMerchantData(),
39+
'DS_MERCHANT_MERCHANTNAME' => $this->getMerchantName(),
40+
'DS_MERCHANT_CONSUMERLANGUAGE' => $this->getConsumerLanguage(),
41+
);
42+
43+
44+
$request = new SimpleXMLElement('<REQUEST/>');
45+
$requestData = $request->addChild('DATOSENTRADA');
46+
foreach ($data as $tag => $value) {
47+
$requestData->addChild($tag, $value);
48+
}
49+
50+
$security = new Security;
51+
52+
$request->addChild('DS_SIGNATUREVERSION', Security::VERSION);
53+
$request->addChild('DS_SIGNATURE', $security->createSignature(
54+
$requestData->asXML(),
55+
$data['DS_MERCHANT_ORDER'],
56+
$this->getHmacKey()
57+
));
58+
59+
// keep data as nested array for method signature compatibility
60+
return array(
61+
'DATOSENTRADA' => $data,
62+
'DS_SIGNATUREVERSION' => (string)$request->DS_SIGNATUREVERSION,
63+
'DS_SIGNATURE' => (string)$request->DS_SIGNATURE,
64+
);
65+
}
66+
67+
/**
68+
* Send the data
69+
*
70+
* Uses its own SOAP wrapper instead of PHP's SoapClient
71+
*/
72+
public function sendData($data)
73+
{
74+
// re-create the XML
75+
$request = new SimpleXMLElement('<REQUEST/>');
76+
$requestData = $request->addChild('DATOSENTRADA');
77+
foreach ($data['DATOSENTRADA'] as $tag => $value) {
78+
$requestData->addChild($tag, $value);
79+
}
80+
$request->addChild('DS_SIGNATUREVERSION', $data['DS_SIGNATUREVERSION']);
81+
$request->addChild('DS_SIGNATURE', $data['DS_SIGNATURE']);
82+
83+
// wrap in SOAP envelope
84+
$requestEnvelope = "<soapenv:Envelope xmlns:soapenv='http://schemas.xmlsoap.org/soap/envelope/'>
85+
<soapenv:Header/>
86+
<soapenv:Body>
87+
<impl:trataPeticion xmlns:impl='http://webservice.sis.sermepa.es'>
88+
<impl:datosEntrada>
89+
".htmlspecialchars($request->asXML())."
90+
</impl:datosEntrada>
91+
</impl:trataPeticion>
92+
</soapenv:Body>
93+
</soapenv:Envelope>";
94+
95+
// send the actual SOAP request
96+
$httpResponse = $this->httpClient->post(
97+
$this->getEndpoint(),
98+
array('SOAPAction' => 'trataPeticion'),
99+
$requestEnvelope
100+
)->send();
101+
102+
// unwrap httpResponse into actual data as SimpleXMLElement tree
103+
$responseEnvelope = $httpResponse->xml();
104+
$responseData = new SimpleXMLElement(htmlspecialchars_decode(
105+
$responseEnvelope->children("http://schemas.xmlsoap.org/soap/envelope/")
106+
->Body->children("http://webservice.sis.sermepa.es")
107+
->trataPeticionResponse
108+
->trataPeticionReturn
109+
));
110+
111+
112+
// remove any reflected request data (this happens on SIS errors, and includes card number)
113+
if (isset($responseData->RECIBIDO)) {
114+
unset($responseData->RECIBIDO);
115+
}
116+
117+
// convert to nested arrays (drop the 'true' to use simple objects)
118+
$responseData = json_decode(json_encode($responseData), true);
119+
120+
return $this->response = new WebservicePurchaseResponse($this, $responseData);
121+
}
122+
}

0 commit comments

Comments
 (0)