Issues (1065)

Sources/Subscriptions-PayPal.php (1 issue)

1
<?php
2
3
/**
4
 * Simple Machines Forum (SMF)
5
 *
6
 * @package SMF
7
 * @author Simple Machines https://www.simplemachines.org
8
 * @copyright 2023 Simple Machines and individual contributors
9
 * @license https://www.simplemachines.org/about/smf/license.php BSD
10
 *
11
 * @version 2.1.4
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_POSTFIELDS, $requestString);
214
215
			// Set up the headers so paypal will accept the post
216
			curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
217
			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1);
218
			curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
219
			curl_setopt($curl, CURLOPT_FORBID_REUSE, 1);
220
			curl_setopt($curl, CURLOPT_HTTPHEADER, array(
221
				'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com',
222
				'Connection: close'
223
			));
224
225
			// Fetch the data returned as a string.
226
			curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
227
228
			// Fetch the data.
229
			$this->return_data = curl_exec($curl);
0 ignored issues
show
Documentation Bug introduced by
It seems like curl_exec($curl) can also be of type true. However, the property $return_data 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...
230
231
			// Close the session.
232
			curl_close($curl);
233
		}
234
		// Otherwise good old HTTP.
235
		else
236
		{
237
			// Setup the headers.
238
			$header = 'POST /cgi-bin/webscr HTTP/1.1' . "\r\n";
239
			$header .= 'content-type: application/x-www-form-urlencoded' . "\r\n";
240
			$header .= 'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com' . "\r\n";
241
			$header .= 'content-length: ' . strlen($requestString) . "\r\n";
242
			$header .= 'connection: close' . "\r\n\r\n";
243
244
			// Open the connection.
245
			if (!empty($modSettings['paidsubs_test']))
246
				$fp = fsockopen('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30);
247
			else
248
				$fp = fsockopen('www.paypal.com', 80, $errno, $errstr, 30);
249
250
			// Did it work?
251
			if (!$fp)
252
				generateSubscriptionError($txt['paypal_could_not_connect']);
253
254
			// Put the data to the port.
255
			fputs($fp, $header . $requestString);
256
257
			// Get the data back...
258
			while (!feof($fp))
259
			{
260
				$this->return_data = fgets($fp, 1024);
261
				if (strcmp(trim($this->return_data), 'VERIFIED') === 0)
262
					break;
263
			}
264
265
			// Clean up.
266
			fclose($fp);
267
		}
268
269
		// If this isn't verified then give up...
270
		if (strcmp(trim($this->return_data), 'VERIFIED') !== 0)
271
			exit;
272
273
		// Check that this is intended for us.
274
		if (strtolower($modSettings['paypal_email']) != strtolower($_POST['business']) && (empty($modSettings['paypal_additional_emails']) || !in_array(strtolower($_POST['business']), explode(',', strtolower($modSettings['paypal_additional_emails'])))))
275
			exit;
276
277
		// Is this a subscription - and if so is it a secondary payment that we need to process?
278
		// If so, make sure we get it in the expected format. Seems PayPal sometimes sends it without urlencoding.
279
		if (!empty($_POST['item_number']) && strpos($_POST['item_number'], ' ') !== false)
280
			$_POST['item_number'] = str_replace(' ', '+', $_POST['item_number']);
281
		if ($this->isSubscription() && (empty($_POST['item_number']) || strpos($_POST['item_number'], '+') === false))
282
			// Calculate the subscription it relates to!
283
			$this->_findSubscription();
284
285
		// Verify the currency!
286
		if (strtolower($_POST['mc_currency']) !== strtolower($modSettings['paid_currency_code']))
287
			exit;
288
289
		// Can't exist if it doesn't contain anything.
290
		if (empty($_POST['item_number']))
291
			exit;
292
293
		// Return the id_sub and id_member
294
		return explode('+', $_POST['item_number']);
295
	}
296
297
	/**
298
	 * Is this a refund?
299
	 *
300
	 * @return boolean Whether this is a refund
301
	 */
302
	public function isRefund()
303
	{
304
		if ($_POST['payment_status'] === 'Refunded' || $_POST['payment_status'] === 'Reversed' || $_POST['txn_type'] === 'Refunded' || ($_POST['txn_type'] === 'reversal' && $_POST['payment_status'] === 'Completed'))
305
			return true;
306
		else
307
			return false;
308
	}
309
310
	/**
311
	 * Is this a subscription?
312
	 *
313
	 * @return boolean Whether this is a subscription
314
	 */
315
	public function isSubscription()
