Browse Source

! paypal will be requiring http 1.1 requests as well as requiring host and connection headers [bug 5009]
! update sandbox to use https, the former address results in a redirect
! curl did not work due to improper check
! subscriptions should also check for approved payment
! doc blocks and general cleanup
! forgot the copyright was changed

Signed-off-by: Spuds <[email protected]>

Spuds 11 years ago
parent
commit
07d4bc9fba
2 changed files with 147 additions and 35 deletions
  1. 112 28
      Sources/Subscriptions-PayPal.php
  2. 35 7
      subscriptions.php

+ 112 - 28
Sources/Subscriptions-PayPal.php

@@ -17,22 +17,40 @@
 if (!defined('SMF'))
 	die('Hacking attempt...');
 
+/**
+ * Class for returning available form data for this gateway
+ */
 class paypal_display
 {
+	/**
+	 * Name of this payment gateway
+	 */
 	public $title = 'PayPal';
 
+	/**
+	 * Return the admin settings for this gateway
+	 *
+	 * @return array
+	 */
 	public function getGatewaySettings()
 	{
 		global $txt;
 
 		$setting_data = array(
-			array('text', 'paypal_email', 'subtext' => $txt['paypal_email_desc']),
+			array(
+				'text', 'paypal_email',
+				'subtext' => $txt['paypal_email_desc']
+			),
 		);
 
 		return $setting_data;
 	}
 
-	// Is this enabled for new payments?
+	/**
+	 * Is this enabled for new payments?
+	 *
+	 * @return boolean
+	 */
 	public function gatewayEnabled()
 	{
 		global $modSettings;
@@ -40,7 +58,19 @@ class paypal_display
 		return !empty($modSettings['paypal_email']);
 	}
 
-	// What do we want?
+	/**
+	 * What do we want?
+	 *
+	 * Called from Profile-Actions.php to return a unique set of fields for the given gateway
+	 * plus all the standard ones for the subscription form
+	 *
+	 * @param type $unique_id
+	 * @param type $sub_data
+	 * @param type $value
+	 * @param type $period
+	 * @param type $return_url
+	 * @return string
+	 */
 	public function fetchGatewayFields($unique_id, $sub_data, $value, $period, $return_url)
 	{
 		global $modSettings, $txt, $boardurl;
@@ -99,11 +129,18 @@ class paypal_display
 	}
 }
 
