Passed
Push — develop-3.3.x ( eebe3b...d48415 )
by Mario
02:53
created

ipn_paypal   A

Complexity

Total Complexity 31

Size/Duplication

Total Lines 406
Duplicated Lines 0 %

Importance

Changes 10
Bugs 0 Features 0
Metric Value
eloc 88
c 10
b 0
f 0
dl 0
loc 406
rs 9.92
wmc 31

22 Methods

Rating   Name   Duplication   Size   Complexity  
A get_remote_uri() 0 3 1
A initiate_paypal_connection() 0 9 2
A set_u_paypal() 0 3 1
A is_curl_strcmp() 0 3 2
A get_u_paypal() 0 3 1
A set_postback_args() 0 12 3
A set_args_return_uri() 0 11 1
A get_postback_args() 0 3 1
A is_payment_date_and_has_plus() 0 3 2
A get_remote_used() 0 3 1
A check_response_status() 0 3 1
A __construct() 0 11 1
A init_curl_session() 0 27 2
A get_report_response() 0 3 1
A is_remote_detected() 0 8 2
A log_paypal_connection_error() 0 8 1
A get_decoded_input_params() 0 4 1
A valuate_response() 0 10 2
A parse_curl_response() 0 5 1
A log_curl_error() 0 5 1
A curl_post() 0 9 2
A get_response_status() 0 3 1
1
<?php
2
/**
3
 *
4
 * PayPal Donation extension for the phpBB Forum Software package.
5
 *
6
 * @copyright (c) 2015-2024 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_ipn_log;
42
	protected $request;
43
	/**
44
	 * @var array
45
	 */
46
	private $curl_fsock = ['curl' => false, 'none' => true];
47
	/**
48
	 * @var array
49
	 */
50
	private $postback_args = [];
51
	/**
52
	 * Full PayPal response for include in text report
53
	 *
54
	 * @var string
55
	 */
56
	private $report_response = '';
57
	/**
58
	 * PayPal response (VERIFIED or INVALID)
59
	 *
60
	 * @var string
61
	 */
62
	private $response = '';
63
	/**
64
	 * PayPal response status (code 200 or other)
65
	 *
66
	 * @var string
67
	 */
68
	private $response_status = '';
69
	/**
70
	 * PayPal URL
71
	 * Could be Sandbox URL ou normal PayPal URL.
72
	 *
73
	 * @var string
74
	 */
75
	private $u_paypal = '';
76
77
	/**
78
	 * Constructor
79
	 *
80
	 * @param config            $config           Config object
81
	 * @param language          $language         Language user object
82
	 * @param ipn_log           $ppde_ipn_log     IPN log
83
	 * @param request           $request          Request object
84
	 *
85
	 * @access public
86
	 */
87
	public function __construct(
88
		config $config,
89
		language $language,
90
		ipn_log $ppde_ipn_log,
91
		request $request
92
	)
93
	{
94
		$this->config = $config;
95
		$this->language = $language;
96
		$this->ppde_ipn_log = $ppde_ipn_log;
97
		$this->request = $request;
98
	}
99
100
	/**
101
	 * @return array
102
	 */
103
	public static function get_remote_uri(): array
104
	{
105
		return self::$remote_uri;
106
	}
107
108
	/**
109
	 * Initiate communication with PayPal.
110
	 * We use cURL. If it is not available we log an error.
111
	 *
112
	 * @param array $data
113
	 *
114
	 * @return void
115
	 * @access public
116
	 */
117
	public function initiate_paypal_connection($data): void
118
	{
119
		if ($this->curl_fsock['curl'])
120
		{
121
			$this->curl_post($this->args_return_uri);
122
			return;
123
		}
124
125
		$this->log_paypal_connection_error($data);
126
	}
127
128
	private function log_paypal_connection_error($data): void
129
	{
130
		$this->ppde_ipn_log->log_error(
131
			$this->language->lang('NO_CONNECTION_DETECTED'),
132
			$this->ppde_ipn_log->is_use_log_error(),
133
			true,
134
			E_USER_ERROR,
135
			$data
136
		);
137
	}
138
139
	/**
140
	 * Post Back Using cURL
141
	 *
142
	 * Sends the post back to PayPal using the cURL library. Called by
143
	 * the validate_transaction() method if the curl_fsock['curl'] property is true.
144
	 * Throws an exception if the post fails. Populates the response and response_status properties on success.
145
	 *
146
	 * @param string $encoded_data The post data as a URL encoded string
147
	 *
148
	 * @return void
149
	 * @access private
150
	 */
