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); |
|
|
|
|
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)); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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); |
|
|
|
|
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
|
|
|
|
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 theid
property of an instance of theAccount
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.