BxPaymentProviderPayPal.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php defined('BX_DOL') or die('hack attempt');
  2. /**
  3. * Copyright (c) UNA, Inc - https://una.io
  4. * MIT License - https://opensource.org/licenses/MIT
  5. *
  6. * @defgroup Payment Payment
  7. * @ingroup UnaModules
  8. *
  9. * @{
  10. */
  11. /**
  12. * Usefull links related to the integration:
  13. *
  14. * HTML Form Basics for PayPal Payments Standard
  15. * https://developer.paypal.com/docs/classic/paypal-payments-standard/integration-guide/formbasics/
  16. *
  17. * HTML Variables for PayPal Payments Standard
  18. * https://developer.paypal.com/docs/classic/paypal-payments-standard/integration-guide/Appx_websitestandard_htmlvariables/
  19. *
  20. */
  21. define('PP_MODE_LIVE', 1);
  22. define('PP_MODE_TEST', 2);
  23. define('PP_PRC_TYPE_DIRECT', 1);
  24. define('PP_PRC_TYPE_PDT', 2);
  25. define('PP_PRC_TYPE_IPN', 3);
  26. class BxPaymentProviderPayPal extends BxBaseModPaymentProvider implements iBxBaseModPaymentProvider
  27. {
  28. function __construct($aConfig)
  29. {
  30. $this->MODULE = 'bx_payment';
  31. parent::__construct($aConfig);
  32. $this->_bRedirectOnResult = false;
  33. $this->_sLogFile = BX_DIRECTORY_PATH_LOGS . 'bx_pp_' . $this->_sName . '.log';
  34. }
  35. public function initializeCheckout($iPendingId, $aCartInfo, $bRecurring = false, $iRecurringDays = 0)
  36. {
  37. $iMode = (int)$this->getOption('mode');
  38. $sActionURL = $iMode == PP_MODE_LIVE ? 'https://www.paypal.com/cgi-bin/webscr' : 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  39. if($bRecurring)
  40. $aFormData = array(
  41. 'cmd' => '_xclick-subscriptions',
  42. 'a3' => sprintf("%.2f", (float)$aCartInfo['items_price']),
  43. 'p3' => $iRecurringDays,
  44. 't3' => 'D',
  45. 'src' => '1', // repeat billings unles member cancels subscription
  46. 'sra' => '1' // reattempt on failure
  47. );
  48. else
  49. $aFormData = array(
  50. 'cmd' => '_cart',
  51. 'upload' => '1',
  52. 'amount' => sprintf( "%.2f", (float)$aCartInfo['items_price'])
  53. );
  54. $aFormData = array_merge($aFormData, array(
  55. 'business' => $iMode == PP_MODE_LIVE ? $this->getOption('business') : $this->getOption('sandbox'),
  56. 'currency_code' => $aCartInfo['vendor_currency_code'],
  57. 'item_name' => _t($this->_sLangsPrefix . 'txt_payment_to_for', $aCartInfo['vendor_name']),
  58. 'item_number' => $iPendingId,
  59. 'no_note' => '1',
  60. 'no_shipping' => '1',
  61. 'custom' => md5($aCartInfo['vendor_id'] . $iPendingId)
  62. ));
  63. foreach($aCartInfo['items'] as $aItem)
  64. $aFormData['item_name'] .= ' ' . $aItem['title'] . ',';
  65. $aFormData['item_name'] = trim($aFormData['item_name'], ', ');
  66. switch($this->getOption('prc_type')) {
  67. case PP_PRC_TYPE_PDT:
  68. case PP_PRC_TYPE_DIRECT:
  69. $aFormData = array_merge($aFormData, array(
  70. 'return' => $this->getReturnDataUrl($aCartInfo['vendor_id']),
  71. 'rm' => '2'
  72. ));
  73. break;
  74. case PP_PRC_TYPE_IPN:
  75. $aFormData = array_merge($aFormData, array(
  76. 'return' => $this->getReturnUrl(),
  77. 'notify_url' => $this->getReturnDataUrl($aCartInfo['vendor_id']),
  78. 'rm' => '1'
  79. ));
  80. break;
  81. }
  82. $this->_oModule->_oTemplate->displayPageCodeRedirect($sActionURL, $aFormData);
  83. exit;
  84. }
  85. public function finalizeCheckout(&$aData)
  86. {
  87. $aResult = $this->_registerCheckout($aData);
  88. if(!isset($aResult['code']) || (int)$aResult['code'] != BX_PAYMENT_RESULT_SUCCESS) {
  89. $this->log('Finalize Checkout Failed');
  90. $this->log($aData);
  91. }
  92. return $aResult;
  93. }
  94. /**
  95. *
  96. * @param $aData - data from payment provider.
  97. * @param $bSubscription - Is not needed. May be used in the future for subscriptions.
  98. * @param $iPendingId - Is not needed. May be used in the future for subscriptions.
  99. * @return array with results.
  100. */
  101. protected function _registerCheckout(&$aData, $bSubscription = false, $iPendingId = 0)
  102. {
  103. if(empty($this->_aOptions) && isset($aData['item_number']))
  104. $this->_aOptions = $this->getOptionsByPending($aData['item_number']);
  105. if(empty($this->_aOptions))
  106. return array('code' => 2, 'message' => $this->_sLangsPrefix . 'err_unknown_vendor');
  107. $iPrcType = (int)$this->getOption('prc_type');
  108. if(($iPrcType == PP_PRC_TYPE_IPN || $iPrcType == PP_PRC_TYPE_DIRECT) && (!isset($aData['item_number']) || !isset($aData['txn_id'])))
  109. return array('code' => 1, 'message' => $this->_sLangsPrefix . 'pp_err_no_data_given');
  110. else if($iPrcType == PP_PRC_TYPE_PDT && !isset($aData['tx']))
  111. return array('code' => 1, 'message' => $this->_sLangsPrefix . 'pp_err_no_data_given');
  112. $aResult = $this->_validateCheckout($aData);
  113. if(!$bSubscription || empty($iPendingId))
  114. $iPendingId = (int)$aData['item_number'];
  115. $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $iPendingId));
  116. if(!empty($aPending['order']) || !empty($aPending['error_code']) || !empty($aPending['error_msg']) || (int)$aPending['processed'] != 0)
  117. return array('code' => 3, 'message' => $this->_sLangsPrefix . 'err_already_processed');
  118. //--- Update pending transaction ---//
  119. $this->_oModule->_oDb->updateOrderPending($iPendingId, array(
  120. 'order' => $aData['txn_id'],
  121. 'error_code' => $aResult['code'],
  122. 'error_msg' => _t($aResult['message'])
  123. ));
  124. $aResult['pending_id'] = $iPendingId;
  125. $aResult['client_name'] = _t($this->_sLangsPrefix . 'txt_buyer_name_mask', bx_process_input($aData['first_name']), bx_process_input($aData['last_name']));
  126. $aResult['client_email'] = bx_process_input($aData['payer_email']);
  127. $aResult['paid'] = true;
  128. return $aResult;
  129. }
  130. protected function _validateCheckout(&$aData)
  131. {
  132. $iMode = (int)$this->getOption('mode');
  133. if($iMode == PP_MODE_LIVE) {
  134. $sBusiness = $this->getOption('business');
  135. $sConnectionUrl = 'www.paypal.com';
  136. } else {
  137. $sBusiness = $this->getOption('sandbox');
  138. $sConnectionUrl = 'www.sandbox.paypal.com';
  139. }
  140. $iPrcType = $this->getOption('prc_type');
  141. if($iPrcType == PP_PRC_TYPE_DIRECT || $iPrcType == PP_PRC_TYPE_IPN) {
  142. if($aData['payment_status'] != 'Completed' )
  143. return array('code' => 4, 'message' => $this->_sLangsPrefix . 'pp_err_not_completed');
  144. if($aData['business'] != $sBusiness)
  145. return array('code' => 5, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_business');
  146. $sRequest = 'cmd=_notify-validate';
  147. foreach($aData as $sKey => $sValue) {
  148. if(in_array($sKey, array('cmd')))
  149. continue;
  150. $sRequest .= '&' . urlencode($sKey) . '=' . urlencode(bx_process_pass($sValue));
  151. }
  152. $aResponse = $this->_readValidationData($sConnectionUrl, $sRequest);
  153. if((int)$aResponse['code'] !== 0)
  154. return $aResponse;
  155. array_walk($aResponse['content'], create_function('&$arg', "\$arg = trim(\$arg);"));
  156. if(strcmp($aResponse['content'][0], "INVALID") === 0)
  157. return array('code' => 7, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_transaction');
  158. else if(strcmp($aResponse['content'][0], "VERIFIED") !== 0)
  159. return array('code' => 8, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_verification_status');
  160. }
  161. else if($iPrcType == PP_PRC_TYPE_PDT) {
  162. $sRequest = "cmd=_notify-synch&tx=" . $aData['tx'] . "&at=" . $this->getOption('token');
  163. $aResponse = $this->_readValidationData($sConnectionUrl, $sRequest);
  164. if((int)$aResponse['code'] !== 0)
  165. return $aResponse;
  166. if(strcmp($aResponse['content'][0], "FAIL") === 0)
  167. return array('code' => 7, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_transaction');
  168. else if(strcmp($aResponse['content'][0], "SUCCESS") !== 0)
  169. return array('code' => 8, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_verification_status');
  170. $aKeys = array();
  171. foreach($aResponse['content'] as $sLine) {
  172. list($sKey, $sValue) = explode("=", $sLine);
  173. $aKeys[urldecode($sKey)] = urldecode($sValue);
  174. }
  175. $aData = array_merge($aData, $aKeys);
  176. if($aData['payment_status'] != 'Completed' )
  177. return array('code' => 4, 'message' => $this->_sLangsPrefix . 'pp_err_not_completed');
  178. if($aData['business'] != $sBusiness)
  179. return array('code' => 5, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_business');
  180. }
  181. $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $aData['item_number']));
  182. $aVendor = $this->_oModule->getVendorInfo($aPending['seller_id']);
  183. $fAmount = (float)$this->_getReceivedAmount($aVendor['currency_code'], $aData);
  184. if($fAmount != (float)$aPending['amount'])
  185. return array('code' => 9, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_amount');
  186. if($aData['custom'] != md5($aPending['seller_id'] . $aPending['id']))
  187. return array('code' => 10, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_custom_data');
  188. return array('code' => BX_PAYMENT_RESULT_SUCCESS, 'message' => $this->_sLangsPrefix . 'pp_msg_verified');
  189. }
  190. protected function _readValidationData($sConnectionUrl, $sRequest)
  191. {
  192. $rConnect = curl_init('https://' . $sConnectionUrl . '/cgi-bin/webscr');
  193. curl_setopt($rConnect, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  194. curl_setopt($rConnect, CURLOPT_POST, 1);
  195. curl_setopt($rConnect, CURLOPT_RETURNTRANSFER,1);
  196. curl_setopt($rConnect, CURLOPT_POSTFIELDS, $sRequest);
  197. curl_setopt($rConnect, CURLOPT_SSL_VERIFYPEER, 1);
  198. curl_setopt($rConnect, CURLOPT_SSL_VERIFYHOST, 2);
  199. curl_setopt($rConnect, CURLOPT_FORBID_REUSE, 1);
  200. curl_setopt($rConnect, CURLOPT_HTTPHEADER, array('Connection: Close'));
  201. $sResponse = curl_exec($rConnect);
  202. if(curl_errno($rConnect) == 60) { // CURLE_SSL_CACERT
  203. curl_setopt($rConnect, CURLOPT_CAINFO, BX_DIRECTORY_PATH_PLUGINS . 'curl/cacert.pem');
  204. $sResponse = curl_exec($rConnect);
  205. }
  206. if(!$sResponse) {
  207. $this->log('Validation Data: ' . curl_error($rConnect));
  208. $this->log($sResponse);
  209. curl_close($rConnect);
  210. return array('code' => 6, 'message' => $this->_sLangsPrefix . 'err_cannot_validate');
  211. }
  212. curl_close($rConnect);
  213. return array('code' => 0, 'content' => explode("\n", $sResponse));
  214. }
  215. protected function _getReceivedAmount($sCurrencyCode, &$aResultData)
  216. {
  217. $fAmount = 0.00;
  218. $fTax = isset($aResultData['tax']) ? (float)$aResultData['tax'] : 0.00;
  219. if($aResultData['mc_currency'] == $sCurrencyCode && isset($aResultData['payment_gross']) && !empty($aResultData['payment_gross']))
  220. $fAmount = (float)$aResultData['payment_gross'] - $fTax;
  221. else if($aResultData['mc_currency'] == $sCurrencyCode && isset($aResultData['mc_gross']) && !empty($aResultData['mc_gross']))
  222. $fAmount = (float)$aResultData['mc_gross'] - $fTax;
  223. else if($aResultData['settle_currency'] == $sCurrencyCode && isset($aResultData['settle_amount']) && !empty($aResultData['settle_amount']))
  224. $fAmount = (float)$aResultData['settle_amount'] - $fTax;
  225. return $fAmount;
  226. }
  227. }
  228. /** @} */