Test Failed
Push — develop ( 983888...05205d )
by Reüel
02:39
created

src/Client.php (14 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;
0 ignored issues
show
The type WP_Error was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
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(
0 ignored issues
show
The function wp_remote_post was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

162
			$response = /** @scrutinizer ignore-call */ wp_remote_post( $url, array(
Loading history...
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 ) ) {
0 ignored issues
show
The function is_wp_error was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

171
			if ( ! /** @scrutinizer ignore-call */ is_wp_error( $response ) ) {
Loading history...
172
				if ( 200 === wp_remote_retrieve_response_code( $response ) ) {
0 ignored issues
show
The function wp_remote_retrieve_response_code was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

172
				if ( 200 === /** @scrutinizer ignore-call */ wp_remote_retrieve_response_code( $response ) ) {
Loading history...
173
					$body = wp_remote_retrieve_body( $response );
0 ignored issues
show
The function wp_remote_retrieve_body was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

173
					$body = /** @scrutinizer ignore-call */ wp_remote_retrieve_body( $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' ),
0 ignored issues
show
The function __ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

193
							/** @scrutinizer ignore-call */ 
194
       __( 'The response code (<code>%s<code>) from the iDEAL provider was incorrect.', 'pronamic_ideal' ),
Loading history...
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 could also return false which is incompatible with the documented return type Pronamic\WordPress\Pay\G...dV3\XML\ResponseMessage. Did you maybe forget to handle an error condition?

If the returned type also contains false, it is an indicator that maybe an error condition leading to the specific return statement remains unhandled.

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 )
0 ignored issues
show
The function __ was not found. Maybe you did not declare it correctly or list all dependencies? ( Ignorable by Annotation )

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

239
					sprintf( /** @scrutinizer ignore-call */ __( 'Unknwon iDEAL message (%s)', 'pronamic_ideal' ), $name )
Loading history...
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 );
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...
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 );
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...
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