Test Failed
Push — master ( 44b23a...ab385e )
by Remco
04:58 queued 16s
created

src/Client.php (7 issues)

1
<?php
2
3
namespace Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3;
4
5
use DOMDocument;
6
use Exception;
7
use Pronamic\WordPress\Pay\Core\Util as Core_Util;
8
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\AcquirerErrorResMessage;
9
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\AcquirerStatusReqMessage;
10
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\AcquirerStatusResMessage;
11
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\DirectoryRequestMessage;
12
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\DirectoryResponseMessage;
13
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\Message;
14
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\RequestMessage;
15
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\ResponseMessage;
16
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\TransactionRequestMessage;
17
use Pronamic\WordPress\Pay\Gateways\IDealAdvancedV3\XML\TransactionResponseMessage;
18
use SimpleXMLElement;
19
use WP_Error;
20
use XMLSecurityDSig;
21
use XMLSecurityKey;
22
23
/**
24
 * Title: iDEAL client
25
 * Description:
26
 * Copyright: 2005-2019 Pronamic
27
 * Company: Pronamic
28
 *
29
 * @author  Remco Tolsma
30
 * @version 2.0.0
31
 * @since   1.0.0
32
 */
33
class Client {
34
	/**
35
	 * Acquirer URL
36
	 *
37
	 * @var string
38
	 */
39
	public $acquirer_url;
40
41
	/**
42
	 * Directory request URL
43
	 *
44
	 * @var string
45
	 */
46
	public $directory_request_url;
47
48
	/**
49
	 * Transaction request URL
50
	 *
51
	 * @var string
52
	 */
53
	public $transaction_request_url;
54
55
	/**
56
	 * Status request URL
57
	 *
58
	 * @var string
59
	 */
60
	public $status_request_url;
61
62
	/**
63
	 * Merchant ID
64
	 *
65
	 * @var string
66
	 */
67
	public $merchant_id;
68
69
	/**
70
	 * Sub ID
71
	 *
72
	 * @var string
73
	 */
74
	public $sub_id;
75
76
	/**
77
	 * Private certificate
78
	 *
79
	 * @var string
80
	 */
81
	public $private_certificate;
82
83
	/**
84
	 * Private key
85
	 *
86
	 * @var string
87
	 */
88
	public $private_key;
89
90
	/**
91
	 * Private key password
92
	 *
93
	 * @var string
94
	 */
95
	public $private_key_password;
96
97
	/**
98
	 * Error
99
	 *
100
	 * @var WP_Error
101
	 */
102
	private $error;
103
104
	/**
105
	 * Constructs and initialzies an iDEAL Advanced v3 client object
106
	 */
107
	public function __construct() {
108
109
	}
110
111
	/**
112
	 * Get the latest error
113
	 *
114
	 * @return WP_Error or null
115
	 */
116
	public function get_error() {
117
		return $this->error;
118
	}
119
120
	/**
121
	 * Set the acquirer URL
122
	 *
123
	 * @param string $url URL.
124
	 */
125
	public function set_acquirer_url( $url ) {
126
		$this->acquirer_url = $url;
127
128
		$this->directory_request_url   = $url;
129
		$this->transaction_request_url = $url;
130
		$this->status_request_url      = $url;
131
	}
132
133
	/**
134
	 * Send an specific request message to an specific URL
135
	 *
136
	 * @param string         $url     URL.
137
	 * @param RequestMessage $message Message.
138
	 *
139
	 * @return ResponseMessage
140
	 */
141
	private function send_message( $url, RequestMessage $message ) {
142
		$result = false;
143
144
		// Sign.
145
		$document = $message->get_document();
146
		$document = $this->sign_document( $document );
147
148
		if ( false !== $document ) {
149
			// Stringify.
150
			$data = $document->saveXML();
151
152
			/*
153
			 * Fix for a incorrect implementation at https://www.ideal-checkout.nl/simulator/.
154
			 *
155
			 * @since 1.1.11
156
			 */
157
			if ( 'https://www.ideal-checkout.nl/simulator/' === $url ) {
158
				$data = $document->C14N( true, false );
159
			}
160
161
			// Remote post.
162
			$response = wp_remote_post(
163
				$url,
164
				array(
165
					'method'  => 'POST',
166
					'headers' => array(
167
						'Content-Type' => 'text/xml; charset=' . Message::XML_ENCODING,
168
					),
169
					'body'    => $data,
170
				)
171
			);
172
173
			// Handle response.
174
			if ( ! is_wp_error( $response ) ) {
175
				if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
0 ignored issues
show
It seems like $response can also be of type WP_Error; however, parameter $response of wp_remote_retrieve_response_code() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

175
				if ( 200 === wp_remote_retrieve_response_code( /** @scrutinizer ignore-type */ $response ) ) {
Loading history...
176
					$body = wp_remote_retrieve_body( $response );
0 ignored issues
show
It seems like $response can also be of type WP_Error; however, parameter $response of wp_remote_retrieve_body() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

176
					$body = wp_remote_retrieve_body( /** @scrutinizer ignore-type */ $response );
Loading history...
177
178
					$xml = Core_Util::simplexml_load_string( $body );
179
180
					if ( is_wp_error( $xml ) ) {
181
						$this->error = $xml;
0 ignored issues
show
Documentation Bug introduced by
It seems like $xml of type SimpleXMLElement is incompatible with the declared type WP_Error of property $error.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
182
					} else {
183
						$document = self::parse_document( $xml );
0 ignored issues
show
Bug Best Practice introduced by
The method Pronamic\WordPress\Pay\G...lient::parse_document() is not static, but was called statically. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
						/** @scrutinizer ignore-call */ 
184
      $document = self::parse_document( $xml );
Loading history...
184
185
						if ( is_wp_error( $document ) ) {
186
							$this->error = $document;
187
						} else {
188
							$result = $document;
189
						}
190
					}
191
				} else {
192
					$this->error = new WP_Error(
193
						'wrong_response_code',
194
						sprintf(
195
							/* translators: %s: response code */
196
							__( 'The response code (<code>%s<code>) from the iDEAL provider was incorrect.', 'pronamic_ideal' ),
197
							wp_remote_retrieve_response_code( $response )
198
						)
199
					);
200
				}
201
			} else {
202
				$this->error = $response;
203
			}
204
		}
205
206
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result returns the type WP_Error|false which is incompatible with the documented return type Pronamic\WordPress\Pay\G...dV3\XML\ResponseMessage.
Loading history...
207
	}
208
209
	/**
210
	 * Parse the specified document and return parsed result
211
	 *
212
	 * @param SimpleXMLElement $document Document.
213
	 *
214
	 * @return ResponseMessage|WP_Error
215
	 */
216
	private function parse_document( SimpleXMLElement $document ) {
217
		$this->error = null;
218
219
		$name = $document->getName();
220
221
		switch ( $name ) {
222
			case AcquirerErrorResMessage::NAME:
223
				$message = AcquirerErrorResMessage::parse( $document );
224
225
				$this->error = new WP_Error(
226
					'IDealAdvancedV3_error',
227
					sprintf( '%s. %s', $message->error->get_message(), $message->error->get_detail() ),
228
					$message->error
229
				);
230
231
				return $message;
232
			case DirectoryResponseMessage::NAME:
233
				return DirectoryResponseMessage::parse( $document );
234
			case TransactionResponseMessage::NAME:
235
				return TransactionResponseMessage::parse( $document );
236
			case AcquirerStatusResMessage::NAME:
237
				return AcquirerStatusResMessage::parse( $document );
238
			default:
239
				return new WP_Error(
240
					'IDealAdvancedV3_error',
241
					/* translators: %s: XML document element name */
242
					sprintf( __( 'Unknwon iDEAL message (%s)', 'pronamic_ideal' ), $name )
243
				);
244
		}
245
	}
246
247
	/**
248
	 * Get directory of issuers
249
	 *
250
	 * @return Directory
251
	 */
252
	public function get_directory() {
253
		$directory = false;
254
255
		$request_dir_message = new DirectoryRequestMessage();
256
257
		$merchant = $request_dir_message->get_merchant();
258
		$merchant->set_id( $this->merchant_id );
259
		$merchant->set_sub_id( $this->sub_id );
260
261
		$response_dir_message = $this->send_message( $this->directory_request_url, $request_dir_message );
262
263
		if ( $response_dir_message instanceof DirectoryResponseMessage ) {
264
			$directory = $response_dir_message->get_directory();
265
		}
266
267
		return $directory;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $directory returns the type Directory|false which is incompatible with the documented return type Pronamic\WordPress\Pay\G...ealAdvancedV3\Directory.
Loading history...
268
	}
269
270
	/**
271
	 * Create transaction
272
	 *
273
	 * @param Transaction $transaction Transaction.
274
	 * @param string      $return_url  Return URL.
275
	 * @param string      $issuer_id   Issuer ID.
276
	 *
277
	 * @return TransactionResponseMessage
278
	 */
279
	public function create_transaction( Transaction $transaction, $return_url, $issuer_id ) {
280
		$message = new TransactionRequestMessage();
281
282
		$merchant = $message->get_merchant();
283
		$merchant->set_id( $this->merchant_id );
284
		$merchant->set_sub_id( $this->sub_id );
285
		$merchant->set_return_url( $return_url );
286
287
		$message->issuer = new Issuer();
288
		$message->issuer->set_id( $issuer_id );
289
290
		$message->transaction = $transaction;
291
292
		return $this->send_message( $this->transaction_request_url, $message );
293
	}
294
295
	/**
296
	 * Get the status of the specified transaction ID
297
	 *
298
	 * @param string $transaction_id Transaction ID.
299
	 *
300
	 * @return TransactionResponseMessage
301
	 */
302
	public function get_status( $transaction_id ) {
303
		$message = new AcquirerStatusReqMessage();
304
305
		$merchant = $message->get_merchant();
306
		$merchant->set_id( $this->merchant_id );
307
		$merchant->set_sub_id( $this->sub_id );
308
309
		$message->transaction = new Transaction();
310
		$message->transaction->set_id( $transaction_id );
311
312
		return $this->send_message( $this->status_request_url, $message );
313
	}
314
315
	/**
316
	 * Sign the specified DOMDocument
317
	 *
318
	 * @link https://github.com/Maks3w/xmlseclibs/blob/v1.3.0/tests/xml-sign.phpt
319
	 *
320
	 * @param DOMDocument $document Document.
321
	 *
322
	 * @return DOMDocument
323
	 *
324
	 * @throws Exception Can not load private key.
325
	 */
326
	private function sign_document( DOMDocument $document ) {
327
		$result = false;
328
329
		try {
330
			$dsig = new XMLSecurityDSig();
331
332
			/*
333
			 * For canonicalization purposes the exclusive (9) algorithm must be used.
334
			 *
335
			 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 30
336
			 */
337
			$dsig->setCanonicalMethod( XMLSecurityDSig::EXC_C14N );
338
339
			/*
340
			 * For hashing purposes the SHA-256 (11) algorithm must be used.
341
			 *
342
			 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 30
343
			 */
344
			$dsig->addReference(
345
				$document,
346
				XMLSecurityDSig::SHA256,
347
				array( 'http://www.w3.org/2000/09/xmldsig#enveloped-signature' ),
348
				array(
349
					'force_uri' => true,
350
				)
351
			);
352
353
			/*
354
			 * For signature purposes the RSAWithSHA 256 (12) algorithm must be used.
355
			 *
356
			 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 31
357
			 */
358
			$key = new XMLSecurityKey(
359
				XMLSecurityKey::RSA_SHA256,
360
				array(
361
					'type' => 'private',
362
				)
363
			);
364
365
			$key->passphrase = $this->private_key_password;
366
367
			$key->loadKey( $this->private_key );
368
369
			/*
370
			 * Test if we can get an private key object, to prevent the following error:
371
			 * Warning: openssl_sign() [function.openssl-sign]: supplied key param cannot be coerced into a private key.
372
			 */
373
			$result = openssl_get_privatekey( $this->private_key, $this->private_key_password );
374
375
			if ( false !== $result ) {
376
				// Sign.
377
				$dsig->sign( $key );
378
379
				/*
380
				 * The public key must be referenced using a fingerprint of an X.509 certificate. The
381
				 * fingerprint must be calculated according to the following formula HEX(SHA-1(DER certificate)) (13).
382
				 *
383
				 * @link http://pronamic.nl/wp-content/uploads/2012/12/iDEAL-Merchant-Integration-Guide-ENG-v3.3.1.pdf #page 31
384
				 */
385
				$fingerprint = Security::get_sha_fingerprint( $this->private_certificate );
386
387
				$dsig->addKeyInfoAndName( $fingerprint );
388
389
				// Add the signature.
390
				$dsig->appendSignature( $document->documentElement );
391
392
				$result = $document;
393
			} else {
394
				throw new Exception( 'Can not load private key' );
395
			}
396
		} catch ( Exception $e ) {
397
			$this->error = new WP_Error( 'xml_security', $e->getMessage(), $e );
398
		}
399
400
		return $result;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $result also could return the type false|resource which is incompatible with the documented return type DOMDocument.
Loading history...
401
	}
402
}
403