151
	private function curl_post($encoded_data): void
152
	{
153
		$ch = $this->init_curl_session($encoded_data);
154
		$this->valuate_response(curl_exec($ch), $ch);
155
		if ($this->ppde_ipn_log->is_use_log_error())
156
		{
157
			$this->parse_curl_response();
158
		}
159
		curl_close($ch);
160
	}
161
162
	/**
163
	 * Initializes a cURL session with the specified encoded data.
164
	 *
165
	 * @param string $encoded_data The encoded data to be sent in the cURL request.
166
	 *
167
	 * @return resource Returns a cURL session handle on success, false on failure.
168
	 * @access private
169
	 */
170
	private function init_curl_session($encoded_data)
171
	{
172
		$ch = curl_init($this->u_paypal);
173
174
		curl_setopt_array($ch, [
175
			CURLOPT_HTTP_VERSION   => CURL_HTTP_VERSION_1_1,
176
			CURLOPT_POST           => true,
177
			CURLOPT_RETURNTRANSFER => true,
178
			CURLOPT_POSTFIELDS     => $encoded_data,
179
			CURLOPT_SSLVERSION     => 6,
180
			CURLOPT_SSL_VERIFYPEER => 1,
181
			CURLOPT_SSL_VERIFYHOST => 2,
182
			CURLOPT_FORBID_REUSE   => true,
183
			CURLOPT_CONNECTTIMEOUT => 30,
184
			CURLOPT_HTTPHEADER     => [
185
				'User-Agent: PHP-IPN-Verification-Script',
186
				'Connection: Close',
187
			],
188
		]);
189
190
		if ($this->ppde_ipn_log->is_use_log_error())
191
		{
192
			curl_setopt($ch, CURLOPT_HEADER, true);
193
			curl_setopt($ch, CURLINFO_HEADER_OUT, true);
194
		}
195
196
		return $ch;
197
	}
198
199
	/**
200
	 * Updates the response status and logs any cURL errors.
201
	 *
202
	 * @param mixed    $response The response received from the API call.
203
	 * @param resource $ch       The cURL handle used to make the API call.
204
	 * @return void
205
	 */
206
	private function valuate_response($response, $ch): void
207
	{
208
		$this->report_response = $response;
209
		if (curl_errno($ch) != 0)
210
		{
211
			$this->log_curl_error($ch);
212
		}
213
		else
214
		{
215
			$this->response_status = curl_getinfo($ch)['http_code'];
216
		}
217
	}
218
219
	/**
220
	 * Log the error message from a cURL request.
221
	 *
222
	 * @param resource $ch The cURL handle.
223
	 * @return void
224
	 */
225
	private function log_curl_error($ch): void
226
	{
227
		$this->ppde_ipn_log->log_error(
228
			$this->language->lang('CURL_ERROR', curl_errno($ch) . ' (' . curl_error($ch) . ')'),
229
			$this->ppde_ipn_log->is_use_log_error()
230
		);
231
	}
232
233
	/**
234
	 * Parses the cURL response and separates the response headers from the payload.
235
	 *
236
	 * This method splits the response by the double line-break "\r\n\r\n". It then trims the response headers and
237
	 * stores the trimmed payload as the new response.
238
	 *
239
	 * @access private
240
	 * @return void
241
	 */
242
	private function parse_curl_response(): void
243
	{
244
		// Split response headers and payload, a better way for strcmp
245
		$tokens = explode("\r\n\r\n", trim($this->report_response));
246
		$this->response = trim(end($tokens));
247
	}
248
249
	/**
250
	 * Set property 'curl_fsock' to use cURL based on config settings.
251
	 * If cURL is not available we use default value of the property 'curl_fsock'.
252
	 *
253
	 * @return bool
254
	 * @access public
255
	 */
256
	public function is_remote_detected(): bool
257
	{
258
		if ($this->config['ppde_curl_detected'])
259
		{
260
			$this->curl_fsock = ['curl' => true, 'none' => false];
261
		}
262
263
		return array_search(true, $this->curl_fsock);
264
	}
265
266
	/**
267
	 * Set the property '$u_paypal'
268
	 *
269
	 * @param string $u_paypal
270
	 *
271
	 * @return void
272
	 * @access public
273
	 */
