Passed
Pull Request — 3.3.x (#95)
by Mario
04:11
created

ipn_paypal::set_postback_args()   A

Complexity

Conditions 5
Paths 5

Size

Total Lines 19
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 9
c 1
b 0
f 0
nc 5
nop 0
dl 0
loc 19
rs 9.6111
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
		}
127
		else
128
		{
129
			$this->ppde_ipn_log->log_error($this->language->lang('NO_CONNECTION_DETECTED'), $this->ppde_ipn_log->is_use_log_error(), true, E_USER_ERROR, $data);
130
		}
131
	}
132
133
	/**
134
	 * Post Back Using cURL
135
	 *
136
	 * Sends the post back to PayPal using the cURL library. Called by
137
	 * the validate_transaction() method if the curl_fsock['curl'] property is true.
138
	 * Throws an exception if the post fails. Populates the response and response_status properties on success.
139
	 *
140
	 * @param string $encoded_data The post data as a URL encoded string
141
	 *
142
	 * @return void
143
	 * @access private
144
	 */
145
	private function curl_post($encoded_data): void
146
	{
147
		$ch = curl_init($this->u_paypal);
148
		curl_setopt($ch, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
149
		curl_setopt($ch, CURLOPT_POST, true);
150
		curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
151
		curl_setopt($ch, CURLOPT_POSTFIELDS, $encoded_data);
152
		curl_setopt($ch, CURLOPT_SSLVERSION, 6);
153
		curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, 1);
154
		curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);
155
		curl_setopt($ch, CURLOPT_FORBID_REUSE, true);
156
		curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 30);
157
		curl_setopt($ch, CURLOPT_HTTPHEADER, [
158
			'User-Agent: PHP-IPN-Verification-Script',
159
			'Connection: Close',
160
		]);
161
162
		if ($this->ppde_ipn_log->is_use_log_error())
163
		{
164
			curl_setopt($ch, CURLOPT_HEADER, true);
165
			curl_setopt($ch, CURLINFO_HEADER_OUT, true);
166
		}
167
168
		$this->report_response = $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...
Documentation Bug introduced by
It seems like $this->response = curl_exec($ch) can also be of type true. However, the property $report_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...
169
		if (curl_errno($ch) != 0)
170
		{
171
			// cURL error
172
			$this->ppde_ipn_log->log_error($this->language->lang('CURL_ERROR', curl_errno($ch) . ' (' . curl_error($ch) . ')'), $this->ppde_ipn_log->is_use_log_error());
173
		}
174
		else
175
		{
176
			$info = curl_getinfo($ch);
177
			$this->response_status = $info['http_code'];
178
		}
179
		curl_close($ch);
180
181
		// Split response headers and payload, a better way for strcmp
182
		$tokens = explode("\r\n\r\n", trim($this->response));
0 ignored issues
show
Bug introduced by
It seems like $this->response can also be of type true; however, parameter $string of trim() 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

182
		$tokens = explode("\r\n\r\n", trim(/** @scrutinizer ignore-type */ $this->response));
Loading history...
183
		$this->response = trim(end($tokens));
184
	}
185
186
	/**
187
	 * Set property 'curl_fsock' to use cURL based on config settings.
188
	 * If cURL is not available we use default value of the property 'curl_fsock'.
189
	 *
190
	 * @return bool
191
	 * @access public
192
	 */
193
	public function is_remote_detected(): bool
194
	{
195
		if ($this->config['ppde_curl_detected'])
196
		{
197
			$this->curl_fsock = ['curl' => true, 'none' => false];
198
		}
199
200
		return array_search(true, $this->curl_fsock);
201
	}
202
203
	/**
204
	 * Set the property '$u_paypal'
205
	 *
206
	 * @param string $u_paypal
207
	 *
208
	 * @return void
209
	 * @access public
210
	 */
211
	public function set_u_paypal($u_paypal): void
212
	{
213
		$this->u_paypal = (string) $u_paypal;
214
	}
215
216
	/**
217
	 * Get the property '$u_paypal'
218
	 *
219
	 * @return string
220
	 * @access public
221
	 */
222
	public function get_u_paypal(): string
223
	{
224
		return $this->u_paypal;
225
	}
226
227
	/**
228
	 * Get the service that will be used to contact PayPal
229
	 * Returns the name of the key that is set to true.
230
	 *
231
	 * @return string
232
	 * @access public
233
	 */
234
	public function get_remote_used(): string
235
	{
236
		return array_search(true, $this->curl_fsock);
237
	}
238
239
	/**
240
	 * Full PayPal response for include in text report
241
	 *
242
	 * @return string
243
	 * @access public
244
	 */
245
	public function get_report_response(): string
246
	{
247
		return $this->report_response;
248
	}
249
250
	/**
251
	 * PayPal response status
252
	 *
253
	 * @return string
254
	 * @access public
255
	 */
256
	public function get_response_status(): string
257
	{
258
		return $this->response_status;
259
	}
260
261
	/**
262
	 * Check if the response status is equal to "200".
263
	 *
264
	 * @return bool
265
	 * @access public
266
	 */
267
	public function check_response_status(): bool
268
	{
269
		return $this->response_status != 200;
270
	}
