Failed Conditions
Push — develop ( 7b59d6...8cdaad )
by Reüel
03:46
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
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
137
	 * @param RequestMessage $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( $url, array(
163
				'method'  => 'POST',
164
				'headers' => array(
165
					'Content-Type' => 'text/xml; charset=' . Message::XML_ENCODING,
166
				),
167
				'body'    => $data,
168
			) );
169
170
			// Handle response
171
			if ( ! is_wp_error( $response ) ) {
172
				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

172
				if ( 200 === wp_remote_retrieve_response_code( /** @scrutinizer ignore-type */ $response ) ) {
Loading history...
173
					$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

173
					$body = wp_remote_retrieve_body( /** @scrutinizer ignore-type */ $response );
Loading history...
174
175
					$xml = Core_Util::simplexml_load_string( $body );
176
177
					if ( is_wp_error( $xml ) ) {
178
						$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...
179
					} else {
180
						$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

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