Issues (11)

src/PaymentsController.php (2 issues)

Labels
Severity
1
<?php
2
/**
3
 * Payments controller
4
 *
5
 * @author    Pronamic <[email protected]>
6
 * @copyright 2005-2020 Pronamic
7
 * @license   GPL-3.0-or-later
8
 * @package   Pronamic\WordPress\Pay\Gateways\Adyen
9
 */
10
11
namespace Pronamic\WordPress\Pay\Gateways\Adyen;
12
13
use JsonSchema\Exception\ValidationException;
14
use Pronamic\WordPress\Pay\Core\Server;
15
use Pronamic\WordPress\Pay\Plugin;
16
use WP_REST_Request;
17
18
/**
19
 * Payments result controller
20
 *
21
 * @link https://docs.adyen.com/developers/checkout/web-sdk/customization/logic#beforecomplete
22
 *
23
 * @author  Reüel van der Steege
24
 * @version 1.1.2
25
 * @since   1.1.0
26
 */
27
class PaymentsController {
28
	/**
29
	 * Setup.
30
	 *
31
	 * @return void
32
	 */
33 1
	public function setup() {
34 1
		add_action( 'rest_api_init', array( $this, 'rest_api_init' ) );
35 1
	}
36
37
	/**
38
	 * REST API init.
39
	 *
40
	 * @link https://developer.wordpress.org/rest-api/extending-the-rest-api/adding-custom-endpoints/
41
	 * @link https://developer.wordpress.org/reference/hooks/rest_api_init/
42
	 *
43
	 * @return void
44
	 */
45 10
	public function rest_api_init() {
46
		// Register REST route `/payments//{payment_id}`.
47 10
		register_rest_route(
48 10
			Integration::REST_ROUTE_NAMESPACE,
49 10
			'/payments/(?P<payment_id>\d+)',
50
			array(
51 10
				'methods'             => 'POST',
52 10
				'callback'            => array( $this, 'rest_api_adyen_payments' ),
53 10
				'permission_callback' => '__return_true',
54
				'args'                => array(
55
					'payment_id' => array(
56 10
						'description' => __( 'Payment ID.', 'pronamic_ideal' ),
57 10
						'type'        => 'integer',
58
					),
59
				),
60
			)
61
		);
62
63
		// Register REST route `/payments/details/{payment_id}`.
64 10
		register_rest_route(
65 10
			Integration::REST_ROUTE_NAMESPACE,
66 10
			'/payments/details/(?P<payment_id>\d+)',
67
			array(
68 10
				'methods'             => 'POST',
69 10
				'callback'            => array( $this, 'rest_api_adyen_payment_details' ),
70 10
				'permission_callback' => '__return_true',
71
				'args'                => array(
72
					'payment_id' => array(
73 10
						'description' => __( 'Payment ID.', 'pronamic_ideal' ),
74 10
						'type'        => 'integer',
75
					),
76
				),
77
			)
78
		);
79
80
		// Register REST route `/payments/applepay/merchant-validation/`.
81 10
		register_rest_route(
82 10
			Integration::REST_ROUTE_NAMESPACE,
83 10
			'/payments/applepay/merchant-validation/(?P<payment_id>\d+)',
84
			array(
85 10
				'methods'             => 'POST',
86 10
				'callback'            => array( $this, 'rest_api_applepay_merchant_validation' ),
87 10
				'permission_callback' => '__return_true',
88
				'args'                => array(
89
					'payment_id' => array(
90 10
						'description' => __( 'Payment ID.', 'pronamic_ideal' ),
91 10
						'type'        => 'integer',
92
					),
93
				),
94
			)
95
		);
96 10
	}
97
98
	/**
99
	 * REST API Adyen payments handler.
100
	 *
101
	 * @param WP_REST_Request $request Request.
102
	 * @return object
103
	 * @throws \Exception Throws exception on Adyen service exception response.
104
	 */
105
	public function rest_api_adyen_payments( WP_REST_Request $request ) {
106
		$payment_id = $request->get_param( 'payment_id' );
107
108
		// Payment ID.
109
		if ( null === $payment_id ) {
110
			return new \WP_Error(
111
				'pronamic-pay-adyen-no-payment-id',
112
				__( 'No payment ID given in `payment_id` parameter.', 'pronamic_ideal' )
113
			);
114
		}
115
116
		$payment = \get_pronamic_payment( $payment_id );
117
118
		if ( null === $payment ) {
119
			return new \WP_Error(
120
				'pronamic-pay-adyen-payment-not-found',
121
				sprintf(
122
					/* translators: %s: payment ID */
123
					__( 'Could not find payment with ID `%s`.', 'pronamic_ideal' ),
124
					$payment_id
125
				),
126
				$payment_id
127
			);
128
		}
129
130
		// State data.
131
		$data = \json_decode( $request->get_body() );
132
133
		if ( null === $data ) {
134
			return new \WP_Error(
135
				'pronamic-pay-adyen-no-data',
136
				__( 'No data given in request body.', 'pronamic_ideal' )
137
			);
138
		}
139
140
		// Gateway.
141
		$config_id = $payment->get_config_id();
142
143
		if ( null === $config_id ) {
144
			return new \WP_Error(
145
				'pronamic-pay-adyen-no-config',
146
				__( 'No gateway configuration ID given in payment.', 'pronamic_ideal' )
147
			);
148
		}
149
150
		$gateway = Plugin::get_gateway( $config_id );
0 ignored issues
show
Are you sure the assignment to $gateway is correct as Pronamic\WordPress\Pay\P...get_gateway($config_id) targeting Pronamic\WordPress\Pay\Plugin::get_gateway() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
151
152
		if ( empty( $gateway ) ) {
153
			return new \WP_Error(
154
				'pronamic-pay-adyen-gateway-not-found',
155
				sprintf(
156
					/* translators: %s: Gateway configuration ID */
157
					__( 'Could not find gateway with ID `%s`.', 'pronamic_ideal' ),
158
					$config_id
159
				),
160
				$config_id
161
			);
162
		}
163
164
		if ( ! isset( $gateway->client ) ) {
165
			return new \WP_Error(
166
				'pronamic-pay-adyen-client-not-found',
167
				sprintf(
168
					/* translators: %s: Gateway configuration ID */
169
					__( 'Could not find client in gateway with ID `%s`.', 'pronamic_ideal' ),
170
					$config_id
171
				),
172
				$config_id
173
			);
174
		}
175
176
		// Create payment.
177
		// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Adyen JSON object.
178
		if ( ! isset( $data->paymentMethod->type ) ) {
179
			return new \WP_Error(
180
				'pronamic-pay-adyen-no-payment-method',
181
				__( 'No payment method given.', 'pronamic_ideal' )
182
			);
183
		}
184
185
		// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Adyen JSON object.
186
		$payment_method = PaymentMethod::from_object( $data->paymentMethod );
187
188
		try {
189
			if ( ! \is_callable( array( $gateway, 'create_payment' ) ) ) {
190
				return (object) array(
191
					'error' => __( 'Gateway does not support method to create payment.', 'pronamic_ideal' ),
192
				);
193
			}
194
195
			try {
196
				$response = $gateway->create_payment( $payment, $payment_method, $data );
197
			} catch ( \Pronamic\WordPress\Pay\Gateways\Adyen\ServiceException $service_exception ) {
198
				$message = $service_exception->getMessage();
199
200
				$error_code = $service_exception->get_error_code();
201
202
				if ( ! empty( $error_code ) ) {
203
					$message = sprintf(
204
						/* translators: 1: error message, 2: error code */
205
						__( '%1$s (error %2$s)', 'pronamic_ideal' ),
206
						$service_exception->getMessage(),
207
						$error_code
208
					);
209
				}
210
211
				throw new \Exception( $message );
212
			}
213
		} catch ( \Exception $e ) {
214
			$error = $e->getMessage();
215
216
			$error_code = $e->getCode();
217
218
			if ( ! empty( $error_code ) ) {
219
				$error = sprintf( '%s - %s', $error_code, $e->getMessage() );
220
			}
221
222
			return (object) array( 'error' => $error );
223
		}
224
225
		// Update payment status based on response.
226
		PaymentResponseHelper::update_payment( $payment, $response );
227
228
		$result = array(
229
			'resultCode' => $response->get_result_code(),
230
		);
231
232
		// Return action if available.
233
		$action = $response->get_action();
234
235
		if ( null !== $action ) {
236
			$result['action'] = $action->get_json();
237
		}
238
239
		// Return refusal reason if available.
240
		$refusal_reason = $response->get_refusal_reason();
241
242
		if ( null !== $refusal_reason ) {
243
			$result['refusalReason'] = $refusal_reason;
244
		}
245
246
		return (object) $result;
247
	}
248
249
	/**
250
	 * REST API Adyen payment details handler.
251
	 *
252
	 * @param WP_REST_Request $request Request.
253
	 * @return object
254
	 * @throws \Exception Throws exception on Adyen service exception response.
255
	 */
256
	public function rest_api_adyen_payment_details( WP_REST_Request $request ) {
257
		$payment_id = $request->get_param( 'payment_id' );
258
259
		// Payment ID.
260
		if ( null === $payment_id ) {
261
			return new \WP_Error(
262
				'pronamic-pay-adyen-no-payment-id',
263
				__( 'No payment ID given in `payment_id` parameter.', 'pronamic_ideal' )
264
			);
265
		}
266
267
		$payment = \get_pronamic_payment( $payment_id );
268
269
		if ( null === $payment ) {
270
			return new \WP_Error(
271
				'pronamic-pay-adyen-payment-not-found',
272
				sprintf(
273
					/* translators: %s: payment ID */
274
					__( 'Could not find payment with ID `%s`.', 'pronamic_ideal' ),
275
					$payment_id
276
				),
277
				$payment_id
278
			);
279
		}
280
281
		// State data.
282
		$data = \json_decode( $request->get_body() );
283
284
		if ( null === $data ) {
285
			return new \WP_Error(
286
				'pronamic-pay-adyen-no-data',
287
				__( 'No data given in request body.', 'pronamic_ideal' )
288
			);
289
		}
290
291
		// Gateway.
292
		$config_id = $payment->get_config_id();
293
294
		if ( null === $config_id ) {
295
			return new \WP_Error(
296
				'pronamic-pay-adyen-no-config',
297
				__( 'No gateway configuration ID given in payment.', 'pronamic_ideal' )
298
			);
299
		}
300
301
		$gateway = Plugin::get_gateway( $config_id );
0 ignored issues
show
Are you sure the assignment to $gateway is correct as Pronamic\WordPress\Pay\P...get_gateway($config_id) targeting Pronamic\WordPress\Pay\Plugin::get_gateway() seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
302
303
		if ( empty( $gateway ) ) {
304
			return new \WP_Error(
305
				'pronamic-pay-adyen-gateway-not-found',
306
				sprintf(
307
					/* translators: %s: Gateway configuration ID */
308
					__( 'Could not find gateway with ID `%s`.', 'pronamic_ideal' ),
309
					$config_id
310
				),
311
				$config_id
312
			);
313
		}
314
315
		if ( ! isset( $gateway->client ) ) {
316
			return new \WP_Error(
317
				'pronamic-pay-adyen-client-not-found',
318
				sprintf(
319
					/* translators: %s: Gateway configuration ID */
320
					__( 'Could not find client in gateway with ID `%s`.', 'pronamic_ideal' ),
321
					$config_id
322
				),
323
				$config_id
324
			);
325
		}
326
327
		// phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase -- Adyen JSON object.
328
		if ( ! isset( $data->paymentMethod->type ) ) {
329
			return new \WP_Error(
330
				'pronamic-pay-adyen-no-payment-method',
331
				__( 'No payment method given.', 'pronamic_ideal' )
332
			);
333
		}
334
335
		// Send additional payment details.
336
		$payment_details_request = new PaymentDetailsRequest();
337
338
		// Set payment data from original payment response.
339
		$payment_response = $payment->get_meta( 'adyen_payment_response' );
340
341
		if ( is_string( $payment_response ) && '' !== $payment_response ) {
342
			$payment_response = \json_decode( $payment_response );
343
344
			$payment_response = PaymentResponse::from_object( $payment_response );
345
346
			$payment_data = $payment_response->get_payment_data();
347
348
			$payment_details_request->set_payment_data( $payment_data );
349
		}
350
351
		try {
352
			if ( ! \is_callable( array( $gateway, 'send_payment_details' ) ) ) {
353
				return (object) array(
354
					'error' => __( 'Gateway does not support sending additional payment details.', 'pronamic_ideal' ),
355
				);
356
			}
357
358
			try {
359
				$response = $gateway->send_payment_details( $payment_details_request );
360
			} catch ( \Pronamic\WordPress\Pay\Gateways\Adyen\ServiceException $service_exception ) {
361
				$message = $service_exception->getMessage();
362
363
				$error_code = $service_exception->get_error_code();
364
365
				if ( ! empty( $error_code ) ) {
366
					$message = sprintf(
367
					/* translators: 1: error message, 2: error code */
368
						__( '%1$s (error %2$s)', 'pronamic_ideal' ),
369
						$service_exception->getMessage(),
370
						$error_code
371
					);
372
				}
373
374
				throw new \Exception( $message );
375
			}
376
377
			// Update payment status based on response.
378
			PaymentResponseHelper::update_payment( $payment, $response );
379
		} catch ( \Exception $e ) {
380
			$error = $e->getMessage();
381
382
			$error_code = $e->getCode();
383
384
			if ( ! empty( $error_code ) ) {
385
				$error = sprintf( '%s - %s', $error_code, $e->getMessage() );
386
			}
387
388
			return (object) array( 'error' => $error );
389
		}
390
391
		$result = array(
392
			'resultCode' => $response->get_result_code(),
393
		);
394
395
		// Return action if available.
396
		$action = $response->get_action();
397
398
		if ( null !== $action ) {
399
			$result['action'] = $action->get_json();
400
		}
401
402
		// Return refusal reason if available.
403
		$refusal_reason = $response->get_refusal_reason();
404
405
		if ( null !== $refusal_reason ) {
406
			$result['refusalReason'] = $refusal_reason;
407
		}
408
409
		return (object) $result;
410
	}
411
412
	/**
413
	 * REST API Apple Pay merchant validation handler.
414
	 *
415
	 * @param WP_REST_Request $request Request.
416
	 * @return object
417
	 * @throws \Exception Throws exception on merchant identity files problems.
418
	 */
419
	public function rest_api_applepay_merchant_validation( WP_REST_Request $request ) {
420
		$payment_id = $request->get_param( 'payment_id' );
421
422
		// Payment ID.
423
		if ( null === $payment_id ) {
424
			return new \WP_Error(
425
				'pronamic-pay-adyen-no-payment-id',
426
				__( 'No payment ID given in `payment_id` parameter.', 'pronamic_ideal' )
427
			);
428
		}
429
430
		$payment = \get_pronamic_payment( $payment_id );
431
432
		if ( null === $payment ) {
433
			return new \WP_Error(
434
				'pronamic-pay-adyen-payment-not-found',
435
				sprintf(
436
					/* translators: %s: payment ID */
437
					__( 'Could not find payment with ID `%s`.', 'pronamic_ideal' ),
438
					$payment_id
439
				),
440
				$payment_id
441
			);
442
		}
443
444
		// State data.
445
		$data = \json_decode( $request->get_body() );
446
447
		if ( null === $data ) {
448
			return new \WP_Error(
449
				'pronamic-pay-adyen-no-data',
450
				__( 'No data given in request body.', 'pronamic_ideal' )
451
			);
452
		}
453
454
		// Gateway.
455
		$config_id = $payment->get_config_id();
456
457
		if ( null === $config_id ) {
458
			return new \WP_Error(
459
				'pronamic-pay-adyen-no-config',
460
				__( 'No gateway configuration ID given in payment.', 'pronamic_ideal' )
461
			);
462
		}
463
464
		$gateway = Plugin::get_gateway( $config_id );
465
466
		if ( empty( $gateway ) ) {
467
			return new \WP_Error(
468
				'pronamic-pay-adyen-gateway-not-found',
469
				sprintf(
470
					/* translators: %s: Gateway configuration ID */
471
					__( 'Could not find gateway with ID `%s`.', 'pronamic_ideal' ),
472
					$config_id
473
				),
474
				$config_id
475
			);
476
		}
477
478
		if ( ! isset( $gateway->client ) ) {
479
			return new \WP_Error(
480
				'pronamic-pay-adyen-client-not-found',
481
				sprintf(
482
					/* translators: %s: Gateway configuration ID */
483
					__( 'Could not find client in gateway with ID `%s`.', 'pronamic_ideal' ),
484
					$config_id
485
				),
486
				$config_id
487
			);
488
		}
489
490
		// Merchant identifier.
491
		$integration = new Integration();
492
493
		$config = $integration->get_config( $config_id );
494
495
		$merchant_identifier = $config->get_apple_pay_merchant_id();
496
497
		if ( empty( $merchant_identifier ) ) {
498
			return new \WP_Error(
499
				'pronamic-pay-adyen-applepay-no-merchant-identifier',
500
				__( 'Apple Pay merchant identifier not configured in gateway settings.', 'pronamic_ideal' )
501
			);
502
		}
503
504
		if ( ! isset( $data->validation_url ) ) {
505
			return new \WP_Error(
506
				'pronamic-pay-adyen-applepay-no-validation-url',
507
				__( 'No Apple Pay merchant validation URL given.', 'pronamic_ideal' )
508
			);
509
		}
510
511
		/*
512
		 * Request an Apple Pay payment session.
513
		 *
514
		 * @link https://developer.apple.com/documentation/apple_pay_on_the_web/applepaysession/1778021-onvalidatemerchant
515
		 * @link https://developer.apple.com/documentation/apple_pay_on_the_web/apple_pay_js_api/requesting_an_apple_pay_payment_session
516
		 * @link https://docs.adyen.com/payment-methods/apple-pay/web-drop-in#show-apple-pay-in-your-payment-form
517
		 */
518
		$request = array(
519
			'merchantIdentifier' => $merchant_identifier,
520
			'displayName'        => \get_bloginfo( 'name' ),
521
			'initiative'         => 'web',
522
			'initiativeContext'  => Server::get( 'HTTP_HOST', FILTER_SANITIZE_STRING ),
523
		);
524
525
		try {
526
			add_action( 'http_api_curl', array( $this, 'http_curl_applepay_merchant_identity' ), 10, 2 );
527
528
			$certificate = $config->get_apple_pay_merchant_id_certificate();
529
			$private_key = $config->get_apple_pay_merchant_id_private_key();
530
531
			if ( empty( $certificate ) || empty( $private_key ) ) {
532
				throw new \Exception( __( 'Invalid Apple Pay Merchant Identity configuration.', 'pronamic_ideal' ) );
533
			}
534
535
			// Create temporary files for merchant validation.
536
			$certificate_file = \tmpfile();
537
			$private_key_file = \tmpfile();
538
539
			if ( false === $certificate_file || false === $private_key_file ) {
540
				throw new \Exception( __( 'Error creating merchant identity files.', 'pronamic_ideal' ) );
541
			}
542
543
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite -- Temporary files.
544
			\fwrite( $certificate_file, $certificate );
545
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fwrite -- Temporary files.
546
			\fwrite( $private_key_file, $private_key );
547
548
			// Validate merchant.
549
			$response = \wp_remote_request(
550
				$data->validation_url,
551
				array(
552
					'method'                           => 'POST',
553
					'headers'                          => array(
554
						'Content-Type' => 'application/json',
555
					),
556
					'body'                             => \wp_json_encode( (object) $request ),
557
					'adyen_applepay_merchant_identity' => array(
558
						'certificate_path'     => stream_get_meta_data( $certificate_file )['uri'],
559
						'private_key_path'     => stream_get_meta_data( $private_key_file )['uri'],
560
						'private_key_password' => $config->get_apple_pay_merchant_id_private_key_password(),
561
					),
562
				)
563
			);
564
565
			// Remove temporary files.
566
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- Temporary files.
567
			\fclose( $certificate_file );
568
			// phpcs:ignore WordPress.WP.AlternativeFunctions.file_system_read_fclose -- Temporary files.
569
			\fclose( $private_key_file );
570
571
			$body = \wp_remote_retrieve_body( $response );
572
573
			$result = \json_decode( $body );
574
		} catch ( \Exception $e ) {
575
			$error = $e->getMessage();
576
577
			$error_code = $e->getCode();
578
579
			if ( ! empty( $error_code ) ) {
580
				$error = sprintf( '%s - %s', $error_code, $e->getMessage() );
581
			}
582
583
			return (object) array( 'error' => $error );
584
		}
585
586
		return (object) $result;
587
	}
588
589
	/**
590
	 * HTTP CURL options for Apple Pay merchant validation.
591
	 *
592
	 * @param resource             $handle      CURL handle.
593
	 * @param array<array<string>> $parsed_args Parsed arguments.
594
	 * @return void
595
	 * @throws \Exception Throws exception on error while reading temporary files.
596
	 */
597
	public function http_curl_applepay_merchant_identity( $handle, $parsed_args ) {
598
		if ( ! isset( $parsed_args['adyen_applepay_merchant_identity'] ) ) {
599
			return;
600
		}
601
602
		$merchant_identity = $parsed_args['adyen_applepay_merchant_identity'];
603
604
		$certificate_path     = $merchant_identity['certificate_path'];
605
		$private_key_path     = $merchant_identity['private_key_path'];
606
		$private_key_password = $merchant_identity['private_key_password'];
607
608
		// Check temporary files existence.
609
		if ( ! \is_readable( $certificate_path ) || ! \is_readable( $private_key_path ) ) {
610
			throw new \Exception( __( 'Error reading merchant identity files.', 'pronamic_ideal' ) );
611
		}
612
613
		// Set merchant identity certificate and private key SSL options.
614
		// phpcs:disable WordPress.WP.AlternativeFunctions.curl_curl_setopt
615
		\curl_setopt( $handle, CURLOPT_SSLCERT, $certificate_path );
616
		\curl_setopt( $handle, CURLOPT_SSLKEY, $private_key_path );
617
618
		// Set merchant identity private key password.
619
		if ( ! empty( $private_key_password ) ) {
620
			\curl_setopt( $handle, CURLOPT_SSLKEYPASSWD, $private_key_password );
621
		}
622
623
		// phpcs:enable WordPress.WP.AlternativeFunctions.curl_curl_setopt
624
	}
625
}
626