Failed Conditions
Push — develop ( a535bd...5d79ac )
by Reüel
04:29 queued 10s
created

src/Client.php (1 issue)

1
<?php
2
/**
3
 * Client.
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2020 Pronamic
7
 * @license   GPL-3.0-or-later
8
 * @package   Pronamic\WordPress\Pay
9
 */
10
11
namespace Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3;
12
13
use DOMDocument;
14
use Pronamic\WordPress\Pay\Core\Util as Core_Util;
15
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\AcquirerErrorResMessage;
16
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\AcquirerStatusReqMessage;
17
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\AcquirerStatusResMessage;
18
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\DirectoryRequestMessage;
19
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\DirectoryResponseMessage;
20
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\Message;
21
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\RequestMessage;
22
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\TransactionRequestMessage;
23
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\TransactionResponseMessage;
24
use SimpleXMLElement;
25
use WP_Error;
26
use XMLSecurityDSig;
27
use XMLSecurityKey;
28
29
/**
30
 * Title: iDEAL client
31
 * Description:
32
 * Copyright: 2005-2020 Pronamic
33
 * Company: Pronamic
34
 *
35
 * @author  Remco Tolsma
36
 * @version 2.0.5
37
 * @since   1.0.0
38
 */
39
class Client {
40
	/**
41
	 * Acquirer URL
42
	 *
43
	 * @var string
44
	 */
45
	public $acquirer_url;
46
47
	/**
48
	 * Directory request URL
49
	 *
50
	 * @var string
51
	 */
52
	public $directory_request_url;
53
54
	/**
55
	 * Transaction request URL
56
	 *
57
	 * @var string
58
	 */
59
	public $transaction_request_url;
60
61
	/**
62
	 * Status request URL
63
	 *
64
	 * @var string
65
	 */
66
	public $status_request_url;
67
68
	/**
69
	 * Merchant ID
70
	 *
71
	 * @var string
72
	 */
73
	public $merchant_id;
74
75
	/**
76
	 * Sub ID
77
	 *
78
	 * @var string
79
	 */
80
	public $sub_id;
81
82
	/**
83
	 * Private certificate
84
	 *
85
	 * @var string
86
	 */
87
	public $private_certificate;
88
89
	/**
90
	 * Private key
91
	 *
92
	 * @var string
93
	 */
94
	public $private_key;
95
96
	/**
97
	 * Private key password
98
	 *
99
	 * @var string
100
	 */
101
	public $private_key_password;
102
103
	/**
104
	 * Set the acquirer URL
105
	 *
106
	 * @param string $url URL.
107
	 * @return void
108
	 */
109
	public function set_acquirer_url( $url ) {
110
		$this->acquirer_url = $url;
111
112
		$this->directory_request_url   = $url;
113
		$this->transaction_request_url = $url;
114
		$this->status_request_url      = $url;
115
	}
116
117
	/**
118
	 * Send an specific request message to an specific URL
119
	 *
120
	 * @param string         $url     URL.
121
	 * @param RequestMessage $message Message.
122
	 * @return DirectoryResponseMessage|TransactionResponseMessage|AcquirerStatusResMessage
123
	 * @throws \Exception Throws exception on error with private key when signing document.
124
	 */
125
	private function send_message( $url, RequestMessage $message ) {
126
		// Sign.
127
		$document = $message->get_document();
128
		$document = $this->sign_document( $document );
129
130
		// Stringify.
131
		$data = $document->saveXML();
132
133
		/*
134
		 * Fix for a incorrect implementation at https://www.ideal-checkout.nl/simulator/.
135
		 *
136
		 * @since 1.1.11
137
		 */
138
		if ( 'https://www.ideal-checkout.nl/simulator/' === $url ) {
139
			$data = $document->C14N( true, false );
140
		}
141
142
		// Remote post.
143
		$response = wp_remote_post(
144
			$url,
145
			array(
146
				'method'  => 'POST',
147
				'headers' => array(
148
					'Content-Type' => 'text/xml; charset=' . Message::XML_ENCODING,
149
				),
150
				'body'    => $data,
151
			)
152
		);
153
154
		// Handle response.
155
		if ( $response instanceof WP_Error ) {
156
			throw new \Exception( $response->get_error_message() );
157
		}
158
159
		if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
160
			throw new \Exception(
161
				sprintf(
162
					/* translators: %s: response code */
163
					__( 'The response code (<code>%s<code>) from the iDEAL provider was incorrect.', 'pronamic_ideal' ),
164
					wp_remote_retrieve_response_code( $response )
165
				)
166
			);
167
		}
168
169
		$body = wp_remote_retrieve_body( $response );
170
171
		try {
172
			$xml = Core_Util::simplexml_load_string( $body );
173
		} catch ( \InvalidArgumentException $e ) {
174
			throw new \Exception( $e->getMessage() );
175
		}
176
177
		$result = $this->parse_document( $xml );
178
179
		return $result;
180
	}