316
	{
317
		if (substr($_POST['txn_type'], 0, 14) === 'subscr_payment' && $_POST['payment_status'] === 'Completed')
318
			return true;
319
		else
320
			return false;
321
	}
322
323
	/**
324
	 * Is this a normal payment?
325
	 *
326
	 * @return boolean Whether this is a normal payment
327
	 */
328
	public function isPayment()
329
	{
330
		if ($_POST['payment_status'] === 'Completed' && $_POST['txn_type'] === 'web_accept')
331
			return true;
332
		else
333
			return false;
334
	}
335
336
	/**
337
	 * Is this a cancellation?
338
	 *
339
	 * @return boolean Whether this is a cancellation
340
	 */
341
	public function isCancellation()
342
	{
343
		// subscr_cancel is sent when the user cancels, subscr_eot is sent when the subscription reaches final payment
344
		// Neither require us to *do* anything as per performCancel().
345
		// subscr_eot, if sent, indicates an end of payments term.
346
		if (substr($_POST['txn_type'], 0, 13) === 'subscr_cancel' || substr($_POST['txn_type'], 0, 10) === 'subscr_eot')
347
			return true;
348
		else
349
			return false;
350
	}
351
352
	/**
353
	 * Things to do in the event of a cancellation
354
	 *
355
	 * @param string $subscription_id
356
	 * @param int $member_id
357
	 * @param array $subscription_info
358
	 */
359
	public function performCancel($subscription_id, $member_id, $subscription_info)
360
	{
361
		// PayPal doesn't require SMF to notify it every time the subscription is up for renewal.
362
		// A cancellation should not cause the user to be immediately dropped from their subscription, but
363
		// let it expire normally. Some systems require taking action in the database to deal with this, but
364
		// PayPal does not, so we actually just do nothing. But this is a nice prototype/example just in case.
365
	}
366
367
	/**
368
	 * How much was paid?
369
	 *
370
	 * @return float The amount paid
371
	 */
372
	public function getCost()
373
	{
374
		return (isset($_POST['tax']) ? $_POST['tax'] : 0) + $_POST['mc_gross'];
375
	}
376
377
	/**
378
	 * Record the transaction reference to finish up.
379
	 *
380
	 */
381
	public function close()
382
	{
383
		global $smcFunc, $subscription_id;
384
385
		// If it's a subscription record the reference.
386
		if ($_POST['txn_type'] == 'subscr_payment' && !empty($_POST['subscr_id']))
387
		{
388
			$smcFunc['db_query']('', '
389
				UPDATE {db_prefix}log_subscribed
390
				SET vendor_ref = {string:vendor_ref}
391
				WHERE id_sublog = {int:current_subscription}',
392
				array(
393
					'current_subscription' => $subscription_id,
394
					'vendor_ref' => $_POST['subscr_id'],
395
				)
396
			);
397
		}
398
	}
399
400
	/**
401
	 * A private function to find out the subscription details.
402
	 *
403
	 * @access private
404
	 * @return boolean|void False on failure, otherwise just sets $_POST['item_number']
405
	 */
406
	private function _findSubscription()
407
	{
408
		global $smcFunc;
409
410
		// Assume we have this?
411
		if (empty($_POST['subscr_id']))
412
			return false;
413
414
		// Do we have this in the database?
415
		$request = $smcFunc['db_query']('', '
416
			SELECT id_member, id_subscribe
417
			FROM {db_prefix}log_subscribed
418
			WHERE vendor_ref = {string:vendor_ref}
419
			LIMIT 1',
420
			array(
421
				'vendor_ref' => $_POST['subscr_id'],
422
			)
423
		);
424
		// No joy?
425
		if ($smcFunc['db_num_rows']($request) == 0)
426
		{
427
			// Can we identify them by email?
428
			if (!empty($_POST['payer_email']))
429
			{
430
				$smcFunc['db_free_result']($request);
431
				$request = $smcFunc['db_query']('', '
432
					SELECT ls.id_member, ls.id_subscribe
433
					FROM {db_prefix}log_subscribed AS ls
434
						INNER JOIN {db_prefix}members AS mem ON (mem.id_member = ls.id_member)
435
					WHERE mem.email_address = {string:payer_email}
436
					LIMIT 1',
437
					array(
438
						'payer_email' => $_POST['payer_email'],
439
					)
440
				);
441
				if ($smcFunc['db_num_rows']($request) === 0)
442
					return false;
443
			}
444
			else
445
				return false;
446
		}
447
		list ($member_id, $subscription_id) = $smcFunc['db_fetch_row']($request);
448
		$_POST['item_number'] = $member_id . '+' . $subscription_id;
449
		$smcFunc['db_free_result']($request);
450
	}
451
}
452
453
?>