+/**
+ * Class of functions to validate a IPN response and provide details of the payment
+ */
 class paypal_payment
 {
 	private $return_data;
 
-	// This function returns true/false for whether this gateway thinks the data is intended for it.
+	/**
+	 * This function returns true/false for whether this gateway thinks the data is intended for it.
+	 *
+	 * @return boolean
+	 */
 	public function isValid()
 	{
 		global $modSettings;
@@ -117,12 +154,22 @@ class paypal_payment
 		// Correct email address?
 		if (!isset($_POST['business']))
 			$_POST['business'] = $_POST['receiver_email'];
-		if ($modSettings['paypal_email'] != $_POST['business'] && (empty($modSettings['paypal_additional_emails']) || !in_array($_POST['business'], explode(',', $modSettings['paypal_additional_emails']))))
+
+		if ($modSettings['paypal_email'] !== $_POST['business'] && (empty($modSettings['paypal_additional_emails']) || !in_array($_POST['business'], explode(',', $modSettings['paypal_additional_emails']))))
 			return false;
 		return true;
 	}
 
-	// Validate all the data was valid.
+	/**
+	 * Post the IPN data received back to paypal for validaion
+	 * Sends the complete unaltered message back to PayPal. The message must contain the same fields
+	 * in the same order and be encoded in the same way as the original message
+	 * PayPal will respond back with a single word, which is either VERIFIED if the message originated with PayPal or INVALID
+	 *
+	 * If valid returns the subscription and member IDs we are going to process if it passes
+	 *
+	 * @return string
+	 */
 	public function precheck()
 	{
 		global $modSettings, $txt;
@@ -134,18 +181,28 @@ class paypal_payment
 		// Build the request string - starting with the minimum requirement.
 		$requestString = 'cmd=_notify-validate';
 
-		// Now my dear, add all the posted bits.
+		// Now my dear, add all the posted bits in the order we got them
 		foreach ($_POST as $k => $v)
 			$requestString .= '&' . $k . '=' . urlencode($v);
 
 		// Can we use curl?
-		if (function_exists('curl_init') && $curl = curl_init('http://www.', !empty($modSettings['paidsubs_test']) ? 'sandbox.' : '', 'paypal.com/cgi-bin/webscr'))
+		if (function_exists('curl_init') && $curl = curl_init((!empty($modSettings['paidsubs_test']) ? 'https://www.sandbox.' : 'http://www.') . 'paypal.com/cgi-bin/webscr'))
 		{
 			// Set the post data.
 			curl_setopt($curl, CURLOPT_POST, true);
 			curl_setopt($curl, CURLOPT_POSTFIELDSIZE, 0);
 			curl_setopt($curl, CURLOPT_POSTFIELDS, $requestString);
 
+			// Set up the headers so paypal will accept the post
+			curl_setopt($curl, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
+			curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1);
+			curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2);
+			curl_setopt($curl, CURLOPT_FORBID_REUSE, 1);
+			curl_setopt($curl, CURLOPT_HTTPHEADER, array(
+				'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com',
+				'Connection: close'
+			));
+
 			// Fetch the data returned as a string.
 			curl_setopt($curl, CURLOPT_RETURNTRANSFER, true);
 
@@ -159,12 +216,17 @@ class paypal_payment
 		else
 		{
 			// Setup the headers.
-			$header = 'POST /cgi-bin/webscr HTTP/1.0' . "\r\n";
+			$header = 'POST /cgi-bin/webscr HTTP/1.1' . "\r\n";
 			$header .= 'Content-Type: application/x-www-form-urlencoded' . "\r\n";
-			$header .= 'Content-Length: ' . strlen ($requestString) . "\r\n\r\n";
+			$header .= 'Host: www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com' . "\r\n";
+			$header .= 'Content-Length: ' . strlen ($requestString) . "\r\n";
+			$header .= 'Connection: close' . "\r\n\r\n";
 
 			// Open the connection.
-			$fp = fsockopen('www.' . (!empty($modSettings['paidsubs_test']) ? 'sandbox.' : '') . 'paypal.com', 80, $errno, $errstr, 30);
+			if (!empty($modSettings['paidsubs_test']))
+				$fp = fsockopen('ssl://www.sandbox.paypal.com', 443, $errno, $errstr, 30);
+			else
+				$fp = fsockopen('www.paypal.com', 80, $errno, $errstr, 30);
 
 			// Did it work?
 			if (!$fp)
@@ -177,7 +239,7 @@ class paypal_payment
 			while (!feof($fp))
 			{
 				$this->return_data = fgets($fp, 1024);
-				if (strcmp($this->return_data, 'VERIFIED') == 0)
+				if (strcmp(trim($this->return_data), 'VERIFIED') === 0)
 					break;
 			}
 
@@ -186,21 +248,20 @@ class paypal_payment
 		}
 
 		// If this isn't verified then give up...
-		// !! This contained a comment "send an email", but we don't appear to send any?
-		if (strcmp($this->return_data, 'VERIFIED') != 0)
+		if (strcmp(trim($this->return_data), 'VERIFIED') !== 0)
 			exit;
 
 		// Check that this is intended for us.
-		if ($modSettings['paypal_email'] != $_POST['business'] && (empty($modSettings['paypal_additional_emails']) || !in_array($_POST['business'], explode(',', $modSettings['paypal_additional_emails']))))
+		if ($modSettings['paypal_email'] !== $_POST['business'] && (empty($modSettings['paypal_additional_emails']) || !in_array($_POST['business'], explode(',', $modSettings['paypal_additional_emails']))))
 			exit;
 
-		// Is this a subscription - and if so it's it a secondary payment that we need to process?
+		// Is this a subscription - and if so is it a secondary payment that we need to process?
 		if ($this->isSubscription() && (empty($_POST['item_number']) || strpos($_POST['item_number'], '+') === false))
 			// Calculate the subscription it relates to!
 			$this->_findSubscription();
 
 		// Verify the currency!
-		if (strtolower($_POST['mc_currency']) != $modSettings['paid_currency_code'])
+		if (strtolower($_POST['mc_currency']) !== $modSettings['paid_currency_code'])
 			exit;
 
 		// Can't exist if it doesn't contain anything.
@@ -211,40 +272,59 @@ class paypal_payment
 		return explode('+', $_POST['item_number']);
 	}
 
-	// Is this a refund?
+	/**
+	 * Is this a refund?
+	 *
+	 * @return boolean
+	 */
 	public function isRefund()
 	{
-		if ($_POST['payment_status'] == 'Refunded' || $_POST['payment_status'] == 'Reversed' || $_POST['txn_type'] == 'Refunded' || ($_POST['txn_type'] == 'reversal' && $_POST['payment_status'] == 'Completed'))
+		if ($_POST['payment_status'] === 'Refunded' || $_POST['payment_status'] === 'Reversed' || $_POST['txn_type'] === 'Refunded' || ($_POST['txn_type'] === 'reversal' && $_POST['payment_status'] === 'Completed'))
 			return true;
 		else
 			return false;
 	}
 
-	// Is this a subscription?
+	/**
+	 * Is this a subscription?
+	 *
+	 * @return boolean
+	 */
 	public function isSubscription()
 	{
-		if (substr($_POST['txn_type'], 0, 14) == 'subscr_payment')
+		if (substr($_POST['txn_type'], 0, 14) === 'subscr_payment' && $_POST['payment_status'] === 'Completed')
 			return true;
 		else
 			return false;
 	}
 
-	// Is this a normal payment?
+	/**
+	 * Is this a normal payment?
+	 *
+	 * @return boolean
+	 */
 	public function isPayment()
 	{
-		if ($_POST['payment_status'] == 'Completed' && $_POST['txn_type'] == 'web_accept')
+		if ($_POST['payment_status'] === 'Completed' && $_POST['txn_type'] === 'web_accept')
 			return true;
 		else
 			return false;
 	}
 
-	// How much was paid?
+	/**
+	 * How much was paid?
+	 *
+	 * @return float
+	 */
 	public function getCost()
 	{
-		return $_POST['tax'] + $_POST['mc_gross'];
+		return (isset($_POST['tax']) ? $_POST['tax'] : 0) + $_POST['mc_gross'];
 	}
 
-	// exit.
+	/**
+	 * Record the transaction reference and exit
+	 *
+	 */
 	public function close()
 	{
 		global $smcFunc, $subscription_id;
@@ -267,7 +347,11 @@ class paypal_payment
 		exit();
 	}
 
-	// A private function to find out the subscription details.
+	/**
+	 * A private function to find out the subscription details.
+	 *
+	 * @return boolean
+	 */
 	private function _findSubscription()
 	{
 		global $smcFunc;
@@ -303,7 +387,7 @@ class paypal_payment
 						'payer_email' => $_POST['payer_email'],
 					)
 				);