274
	public function set_u_paypal($u_paypal): void
275
	{
276
		$this->u_paypal = (string) $u_paypal;
277
	}
278
279
	/**
280
	 * Get the property '$u_paypal'
281
	 *
282
	 * @return string
283
	 * @access public
284
	 */
285
	public function get_u_paypal(): string
286
	{
287
		return $this->u_paypal;
288
	}
289
290
	/**
291
	 * Get the service that will be used to contact PayPal
292
	 * Returns the name of the key that is set to true.
293
	 *
294
	 * @return string
295
	 * @access public
296
	 */
297
	public function get_remote_used(): string
298
	{
299
		return array_search(true, $this->curl_fsock);
300
	}
301
302
	/**
303
	 * Full PayPal response for include in text report
304
	 *
305
	 * @return string
306
	 * @access public
307
	 */
308
	public function get_report_response(): string
309
	{
310
		return $this->report_response;
311
	}
312
313
	/**
314
	 * PayPal response status
315
	 *
316
	 * @return string
317
	 * @access public
318
	 */
319
	public function get_response_status(): string
320
	{
321
		return $this->response_status;
322
	}
323
324
	/**
325
	 * Check if the response status is equal to "200".
326
	 *
327
	 * @return bool
328
	 * @access public
329
	 */
330
	public function check_response_status(): bool
331
	{
332
		return $this->response_status != 200;
333
	}
334
335
	/**
336
	 * If cURL is available we use strcmp() to get the PayPal response
337
	 *
338
	 * @param string $arg
339
	 *
340
	 * @return bool
341
	 * @access public
342
	 */
343
	public function is_curl_strcmp($arg): bool
344
	{
345
		return $this->curl_fsock['curl'] && (strcmp($this->response, $arg) === 0);
346
	}
347
348
	/**
349
	 * Get all args and build the return URI
350
	 *
351
	 * @return void
352
	 * @access public
353
	 */
354
	public function set_args_return_uri(): void
355
	{
356
		// Add the cmd=_notify-validate for PayPal
357
		$this->args_return_uri = 'cmd=_notify-validate';
358
359
		// Grab the post data form and set in an array to be used in the uri to PayPal
360
		$postback_args = $this->get_postback_args();
361
		$query_strings = http_build_query($postback_args);
362
363
		// Append the uri with the query strings
364
		$this->args_return_uri .= '&' . $query_strings;
365
	}
366
367
	/**
368
	 * Sets the postback arguments for the current object.
369
	 *
370
	 * This is used to Postback args to PayPal or for tracking errors.
371
	 * Based on official PayPal IPN class.
372
	 * Ref. https://github.com/paypal/ipn-code-samples/blob/master/php/PaypalIPN.php#L67-L81
373
	 *
374
	 * @return void
375
	 * @access public
376
	 */
377
	public function set_postback_args(): void
378
	{
379
		$postback_args = $this->get_decoded_input_params();
380
381
		foreach ($postback_args as $key => $value)
382
		{
383
			if ($this->is_payment_date_and_has_plus($key, $value))
384
			{
385
				$postback_args[$key] = str_replace('+', '%2B', $value);
386
			}
387
		}
388
		$this->postback_args = $postback_args;
389
	}
390
391
	/**
392
	 * Retrieves the decoded input parameters.
393
	 *
394
	 * @return array The input parameters after being decoded.
395
	 * @access private
396
	 */
397
	private function get_decoded_input_params(): array
398
	{
399
		parse_str(file_get_contents('php://input'), $params);
400
		return array_map('urldecode', $params);
401
	}
402
403
	/**
404
	 * Check if the given key is 'payment_date' and the value contains a '+'.
405
	 *
406
	 * @param string $key   The key to check.
407
	 * @param string $value The value to check.
408
	 *
409
	 * @return bool Returns true if the key is 'payment_date' and the value contains a '+', otherwise returns false.
410
	 * @access private
411
	 */
412
	private function is_payment_date_and_has_plus(string $key, string $value): bool
413
	{
414
		return $key === 'payment_date' && strpos($value, '+') !== false;
415
	}
416
417
	/**
418
	 * Retrieves the postback arguments.
419
	 *
420
	 * @return array The postback arguments.
421
	 * @access public
422
	 */
423
	public function get_postback_args(): array
424
	{
425
		return $this->postback_args;
426
	}
427
}
428