Failed Conditions
Push — develop ( 5ffddc...a4aad9 )
by Reüel
06:27
created

src/Client.php (5 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-2019 Pronamic
25
 * Company: Pronamic
26
 *
27
 * @author  Remco Tolsma
28
 * @version 2.0.0
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 \Pronamic\WordPress\Pay\GatewayException( 'ideal_advanced_v3', $response->get_error_message() );
0 ignored issues
show
The type Pronamic\WordPress\Pay\GatewayException 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...
151
			}
152
153
			if ( 200 !== wp_remote_retrieve_response_code( $response ) ) {
154
				throw new \Pronamic\WordPress\Pay\GatewayException(
155
					'ideal_advanced_v3',
156
					sprintf(
157
						/* translators: %s: response code */
158
						__( 'The response code (<code>%s<code>) from the iDEAL provider was incorrect.', 'pronamic_ideal' ),
159
						wp_remote_retrieve_response_code( $response )
160
					),
161
					$response
162
				);
163
			}
164
165
			$body = wp_remote_retrieve_body( $response );
166
167
			try {
168
				$xml = Core_Util::simplexml_load_string( $body );
169
			} catch ( \InvalidArgumentException $e ) {
170
				throw new \Pronamic\WordPress\Pay\GatewayException( 'ideal_advanced_v3', $e->getMessage(), $body );
171
			}
172
173
			$result = 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

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