-				if ($smcFunc['db_num_rows']($request) == 0)
+				if ($smcFunc['db_num_rows']($request) === 0)
 					return false;
 			}
 			else

+ 35 - 7
subscriptions.php

@@ -3,7 +3,7 @@
 /**
  * This file is the file which all subscription gateways should call
  * when a payment has been received - it sorts out the user status.
- * 
+ *
  * Simple Machines Forum (SMF)
  *
  * @package SMF
@@ -41,12 +41,14 @@ if (empty($modSettings['paid_enabled']))
 // If we have some custom people who find out about problems load them here.
 $notify_users = array();
 if (!empty($modSettings['paid_email_to']))
+{
 	foreach (explode(',', $modSettings['paid_email_to']) as $email)
 		$notify_users[] = array(
 			'email' => $email,
 			'name' => $txt['who_member'],
 			'id' => 0,
 		);
+}
 
 // We need to see whether we can find the correct payment gateway,
 // we'll going to go through all our gateway scripts and find out
@@ -67,7 +69,7 @@ if (empty($txnType))
 	generateSubscriptionError($txt['paid_unknown_transaction_type']);
 
 // Get the subscription and member ID amoungst others...
-@list ($subscription_id, $member_id) = $gatewayClass->precheck();
+@list($subscription_id, $member_id) = $gatewayClass->precheck();
 
 // Integer these just in case.
 $subscription_id = (int) $subscription_id;
@@ -87,7 +89,7 @@ $request = $smcFunc['db_query']('', '
 	)
 );
 // Didn't find them?
-if ($smcFunc['db_num_rows']($request) == 0)
+if ($smcFunc['db_num_rows']($request) === 0)
 	generateSubscriptionError(sprintf($txt['paid_could_not_find_member'], $member_id));
 $member_info = $smcFunc['db_fetch_assoc']($request);
 $smcFunc['db_free_result']($request);
@@ -103,7 +105,7 @@ $request = $smcFunc['db_query']('', '
 );
 
 // Didn't find it?
-if ($smcFunc['db_num_rows']($request) == 0)
+if ($smcFunc['db_num_rows']($request) === 0)
 	generateSubscriptionError(sprintf($txt['paid_count_not_find_subscription'], $member_id, $subscription_id));
 
 $subscription_info = $smcFunc['db_fetch_assoc']($request);
@@ -121,7 +123,7 @@ $request = $smcFunc['db_query']('', '
 		'current_member' => $member_id,
 	)
 );
-if ($smcFunc['db_num_rows']($request) == 0)
+if ($smcFunc['db_num_rows']($request) === 0)
 	generateSubscriptionError(sprintf($txt['paid_count_not_find_subscription_log'], $member_id, $subscription_id));
 $subscription_info += $smcFunc['db_fetch_assoc']($request);
 $smcFunc['db_free_result']($request);
@@ -188,6 +190,7 @@ elseif ($gatewayClass->isPayment() || $gatewayClass->isSubscription())
 		$real_details = @unserialize($subscription_info['pending_details']);
 		if (empty($real_details))
 			generateSubscriptionError(sprintf($txt['paid_count_not_find_outstanding_payment'], $member_id, $subscription_id));
+		
 		// Now we just try to find anything pending.
 		// We don't really care which it is as security happens later.
 		foreach ($real_details as $id => $detail)
@@ -197,6 +200,7 @@ elseif ($gatewayClass->isPayment() || $gatewayClass->isSubscription())
 				$subscription_info['payments_pending']--;
 			break;
 		}
+		
 		$subscription_info['pending_details'] = empty($real_details) ? '' : serialize($real_details);
 
 		$smcFunc['db_query']('', '
@@ -215,6 +219,7 @@ elseif ($gatewayClass->isPayment() || $gatewayClass->isSubscription())
 	if ($subscription_info['length'] == 'F')
 	{
 		$found_duration = 0;
+		
 		// This is a little harder, can we find the right duration?
 		foreach ($cost as $duration => $value)
 		{
@@ -225,7 +230,7 @@ elseif ($gatewayClass->isPayment() || $gatewayClass->isSubscription())
 		}
 
 		// If we have the duration then we're done.
-		if ($found_duration!== 0)
+		if ($found_duration !== 0)
 		{
 			$notify = true;
 			addSubscription($subscription_id, $member_id, $found_duration);
@@ -234,6 +239,7 @@ elseif ($gatewayClass->isPayment() || $gatewayClass->isSubscription())
 	else
 	{
 		$actual_cost = $cost['fixed'];
+		
 		// It must be at least the right amount.
 		if ($total_cost != 0 && $total_cost >= $actual_cost)
 		{
@@ -259,11 +265,31 @@ elseif ($gatewayClass->isPayment() || $gatewayClass->isSubscription())
 		emailAdmins('paid_subscription_new', $replacements, $notify_users);
 	}
 }
+else
+{
+	// Some other "valid" transaction such as:
+	//
+	// subscr_cancel: This IPN response (txn_type) is sent only when the subscriber cancels his/her
+	// current subscription or the merchant cancels the subscribers subscription. In this event according
+	// to Paypal rules the subscr_eot (End of Term) IPN response is NEVER sent, and it is upto you to
+	// keep the subscription of the subscriber acitve for remaining days of subscription should they cancel
+	// their subscription in the middle of the subscription period.
+	//
+	// subscr_signup: This IPN response (txn_type) is sent only the first time the user signs up for a subscription.
+	// It then does not fire in any event later. This response is received somewhere before or after the first payment of
+	// subscription is received (txn_type=subscr_payment) which is what we do process
+	//
+	// Should we log any of these ...
+}
 
 // In case we have anything specific to do.
 $gatewayClass->close();
 
-// Log an error then die.
+/**
+ * Log an error then exit
+ *
+ * @param string $text
+ */
 function generateSubscriptionError($text)
 {
 	global $modSettings, $notify_users, $smcFunc;
@@ -280,8 +306,10 @@ function generateSubscriptionError($text)
 
 	// Maybe we can try to give them the post data?
 	if (!empty($_POST))
+	{
 		foreach ($_POST as $key => $val)
 			$text .= '<br />' . $smcFunc['htmlspecialchars']($key) . ': ' . $smcFunc['htmlspecialchars']($val);
+	}
 
 	// Then just log and die.
 	log_error($text);