Failed Conditions
Push — develop ( e144d7...9a75d9 )
by Remco
07:40
created

src/Client.php (2 issues)

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