Passed
Push — develop-3.3.x ( 378726...43b836 )
by Mario
04:29
created

ipn_paypal::init_curl_session()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 27
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 18
nc 2
nop 1
dl 0
loc 27
rs 9.6666
c 0
b 0
f 0
1
<?php
2
/**
3
 *
4
 * PayPal Donation extension for the phpBB Forum Software package.
5
 *
6
 * @copyright (c) 2015-2020 Skouat
7
 * @license GNU General Public License, version 2 (GPL-2.0)
8
 *
9
 * Special Thanks to the following individuals for their inspiration:
10
 *    David Lewis (Highway of Life) http://startrekguide.com
11
 *    Micah Carrick ([email protected]) http://www.micahcarrick.com
12
 */
13
14
namespace skouat\ppde\controller;
15
16
use phpbb\config\config;
17
use phpbb\language\language;
18
use phpbb\request\request;
19
20
class ipn_paypal
21
{
22
	/**
23
	 * Args from PayPal notify return URL
24
	 *
25
	 * @var string
26
	 */
27
	private $args_return_uri = [];
28
	/** Production and Sandbox Postback URL
29
	 *
30
	 * @var array
31
	 */
32
	private static $remote_uri = [
33
		['hostname' => 'www.paypal.com', 'uri' => 'https://www.paypal.com/cgi-bin/webscr', 'type' => 'live'],
34
		['hostname' => 'www.sandbox.paypal.com', 'uri' => 'https://www.sandbox.paypal.com/cgi-bin/webscr', 'type' => 'sandbox'],
35
		['hostname' => 'ipnpb.paypal.com', 'uri' => 'https://ipnpb.paypal.com/cgi-bin/webscr', 'type' => 'live'],
36
		['hostname' => 'ipnpb.sandbox.paypal.com', 'uri' => 'https://ipnpb.sandbox.paypal.com/cgi-bin/webscr', 'type' => 'sandbox'],
37
	];
38
39
	protected $config;
40
	protected $language;
41
	protected $ppde_ext_manager;
42
	protected $ppde_ipn_log;
43
	protected $request;
44
	/**
45
	 * @var array
46
	 */
47
	private $curl_fsock = ['curl' => false, 'none' => true];
48
	/**
49
	 * @var array
50
	 */
51
	private $postback_args = [];
52
	/**
53
	 * Full PayPal response for include in text report
54
	 *
55
	 * @var string
56
	 */
57
	private $report_response = '';
58
	/**
59
	 * PayPal response (VERIFIED or INVALID)
60
	 *
61
	 * @var string
62
	 */
63
	private $response = '';
64
	/**
65
	 * PayPal response status (code 200 or other)
66
	 *
67
	 * @var string
68
	 */
69
	private $response_status = '';
70
	/**
71
	 * PayPal URL
72
	 * Could be Sandbox URL ou normal PayPal URL.
73
	 *
74
	 * @var string
75
	 */
76
	private $u_paypal = '';
77
78
	/**
79
	 * Constructor
80
	 *
81
	 * @param config            $config           Config object
82
	 * @param language          $language         Language user object
83
	 * @param extension_manager $ppde_ext_manager Extension manager object
84
	 * @param ipn_log           $ppde_ipn_log     IPN log
85
	 * @param request           $request          Request object
86
	 *
87
	 * @access public
88
	 */
89
	public function __construct(
90
		config $config,
91
		language $language,
92
		extension_manager $ppde_ext_manager,
93
		ipn_log $ppde_ipn_log,
94
		request $request
95
	)
96
	{
97
		$this->config = $config;
98
		$this->language = $language;
99
		$this->ppde_ext_manager = $ppde_ext_manager;
100
		$this->ppde_ipn_log = $ppde_ipn_log;
101
		$this->request = $request;
102
	}
103
104
	/**
105
	 * @return array
106
	 */
107
	public static function get_remote_uri(): array
108
	{
109
		return self::$remote_uri;
110
	}
111
112
	/**
113
	 * Initiate communication with PayPal.
114
	 * We use cURL. If it is not available we log an error.
115
	 *
116
	 * @param array $data
117
	 *
118
	 * @return void
119
	 * @access public
120
	 */
121
	public function initiate_paypal_connection($data): void
122
	{
123
		if ($this->curl_fsock['curl'])
124
		{
125
			$this->curl_post($this->args_return_uri);
126
			return;
127
		}
128
129
		$this->log_paypal_connection_error($data);
130
	}
131
132
	private function log_paypal_connection_error($data): void
133
	{
134
		$this->ppde_ipn_log->log_error(
135
			$this->language->lang('NO_CONNECTION_DETECTED'),
136
			$this->ppde_ipn_log->is_use_log_error(),
137
			true,
138
			E_USER_ERROR,
139
			$data
140
		);
141
	}
142
143
	/**
144
	 * Post Back Using cURL
145
	 *
146
	 * Sends the post back to PayPal using the cURL library. Called by
147
	 * the validate_transaction() method if the curl_fsock['curl'] property is true.
148
	 * Throws an exception if the post fails. Populates the response and response_status properties on success.
149
	 *
150
	 * @param string $encoded_data The post data as a URL encoded string
151
	 *
152
	 * @return void
153
	 * @access private
154
	 */
155
	private function curl_post($encoded_data): void
156
	{
157
		$ch = $this->init_curl_session($encoded_data);
158
		$this->valuate_response(curl_exec($ch), $ch);
159
		if ($this->ppde_ipn_log->is_use_log_error())
160
		{
161
			$this->parse_curl_response();
162
		}
163
		curl_close($ch);
164
	}
165
166
	/**
167
	 * Initializes a cURL session with the specified encoded data.
168
	 *
169
	 * @param string $encoded_data The encoded data to be sent in the cURL request.
170
	 *
171
	 * @return resource Returns a cURL session handle on success, false on failure.
172
	 * @access private
173
	 */
174
	private function init_curl_session($encoded_data)
175
	{
176
		$ch = curl_init($this->u_paypal);
177
178
		curl_setopt_array($ch, [
179
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
180
			CURLOPT_POST           => true,
181
			CURLOPT_RETURNTRANSFER => true,
182
			CURLOPT_POSTFIELDS     => $encoded_data,
183
			CURLOPT_SSLVERSION     => 6,
184
			CURLOPT_SSL_VERIFYPEER => 1,
185
			CURLOPT_SSL_VERIFYHOST => 2,
186
			CURLOPT_FORBID_REUSE   => true,
187
			CURLOPT_CONNECTTIMEOUT => 30,
188
			CURLOPT_HTTPHEADER     => [
189
				'User-Agent: PHP-IPN-Verification-Script',
190
				'Connection: Close',
191
			],
192
		]);
193
194
		if ($this->ppde_ipn_log->is_use_log_error())
195
		{
196
			curl_setopt($ch, CURLOPT_HEADER, true);
197
			curl_setopt($ch, CURLINFO_HEADER_OUT, true);
198
		}
199
200
		return $ch;
201
	}
202
203
	/**
204
	 * Updates the response status and logs any cURL errors.
205
	 *
206
	 * @param mixed    $response The response received from the API call.
207
	 * @param resource $ch       The cURL handle used to make the API call.
208
	 * @return void
209
	 */
210
	private function valuate_response($response, $ch)
211
	{
212
		$this->report_response = $response;
213
		if (curl_errno($ch) != 0)
214
		{
215
			$this->log_curl_error($ch);
216
		}
217
		else
218
		{
219
			$this->response_status = curl_getinfo($ch)['http_code'];
220
		}
221
	}
222
223
	/**
224
	 * Log the error message from a cURL request.
225
	 *
226
	 * @param resource $ch The cURL handle.
227
	 * @return void
228
	 */
229
	private function log_curl_error($ch): void
230
	{
231
		$this->ppde_ipn_log->log_error(
232
			$this->language->lang('CURL_ERROR', curl_errno($ch) . ' (' . curl_error($ch) . ')'),
233
			$this->ppde_ipn_log->is_use_log_error()
234
		);
235
	}
236
237
	/**
238
	 * Parses the cURL response and separates the response headers from the payload.
239
	 *
240
	 * This method splits the response by the double line-break "\r\n\r\n". It then trims the response headers and
241
	 * stores the trimmed payload as the new response.
242
	 *
243
	 * @access private
244
	 * @return void
245
	 */
246
	private function parse_curl_response(): void
247
	{
248
		// Split response headers and payload, a better way for strcmp
249
		$tokens = explode("\r\n\r\n", trim($this->report_response));
250
		$this->response = trim(end($tokens));
251
	}
252
253
	/**
254
	 * Set property 'curl_fsock' to use cURL based on config settings.
255
	 * If cURL is not available we use default value of the property 'curl_fsock'.
256
	 *
257
	 * @return bool
258
	 * @access public
259
	 */
260
	public function is_remote_detected(): bool
261
	{
262
		if ($this->config['ppde_curl_detected'])
263
		{
264
			$this->curl_fsock = ['curl' => true, 'none' => false];
265
		}
266
267
		return array_search(true, $this->curl_fsock);
268
	}
269
270
	/**
271
	 * Set the property '$u_paypal'
272
	 *
273
	 * @param string $u_paypal
274
	 *
275
	 * @return void
276
	 * @access public
277
	 */
278
	public function set_u_paypal($u_paypal): void
279
	{
280
		$this->u_paypal = (string) $u_paypal;
281
	}
282
283
	/**
284
	 * Get the property '$u_paypal'
285
	 *
286
	 * @return string
287
	 * @access public
288
	 */
289
	public function get_u_paypal(): string
290
	{
291
		return $this->u_paypal;
292
	}
293
294
	/**
295
	 * Get the service that will be used to contact PayPal
296
	 * Returns the name of the key that is set to true.
297
	 *
298
	 * @return string
299
	 * @access public
300
	 */
301
	public function get_remote_used(): string
302
	{
303
		return array_search(true, $this->curl_fsock);
304
	}
305
306
	/**
307
	 * Full PayPal response for include in text report
308
	 *
309
	 * @return string
310
	 * @access public
311
	 */
312
	public function get_report_response(): string
313
	{
314
		return $this->report_response;
315
	}
316
317
	/**
318
	 * PayPal response status
319
	 *
320
	 * @return string
321
	 * @access public
322
	 */
323
	public function get_response_status(): string
324
	{
325
		return $this->response_status;
326
	}
327
328
	/**
329
	 * Check if the response status is equal to "200".
330
	 *
331
	 * @return bool
332
	 * @access public
333
	 */
334
	public function check_response_status(): bool
335
	{
336
		return $this->response_status != 200;
337
	}
338
339
	/**
340
	 * If cURL is available we use strcmp() to get the Pay
341
	 *
342
	 * @param string $arg
343
	 *
344
	 * @return bool
345
	 * @access public
346
	 */
347
	public function is_curl_strcmp($arg): bool
348
	{
349
		return $this->curl_fsock['curl'] && (strcmp($this->response, $arg) === 0);
350
	}
351
352
	/**
353
	 * Check TLS configuration.
354
	 *
355
	 * This method checks the TLS configuration using a CURL request to a specified TLS host.
356
	 * If the TLS version matches one of the allowed versions, it sets the 'ppde_tls_detected' config value to true.
357
	 * Otherwise, it sets it to false.
358
	 *
359
	 * @return void
360
	 * @access public
361
	 */
362
	public function check_tls(): void
363
	{
364
		$ext_meta = $this->ppde_ext_manager->get_ext_meta();
365
366
		// Reset settings to false
367
		$this->config->set('ppde_tls_detected', false);
0 ignored issues
show
Bug introduced by
false of type false is incompatible with the type string expected by parameter $value of phpbb\config\config::set(). ( Ignorable by Annotation )

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

367
		$this->config->set('ppde_tls_detected', /** @scrutinizer ignore-type */ false);
Loading history...
368
		$this->response = '';
369
370
		$this->check_curl($ext_meta['extra']['security-check']['tls']['tls-host']);
371
372
		// Analyse response
373
		$json = json_decode($this->response);
0 ignored issues
show
Bug introduced by
It seems like $this->response can also be of type true; however, parameter $json of json_decode() does only seem to accept string, 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

373
		$json = json_decode(/** @scrutinizer ignore-type */ $this->response);
Loading history...
374
375
		if ($json !== null && in_array($json->tls_version, $ext_meta['extra']['security-check']['tls']['tls-version'], true))
376
		{
377
			$this->config->set('ppde_tls_detected', true);
0 ignored issues
show
Bug introduced by
true of type true is incompatible with the type string expected by parameter $value of phpbb\config\config::set(). ( Ignorable by Annotation )

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

377
			$this->config->set('ppde_tls_detected', /** @scrutinizer ignore-type */ true);
Loading history...
378
		}
379
	}
380
381
	/**
382
	 * Set the remote detected value.
383
	 *
384
	 * This method retrieves the extension metadata using the PPDE Extension Manager
385
	 * and checks if curl is detected on the remote server. It then sets the value
386
	 * of 'ppde_curl_detected' in the configuration based on the check result.
387
	 *
388
	 * @return void
389
	 * @access public
390
	 */
391
	public function set_remote_detected(): void
392
	{
393
		$ext_meta = $this->ppde_ext_manager->get_ext_meta();
394
		$this->config->set('ppde_curl_detected', $this->check_curl($ext_meta['extra']['version-check']['host']));
395
	}
396
397
	/**
398
	 * Check if cURL is available
399
	 *
400
	 * @param string $host
401
	 *
402
	 * @return bool
403
	 * @access public
404
	 */
405
	public function check_curl($host): bool
406
	{
407
		if ($this->is_curl_available())
408
		{
409
			return $this->execute_curl_request($host);
410
		}
411
412
		return false;
413
	}
414
415
	/**
416
	 * Check if cURL is available.
417
	 *
418
	 * @return bool Returns true if cURL is available, false otherwise.
419
	 */
420
	private function is_curl_available(): bool
421
	{
422
		return extension_loaded('curl') && function_exists('curl_init');
423
	}
424
425
	/**
426
	 * Execute a cURL request.
427
	 *
428
	 * @param string $host The host to send the cURL request to.
429
	 *
430
	 * @return bool Returns true if the cURL request is successful or false otherwise.
431
	 */
432
	private function execute_curl_request(string $host): bool
433
	{
434
		$ch = curl_init($host);
435
436
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
437
438
		$this->response = curl_exec($ch);
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_exec($ch) can also be of type true. However, the property $response is declared as type string. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
439
		$this->response_status = (string) curl_getinfo($ch, CURLINFO_HTTP_CODE);
440
441
		curl_close($ch);
442
443
		return $this->response !== false || $this->response_status !== '0';
444
	}
445
446
	/**
447
	 * Set config value for cURL version
448
	 *
449
	 * @return void
450
	 * @access public
451
	 */
452
	public function set_curl_info(): void
453
	{
454
		// Get cURL version informations
455
		if ($curl_info = $this->check_curl_info())
456
		{
457
			$this->config->set('ppde_curl_version', $curl_info['version']);
458
			$this->config->set('ppde_curl_ssl_version', $curl_info['ssl_version']);
459
		}
460
	}
461
462
	/**
463
	 * Get cURL version if available
464
	 *
465
	 * @return array|bool
466
	 * @access public
467
	 */
468
	public function check_curl_info()
469
	{
470
		if (function_exists('curl_version'))
471
		{
472
			return curl_version();
473
		}
474
475
		return false;
476
	}
477
478
	/**
479
	 * Get all args and build the return URI
480
	 *
481
	 * @return void
482
	 * @access public
483
	 */
484
	public function set_args_return_uri(): void
485
	{
486
		// Add the cmd=_notify-validate for PayPal
487
		$this->args_return_uri = 'cmd=_notify-validate';
488
489
		// Grab the post data form and set in an array to be used in the uri to PayPal
490
		$postback_args = $this->get_postback_args();
491
		$query_strings = http_build_query($postback_args);
492
493
		// Append the uri with the query strings
494
		$this->args_return_uri .= '&' . $query_strings;
495
	}
496
497
	/**
498
	 * Sets the postback arguments for the current object.
499
	 *
500
	 * This is used to Postback args to PayPal or for tracking errors.
501
	 * Based on official PayPal IPN class.
502
	 * Ref. https://github.com/paypal/ipn-code-samples/blob/master/php/PaypalIPN.php#L67-L81
503
	 *
504
	 * @return void
505
	 * @access public
506
	 */
507
	public function set_postback_args(): void
508
	{
509
		$postback_args = $this->get_decoded_input_params();
510
511
		foreach ($postback_args as $key => $value)
512
		{
513
			if ($this->is_payment_date_and_has_plus($key, $value))
514
			{
515
				$postback_args[$key] = str_replace('+', '%2B', $value);
516
			}
517
		}
518
		$this->postback_args = $postback_args;
519
	}
520
521
	/**
522
	 * Retrieves the decoded input parameters.
523
	 *
524
	 * @return array The input parameters after being decoded.
525
	 * @access private
526
	 */
527
	private function get_decoded_input_params(): array
528
	{
529
		parse_str(file_get_contents('php://input'), $params);
530
		return array_map('urldecode', $params);
531
	}
532
533
	/**
534
	 * Check if the given key is 'payment_date' and the value contains a '+'.
535
	 *
536
	 * @param string $key   The key to check.
537
	 * @param string $value The value to check.
538
	 *
539
	 * @return bool Returns true if the key is 'payment_date' and the value contains a '+', otherwise returns false.
540
	 * @access private
541
	 */
542
	private function is_payment_date_and_has_plus(string $key, string $value): bool
543
	{
544
		return $key === 'payment_date' && strpos($value, '+') !== false;
545
	}
546
547
	/**
548
	 * Retrieves the postback arguments.
549
	 *
550
	 * @return array The postback arguments.
551
	 * @access public
552
	 */
553
	public function get_postback_args(): array
554
	{
555
		return $this->postback_args;
556
	}
557
}
558