181
182
	/**
183
	 * Parse the specified document and return parsed result
184
	 *
185
	 * @param SimpleXMLElement $document Document.
186
	 * @return DirectoryResponseMessage|TransactionResponseMessage|AcquirerStatusResMessage
187
	 * @throws \Exception Throws exception if response XML document can not be parsed.
188
	 */
189
	private function parse_document( SimpleXMLElement $document ) {
190
		$name = $document->getName();
191
192
		switch ( $name ) {
193
			case AcquirerErrorResMessage::NAME:
194
				$message = AcquirerErrorResMessage::parse( $document );
195
196
				throw new \Exception(
197
					sprintf( '%s. %s', $message->error->get_message(), $message->error->get_detail() )
198
				);
199
			case DirectoryResponseMessage::NAME:
200
				return DirectoryResponseMessage::parse( $document );
201
			case TransactionResponseMessage::NAME:
202
				return TransactionResponseMessage::parse( $document );
203
			case AcquirerStatusResMessage::NAME:
204
				return AcquirerStatusResMessage::parse( $document );
205
			default:
206
				throw new \Exception(
207
					/* translators: %s: XML document element name */
208
					sprintf( __( 'Unknwon iDEAL message (%s)', 'pronamic_ideal' ), $name )
209
				);
210
		}
211
	}
212
213
	/**
214
	 * Get directory of issuers
215
	 *
216
	 * @return null|Directory
217
	 */
218
	public function get_directory() {
219
		$directory = null;
220
221
		$request_dir_message = new DirectoryRequestMessage();
222
223
		$merchant = $request_dir_message->get_merchant();
224
		$merchant->set_id( $this->merchant_id );
225
		$merchant->set_sub_id( $this->sub_id );
226
227
		$response_dir_message = $this->send_message( $this->directory_request_url, $request_dir_message );
228
229
		if ( $response_dir_message instanceof DirectoryResponseMessage ) {
230
			$directory = $response_dir_message->get_directory();
231
		}
232
233
		return $directory;
234
	}
235
236
	/**
237
	 * Create transaction
238
	 *
239
	 * @param Transaction $transaction Transaction.
240
	 * @param string      $return_url  Return URL.
241
	 * @param string      $issuer_id   Issuer ID.
242
	 * @return TransactionResponseMessage
243
	 * @throws \Exception Throws exception on unexpected transaction request response.
244
	 */
245
	public function create_transaction( Transaction $transaction, $return_url, $issuer_id ) {
246
		$message = new TransactionRequestMessage();
247
248
		$merchant = $message->get_merchant();
249
		$merchant->set_id( $this->merchant_id );
250
		$merchant->set_sub_id( $this->sub_id );
251
		$merchant->set_return_url( $return_url );
252
253
		$message->issuer = new Issuer();
254
		$message->issuer->set_id( $issuer_id );
255
256
		$message->transaction = $transaction;
257
258
		$result = $this->send_message( $this->transaction_request_url, $message );
259
260
		if ( ! ( $result instanceof TransactionResponseMessage ) ) {
261
			throw new \Exception( 'Unexpected response for transaction request.' );
262
		}
263
264
		return $result;
265
	}