271
272
	/**
273
	 * If cURL is available we use strcmp() to get the Pay
274
	 *
275
	 * @param string $arg
276
	 *
277
	 * @return bool
278
	 * @access public
279
	 */
280
	public function is_curl_strcmp($arg): bool
281
	{
282
		return $this->curl_fsock['curl'] && (strcmp($this->response, $arg) === 0);
283
	}
284
285
	/**
286
	 * Check if website use TLS 1.2
287
	 *
288
	 * @return void
289
	 * @access public
290
	 */
291
	public function check_tls(): void
292
	{
293
		$ext_meta = $this->ppde_ext_manager->get_ext_meta();
294
295
		// Reset settings to false
296
		$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

296
		$this->config->set('ppde_tls_detected', /** @scrutinizer ignore-type */ false);
Loading history...
297
		$this->response = '';
298
299
		$this->check_curl($ext_meta['extra']['security-check']['tls']['tls-host']);
300
301
		// Analyse response
302
		$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

302
		$json = json_decode(/** @scrutinizer ignore-type */ $this->response);
Loading history...
303
304
		if ($json !== null && in_array($json->tls_version, $ext_meta['extra']['security-check']['tls']['tls-version']))
305
		{
306
			$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

306
			$this->config->set('ppde_tls_detected', /** @scrutinizer ignore-type */ true);
Loading history...
307
		}
308
	}
309
310
	/**
311
	 * Set config value for cURL
312
	 *
313
	 * @return void
314
	 * @access public
315
	 */
316
	public function set_remote_detected(): void
317
	{
318
		$ext_meta = $this->ppde_ext_manager->get_ext_meta();
319
320
		$this->config->set('ppde_curl_detected', $this->check_curl($ext_meta['extra']['version-check']['host']));
321
	}
322
323
	/**
324
	 * Check if cURL is available
325
	 *
326
	 * @param string $host
327
	 *
328
	 * @return bool
329
	 * @access public
330
	 */
331
	public function check_curl($host): bool
332
	{
333
		if (function_exists('curl_init') && function_exists('curl_exec'))
334
		{
335
			$ch = curl_init($host);
336
337
			curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
338
339
			$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...
340
			$this->response_status = (string) curl_getinfo($ch, CURLINFO_HTTP_CODE);
341
342
			curl_close($ch);
343
344
			return $this->response !== false || $this->response_status !== '0';
345
		}
346
347
		return false;
348
	}
349
350
	/**
351
	 * Set config value for cURL version
352
	 *
353
	 * @return void
354
	 * @access public
355
	 */
356
	public function set_curl_info(): void
357
	{
358
		// Get cURL version informations
359
		if ($curl_info = $this->check_curl_info())
360
		{
361
			$this->config->set('ppde_curl_version', $curl_info['version']);
362
			$this->config->set('ppde_curl_ssl_version', $curl_info['ssl_version']);
363
		}
364
	}
365
366
	/**
367
	 * Get cURL version if available
368
	 *
369
	 * @return array|bool
370
	 * @access public
371
	 */
372
	public function check_curl_info()
373
	{
374
		if (function_exists('curl_version'))
375
		{
376
			return curl_version();
377
		}
378
379
		return false;
380
	}
381
382
	/**
383
	 * Get all args and build the return URI
384
	 *
385
	 * @return void
386
	 * @access public
387
	 */
388
	public function set_args_return_uri(): void
389
	{
390
		$values = [];
391
		// Add the cmd=_notify-validate for PayPal
392
		$this->args_return_uri = 'cmd=_notify-validate';
393
394
		// Grab the post data form and set in an array to be used in the URI to PayPal
395
		foreach ($this->get_postback_args() as $key => $value)
396
		{
397
			$values[] = $key . '=' . urlencode($value);
398
		}
399
400
		// Implode the array into a string URI
401
		$this->args_return_uri .= '&' . implode('&', $values);
402
	}
403
404
	/**
405
	 * Get $_POST content as is. This is used to Postback args to PayPal or for tracking errors.
406
	 * Based on official PayPal IPN class (https://github.com/paypal/ipn-code-samples/blob/master/php/PaypalIPN.php)
407
	 *
408
	 * @return void
409
	 * @access public
410
	 */
411
	public function set_postback_args(): void
412
	{
413
		$raw_post_data = file_get_contents('php://input');
414
		$raw_post_array = explode('&', $raw_post_data);
415
416
		foreach ($raw_post_array as $keyval)
417
		{
418
			$keyval = explode('=', $keyval);
419
			if (count($keyval) === 2)
420
			{
421
				// Since we do not want the plus in the datetime string to be encoded to a space, we manually encode it.
422
				if ($keyval[0] === 'payment_date')
423
				{
424
					if (substr_count($keyval[1], '+') === 1)
425
					{
426
						$keyval[1] = str_replace('+', '%2B', $keyval[1]);
427
					}
428
				}
429
				$this->postback_args[$keyval[0]] = urldecode($keyval[1]);
430
			}
431
		}
432
	}
433
434
	/**
435
	 * @return array
436
	 */
437
	public function get_postback_args(): array
438
	{
439
		return $this->postback_args;
440
	}
441
}
442