1 | <?php |
||
2 | |||
3 | /** |
||
4 | * Simple Machines Forum (SMF) |
||
5 | * |
||
6 | * @package SMF |
||
7 | * @author Simple Machines https://www.simplemachines.org |
||
8 | * @copyright 2022 Simple Machines and individual contributors |
||
9 | * @license https://www.simplemachines.org/about/smf/license.php BSD |
||
10 | * |
||
11 | * @version 2.1.0 |
||
12 | */ |
||
13 | |||
14 | // This won't be dedicated without this - this must exist in each gateway! |
||
15 | // SMF Payment Gateway: paypal |
||
16 | |||
17 | if (!defined('SMF')) |
||
18 | die('No direct access...'); |
||
19 | |||
20 | /** |
||
21 | * Class for returning available form data for this gateway |
||
22 | */ |
||
23 | class paypal_display |
||
24 | { |
||
25 | /** |
||
26 | * @var string Name of this payment gateway |
||
27 | */ |
||
28 | public $title = 'PayPal'; |
||
29 | |||
30 | /** |
||
31 | * Return the admin settings for this gateway |
||
32 | * |
||
33 | * @return array An array of settings data |
||
34 | */ |
||
35 | public function getGatewaySettings() |
||
36 | { |
||
37 | global $txt; |
||
38 | |||
39 | $setting_data = array( |
||
40 | array( |
||
41 | 'email', 'paypal_email', |
||
42 | 'subtext' => $txt['paypal_email_desc'], |
||
43 | 'size' => 60 |
||
44 | ), |
||
45 | array( |
||
46 | 'email', 'paypal_additional_emails', |
||
47 | 'subtext' => $txt['paypal_additional_emails_desc'], |
||
48 | 'size' => 60 |
||
49 | ), |
||
50 | array( |
||
51 | 'email', 'paypal_sandbox_email', |
||
52 | 'subtext' => $txt['paypal_sandbox_email_desc'], |
||
53 | 'size' => 60 |
||
54 | ), |
||
55 | ); |
||
56 | |||
57 | return $setting_data; |
||
58 | } |
||
59 | |||
60 | /** |
||
61 | * Is this enabled for new payments? |
||
62 | * |
||
63 | * @return boolean Whether this gateway is enabled (for PayPal, whether the PayPal email is set) |
||
64 | */ |
||
65 | public function gatewayEnabled() |
||
66 | { |
||
67 | global $modSettings; |
||
68 | |||
69 | return !empty($modSettings['paypal_email']); |
||
70 | } |
||
71 | |||
72 | /** |
||
73 | * What do we want? |
||
74 | * |
||
75 | * Called from Profile-Actions.php to return a unique set of fields for the given gateway |
||
76 | * plus all the standard ones for the subscription form |
||
77 | * |
||
78 | * @param string $unique_id The unique ID of this gateway |
||
79 | * @param array $sub_data Subscription data |
||
80 | * @param int|float $value The amount of the subscription |
||
81 | * @param string $period |
||
82 | * @param string $return_url The URL to return the user to after processing the payment |
||
83 | * @return array An array of data for the form |
||
84 | */ |
||
85 | public function fetchGatewayFields($unique_id, $sub_data, $value, $period, $return_url) |
||
86 | { |
||
87 | global $modSettings, $txt, $boardurl; |
||
88 | |||
89 | $return_data = array( |
||
90 | 'form' => 'https://www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com/cgi-bin/webscr', |
||
91 | 'id' => 'paypal', |
||
92 | 'hidden' => array(), |
||
93 | 'title' => $txt['paypal'], |
||
94 | 'desc' => $txt['paid_confirm_paypal'], |
||
95 | 'submit' => $txt['paid_paypal_order'], |
||
96 | 'javascript' => '', |
||
97 | ); |
||
98 | |||
99 | // All the standard bits. |
||
100 | $return_data['hidden']['business'] = $modSettings['paypal_email']; |
||
101 | $return_data['hidden']['item_name'] = $sub_data['name'] . ' ' . $txt['subscription']; |
||
102 | $return_data['hidden']['item_number'] = $unique_id; |
||
103 | $return_data['hidden']['currency_code'] = strtoupper($modSettings['paid_currency_code']); |
||
104 | $return_data['hidden']['no_shipping'] = 1; |
||
105 | $return_data['hidden']['no_note'] = 1; |
||
106 | $return_data['hidden']['amount'] = $value; |
||
107 | $return_data['hidden']['cmd'] = !$sub_data['repeatable'] ? '_xclick' : '_xclick-subscriptions'; |
||
108 | $return_data['hidden']['return'] = $return_url; |
||
109 | $return_data['hidden']['a3'] = $value; |
||
110 | $return_data['hidden']['src'] = 1; |
||
111 | $return_data['hidden']['notify_url'] = $boardurl . '/subscriptions.php'; |
||
112 | |||
113 | // If possible let's use the language we know we need. |
||
114 | $return_data['hidden']['lc'] = !empty($txt['lang_paypal']) ? $txt['lang_paypal'] : 'US'; |
||
115 | |||
116 | // Now stuff dependant on what we're doing. |
||
117 | if ($sub_data['flexible']) |
||
118 | { |
||
119 | $return_data['hidden']['p3'] = 1; |
||
120 | $return_data['hidden']['t3'] = strtoupper(substr($period, 0, 1)); |
||
121 | } |
||
122 | else |
||
123 | { |
||
124 | preg_match('~(\d*)(\w)~', $sub_data['real_length'], $match); |
||
125 | $unit = $match[1]; |
||
126 | $period = $match[2]; |
||
127 | |||
128 | $return_data['hidden']['p3'] = $unit; |
||
129 | $return_data['hidden']['t3'] = $period; |
||
130 | } |
||
131 | |||
132 | // If it's repeatable do some javascript to respect this idea. |
||
133 | if (!empty($sub_data['repeatable'])) |
||
134 | $return_data['javascript'] = ' |
||
135 | document.write(\'<label for="do_paypal_recur"><input type="checkbox" name="do_paypal_recur" id="do_paypal_recur" checked onclick="switchPaypalRecur();">' . $txt['paid_make_recurring'] . '</label><br>\'); |
||
136 | |||
137 | function switchPaypalRecur() |
||
138 | { |
||
139 | document.getElementById("paypal_cmd").value = document.getElementById("do_paypal_recur").checked ? "_xclick-subscriptions" : "_xclick"; |
||
140 | }'; |
||
141 | |||
142 | return $return_data; |
||
143 | } |
||
144 | } |
||
145 | |||
146 | /** |
||
147 | * Class of functions to validate a IPN response and provide details of the payment |
||
148 | */ |
||
149 | class paypal_payment |
||
150 | { |
||
151 | /** |
||
152 | * @var string $return_data The data to return |
||
153 | */ |
||
154 | private $return_data; |
||
155 | |||
156 | /** |
||
157 | * This function returns true/false for whether this gateway thinks the data is intended for it. |
||
158 | * |
||
159 | * @return boolean Whether this gateway things the data is valid |
||
160 | */ |
||
161 | public function isValid() |
||
162 | { |
||
163 | global $modSettings; |
||
164 | |||
165 | // Has the user set up an email address? |
||
166 | if ((empty($modSettings['paidsubs_test']) && empty($modSettings['paypal_email'])) || (!empty($modSettings['paidsubs_test']) && empty($modSettings['paypal_sandbox_email']))) |
||
167 | return false; |
||
168 | // Check the correct transaction types are even here. |
||
169 | if ((!isset($_POST['txn_type']) && !isset($_POST['payment_status'])) || (!isset($_POST['business']) && !isset($_POST['receiver_email']))) |
||
170 | return false; |
||
171 | // Correct email address? |
||
172 | if (!isset($_POST['business'])) |
||
173 | $_POST['business'] = $_POST['receiver_email']; |
||
174 | |||
175 | // Are we testing? |
||
176 | if (!empty($modSettings['paidsubs_test']) && strtolower($modSettings['paypal_sandbox_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', strtolower($modSettings['paypal_additional_emails']))))) |
||
177 | return false; |
||
178 | elseif (strtolower($modSettings['paypal_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', $modSettings['paypal_additional_emails'])))) |
||
179 | return false; |
||
180 | return true; |
||
181 | } |
||
182 | |||
183 | /** |
||
184 | * Post the IPN data received back to paypal for validation |
||
185 | * Sends the complete unaltered message back to PayPal. The message must contain the same fields |
||
186 | * in the same order and be encoded in the same way as the original message |
||
187 | * PayPal will respond back with a single word, which is either VERIFIED if the message originated with PayPal or INVALID |
||
188 | * |
||
189 | * If valid returns the subscription and member IDs we are going to process if it passes |
||
190 | * |
||
191 | * @return string A string containing the subscription ID and member ID, separated by a + |
||
192 | */ |
||
193 | public function precheck() |
||
194 | { |
||
195 | global $modSettings, $txt; |
||
196 | |||
197 | // Put this to some default value. |
||
198 | if (!isset($_POST['txn_type'])) |
||
199 | $_POST['txn_type'] = ''; |
||
200 | |||
201 | // Build the request string - starting with the minimum requirement. |
||
202 | $requestString = 'cmd=_notify-validate'; |
||
203 | |||
204 | // Now my dear, add all the posted bits in the order we got them |
||
205 | foreach ($_POST as $k => $v) |
||
206 | $requestString .= '&' . $k . '=' . urlencode($v); |
||
207 | |||
208 | // Can we use curl? |
||
209 | if (function_exists('curl_init') && $curl = curl_init((!empty($modSettings['paidsubs_test']) ? 'https://www.sandbox.' : 'https://www.') . 'paypal.com/cgi-bin/webscr')) |
||
210 | { |
||
211 | // Set the post data. |
||
212 | curl_setopt($curl, CURLOPT_POST, true); |
||
213 | curl_setopt($curl, CURLOPT_POSTFIELDSIZE, 0); |
||
214 | curl_setopt($curl, CURLOPT_POSTFIELDS, $requestString); |
||
215 | |||
216 | // Set up the headers so paypal will accept the post |
||
217 | curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1); |
||
218 | curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); |
||
219 | curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); |
||
220 | curl_setopt($curl, CURLOPT_FORBID_REUSE, 1); |
||
221 | curl_setopt($curl, CURLOPT_HTTPHEADER, array( |
||
222 | 'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com', |
||
223 | 'Connection: close' |
||
224 | )); |
||
225 | |||
226 | // Fetch the data returned as a string. |
||
227 | curl_setopt($curl, CURLOPT_RETURNTRANSFER, true); |
||
228 | |||
229 | // Fetch the data. |
||
230 | $this->return_data = curl_exec($curl); |
||
0 ignored issues
–
show
|
|||
231 | |||
232 | // Close the session. |
||
233 | curl_close($curl); |
||
234 | } |
||
235 | // Otherwise good old HTTP. |
||
236 | else |
||
237 | { |
||
238 | // Setup the headers. |
||
239 | $header = 'POST /cgi-bin/webscr HTTP/1.1' . "\r\n"; |
||
240 | $header .= 'content-type: application/x-www-form-urlencoded' . "\r\n"; |
||
241 | $header .= 'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com' . "\r\n"; |
||
242 | $header .= 'content-length: ' . strlen($requestString) . "\r\n"; |
||
243 | $header .= 'connection: close' . "\r\n\r\n"; |
||
244 | |||
245 | // Open the connection. |
||
246 | if (!empty($modSettings['paidsubs_test'])) |
||
247 | $fp = fsockopen('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30); |
||
248 | else |
||
249 | $fp = fsockopen('www.paypal.com', 80, $errno, $errstr, 30); |
||
250 | |||
251 | // Did it work? |
||
252 | if (!$fp) |
||
253 | generateSubscriptionError($txt['paypal_could_not_connect']); |
||
254 | |||
255 | // Put the data to the port. |
||
256 | fputs($fp, $header . $requestString); |
||
257 | |||
258 | // Get the data back... |
||
259 | while (!feof($fp)) |
||
260 | { |
||
261 | $this->return_data = fgets($fp, 1024); |
||
262 | if (strcmp(trim($this->return_data), 'VERIFIED') === 0) |
||
263 | break; |
||
264 | } |
||
265 | |||
266 | // Clean up. |
||
267 | fclose($fp); |
||
268 | } |
||
269 | |||
270 | // If this isn't verified then give up... |
||
271 | if (strcmp(trim($this->return_data), 'VERIFIED') !== 0) |
||
272 | exit; |
||
273 | |||
274 | // Check that this is intended for us. |
||
275 | if (strtolower($modSettings['paypal_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', strtolower($modSettings['paypal_additional_emails']))))) |
||
276 | exit; |
||
277 | |||
278 | // Is this a subscription - and if so is it a secondary payment that we need to process? |
||
279 | // If so, make sure we get it in the expected format. Seems PayPal sometimes sends it without urlencoding. |
||
280 | if (!empty($_POST['item_number']) && strpos($_POST['item_number'], ' ') !== false) |
||
281 | $_POST['item_number'] = str_replace(' ', '+', $_POST['item_number']); |
||
282 | if ($this->isSubscription() && (empty($_POST['item_number']) || strpos($_POST['item_number'], '+') === false)) |
||
283 | // Calculate the subscription it relates to! |
||
284 | $this->_findSubscription(); |
||
285 | |||
286 | // Verify the currency! |
||
287 | if (strtolower($_POST['mc_currency']) !== strtolower($modSettings['paid_currency_code'])) |
||
288 | exit; |
||
289 | |||
290 | // Can't exist if it doesn't contain anything. |
||
291 | if (empty($_POST['item_number'])) |
||
292 | exit; |
||
293 | |||
294 | // Return the id_sub and id_member |
||
295 | return explode('+', $_POST['item_number']); |
||
296 | } |
||
297 | |||
298 | /** |
||
299 | * Is this a refund? |
||
300 | * |
||
301 | * @return boolean Whether this is a refund |
||
302 | */ |
||
303 | public function isRefund() |
||
304 | { |
||
305 | if ($_POST['payment_status'] === 'Refunded' || $_POST['payment_status'] === 'Reversed' || $_POST['txn_type'] === 'Refunded' || ($_POST['txn_type'] === 'reversal' && $_POST['payment_status'] === 'Completed')) |
||
306 | return true; |
||
307 | else |
||
308 | return false; |
||
309 | } |
||
310 | |||
311 | /** |
||
312 | * Is this a subscription? |
||
313 | * |
||
314 | * @return boolean Whether this is a subscription |
||
315 | */ |
||
316 | public function isSubscription() |
||
317 | { |
||
318 | if (substr($_POST['txn_type'], 0, 14) === 'subscr_payment' && $_POST['payment_status'] === 'Completed') |
||
319 | return true; |
||
320 | else |
||
321 | return false; |
||
322 | } |
||
323 | |||
324 | /** |
||
325 | * Is this a normal payment? |
||
326 | * |
||
327 | * @return boolean Whether this is a normal payment |
||
328 | */ |
||
329 | public function isPayment() |
||
330 | { |
||
331 | if ($_POST['payment_status'] === 'Completed' && $_POST['txn_type'] === 'web_accept') |
||
332 | return true; |
||
333 | else |
||
334 | return false; |
||
335 | } |
||
336 | |||
337 | /** |
||
338 | * Is this a cancellation? |
||
339 | * |
||
340 | * @return boolean Whether this is a cancellation |
||
341 | */ |
||
342 | public function isCancellation() |
||
343 | { |
||
344 | // subscr_cancel is sent when the user cancels, subscr_eot is sent when the subscription reaches final payment |
||
345 | // Neither require us to *do* anything as per performCancel(). |
||
346 | // subscr_eot, if sent, indicates an end of payments term. |
||
347 | if (substr($_POST['txn_type'], 0, 13) === 'subscr_cancel' || substr($_POST['txn_type'], 0, 10) === 'subscr_eot') |
||
348 | return true; |
||
349 | else |
||
350 | return false; |
||
351 | } |
||
352 | |||
353 | /** |
||
354 | * Things to do in the event of a cancellation |
||
355 | * |
||
356 | * @param string $subscription_id |
||
357 | * @param int $member_id |
||
358 | * @param array $subscription_info |
||
359 | */ |
||
360 | public function performCancel($subscription_id, $member_id, $subscription_info) |
||
361 | { |
||
362 | // PayPal doesn't require SMF to notify it every time the subscription is up for renewal. |
||
363 | // A cancellation should not cause the user to be immediately dropped from their subscription, but |
||
364 | // let it expire normally. Some systems require taking action in the database to deal with this, but |
||
365 | // PayPal does not, so we actually just do nothing. But this is a nice prototype/example just in case. |
||
366 | } |
||
367 | |||
368 | /** |
||
369 | * How much was paid? |
||
370 | * |
||
371 | * @return float The amount paid |
||
372 | */ |
||
373 | public function getCost() |
||
374 | { |
||
375 | return (isset($_POST['tax']) ? $_POST['tax'] : 0) + $_POST['mc_gross']; |
||
376 | } |
||
377 | |||
378 | /** |
||
379 | * Record the transaction reference to finish up. |
||
380 | * |
||
381 | */ |
||
382 | public function close() |
||
383 | { |
||
384 | global $smcFunc, $subscription_id; |
||
385 | |||
386 | // If it's a subscription record the reference. |
||
387 | if ($_POST['txn_type'] == 'subscr_payment' && !empty($_POST['subscr_id'])) |
||
388 | { |
||
389 | $smcFunc['db_query']('', ' |
||
390 | UPDATE {db_prefix}log_subscribed |
||
391 | SET vendor_ref = {string:vendor_ref} |
||
392 | WHERE id_sublog = {int:current_subscription}', |
||
393 | array( |
||
394 | 'current_subscription' => $subscription_id, |
||
395 | 'vendor_ref' => $_POST['subscr_id'], |
||
396 | ) |
||
397 | ); |
||
398 | } |
||
399 | } |
||
400 | |||
401 | /** |
||
402 | * A private function to find out the subscription details. |
||
403 | * |
||
404 | * @access private |
||
405 | * @return boolean|void False on failure, otherwise just sets $_POST['item_number'] |
||
406 | */ |
||
407 | private function _findSubscription() |
||
408 | { |
||
409 | global $smcFunc; |
||
410 | |||
411 | // Assume we have this? |
||
412 | if (empty($_POST['subscr_id'])) |
||
413 | return false; |
||
414 | |||
415 | // Do we have this in the database? |
||
416 | $request = $smcFunc['db_query']('', ' |
||
417 | SELECT id_member, id_subscribe |
||
418 | FROM {db_prefix}log_subscribed |
||
419 | WHERE vendor_ref = {string:vendor_ref} |
||
420 | LIMIT 1', |
||
421 | array( |
||
422 | 'vendor_ref' => $_POST['subscr_id'], |
||
423 | ) |
||
424 | ); |
||
425 | // No joy? |
||
426 | if ($smcFunc['db_num_rows']($request) == 0) |
||
427 | { |
||
428 | // Can we identify them by email? |
||
429 | if (!empty($_POST['payer_email'])) |
||
430 | { |
||
431 | $smcFunc['db_free_result']($request); |
||
432 | $request = $smcFunc['db_query']('', ' |
||
433 | SELECT ls.id_member, ls.id_subscribe |
||
434 | FROM {db_prefix}log_subscribed AS ls |
||
435 | INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ls.id_member) |
||
436 | WHERE mem.email_address = {string:payer_email} |
||
437 | LIMIT 1', |
||
438 | array( |
||
439 | 'payer_email' => $_POST['payer_email'], |
||
440 | ) |
||
441 | ); |
||
442 | if ($smcFunc['db_num_rows']($request) === 0) |
||
443 | return false; |
||
444 | } |
||
445 | else |
||
446 | return false; |
||
447 | } |
||
448 | list ($member_id, $subscription_id) = $smcFunc['db_fetch_row']($request); |
||
449 | $_POST['item_number'] = $member_id . '+' . $subscription_id; |
||
450 | $smcFunc['db_free_result']($request); |
||
451 | } |
||
452 | } |
||
453 | |||
454 | ?> |
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.