266
267
	/**
268
	 * Get the status of the specified transaction ID
269
	 *
270
	 * @param string $transaction_id Transaction ID.
271
	 * @return TransactionResponseMessage
272
	 * @throws \Exception Throws exception on unexpected acquirer status response.
273
	 */
274
	public function get_status( $transaction_id ) {
275
		$message = new AcquirerStatusReqMessage();
276
277
		$merchant = $message->get_merchant();
278
		$merchant->set_id( $this->merchant_id );
279
		$merchant->set_sub_id( $this->sub_id );
280
281
		$message->transaction = new Transaction();
282
		$message->transaction->set_id( $transaction_id );
283
284
		$result = $this->send_message( $this->status_request_url, $message );
285
286
		if ( ! ( $result instanceof AcquirerStatusResMessage ) ) {
287
			throw new \Exception( 'Unexpected response for acquirer status request.' );
288
		}
289
290
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type Pronamic\WordPress\Pay\G...cquirerStatusResMessage which is incompatible with the documented return type Pronamic\WordPress\Pay\G...nsactionResponseMessage.
Loading history...
291
	}
292
293
	/**
294
	 * Sign the specified DOMDocument
295
	 *
296
	 * @link https://github.com/Maks3w/xmlseclibs/blob/v1.3.0/tests/xml-sign.phpt
297
	 *
298
	 * @param DOMDocument $document Document.
299
	 * @return DOMDocument
300
	 * @throws \Exception Can not load private key.
301
	 */
302
	private function sign_document( DOMDocument $document ) {
303
		$dsig = new XMLSecurityDSig();
304
305
		/*
306
		 * For canonicalization purposes the exclusive (9) algorithm must be used.
307
		 *
308
		 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 30
309
		 */
310
		$dsig->setCanonicalMethod( XMLSecurityDSig::EXC_C14N );
311
312
		/*
313
		 * For hashing purposes the SHA-256 (11) algorithm must be used.
314
		 *
315
		 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 30
316
		 */
317
		$dsig->addReference(
318
			$document,
319
			XMLSecurityDSig::SHA256,
320
			array( 'http://www.w3.org/2000/09/xmldsig#enveloped-signature' ),
321
			array(
322
				'force_uri' => true,
323
			)
324
		);
325
326
		/*
327
		 * For signature purposes the RSAWithSHA 256 (12) algorithm must be used.
328
		 *
329
		 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 31
330
		 */
331
		$key = new XMLSecurityKey(
332
			XMLSecurityKey::RSA_SHA256,
333
			array(
334
				'type' => 'private',
335
			)
336
		);
337
338
		$key->passphrase = $this->private_key_password;
339
340
		$key->loadKey( $this->private_key );
341
342
		/*
343
		 * Test if we can get an private key object, to prevent the following error:
344
		 * Warning: openssl_sign() [function.openssl-sign]: supplied key param cannot be coerced into a private key.
345
		 */
346
		$private_key = openssl_get_privatekey( $this->private_key, $this->private_key_password );
347
348
		if ( false === $private_key ) {
349
			throw new \Exception( 'Can not load private key' );
350
		}
351
352
		// Sign.
353
		$dsig->sign( $key );
354
355
		/*
356
		 * The public key must be referenced using a fingerprint of an X.509 certificate. The
357
		 * fingerprint must be calculated according to the following formula HEX(SHA-1(DER certificate)) (13).
358
		 *
359
		 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 31
360
		 */
361
		$fingerprint = Security::get_sha_fingerprint( $this->private_certificate );
362
363
		if ( null === $fingerprint ) {
364
			throw new \Exception( 'Unable to calculate fingerprint of private certificate.' );
365
		}
366
367
		$dsig->addKeyInfoAndName( $fingerprint );
368
369
		// Add the signature.
370
		$dsig->appendSignature( $document->documentElement );
371
372
		return $document;
373
	}
374
}
375