ipn_paypal   A
last analyzed

Complexity

Total Complexity 31

Size/Duplication

Total Lines 388
Duplicated Lines 0 %

Importance

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

22 Methods

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