BxPaymentProviderPayPal.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313
  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. }
  33. public function initializeCheckout($iPendingId, $aCartInfo, $bRecurring = false, $iRecurringDays = 0)
  34. {
  35. $iMode = (int)$this->getOption('mode');
  36. $sItemName = _t($this->_sLangsPrefix . 'txt_payment_to_for', $aCartInfo['vendor_name']);
  37. $sActionURL = $iMode == PP_MODE_LIVE ? 'https://www.paypal.com/cgi-bin/webscr' : 'https://www.sandbox.paypal.com/cgi-bin/webscr';
  38. if($bRecurring) {
  39. $aFormData = array(
  40. 'cmd' => '_xclick-subscriptions',
  41. 'a3' => sprintf("%.2f", (float)$aCartInfo['items_price']),
  42. 'p3' => $iRecurringDays,
  43. 't3' => 'D',
  44. 'src' => '1', // repeat billings unles member cancels subscription
  45. 'sra' => '1' // reattempt on failure
  46. );
  47. foreach($aCartInfo['items'] as $aItem)
  48. $sItemName .= ' ' . $aItem['title'] . ',';
  49. $sItemName = trim($sItemName, ', ');
  50. }
  51. else {
  52. $aFormData = array(
  53. 'cmd' => '_cart',
  54. 'upload' => '1',
  55. 'item_name' => $sItemName,
  56. 'amount' => sprintf( "%.2f", (float)$aCartInfo['items_price'])
  57. );
  58. foreach($aCartInfo['items'] as $iIndex => $aItem) {
  59. $i = $iIndex + 1;
  60. $aFormData['item_name_' . $i] = bx_process_output($aItem['title']);
  61. $aFormData['quantity_' . $i] = $aItem['quantity'];
  62. $aFormData['amount_' . $i] = $this->_oModule->_oConfig->getPrice(BX_PAYMENT_TYPE_SINGLE, $aItem);
  63. }
  64. }
  65. $aFormData = array_merge($aFormData, array(
  66. 'business' => $iMode == PP_MODE_LIVE ? $this->getOption('business') : $this->getOption('sandbox'),
  67. 'currency_code' => $aCartInfo['vendor_currency_code'],
  68. 'no_note' => '1',
  69. 'no_shipping' => '1',
  70. 'custom' => $this->_constructCustomData($aCartInfo['vendor_id'], $iPendingId),
  71. 'item_name' => $sItemName,
  72. 'item_number' => $iPendingId,
  73. ));
  74. switch($this->getOption('prc_type')) {
  75. case PP_PRC_TYPE_PDT:
  76. case PP_PRC_TYPE_DIRECT:
  77. $aFormData = array_merge($aFormData, array(
  78. 'return' => $this->getReturnDataUrl($aCartInfo['vendor_id']),
  79. 'rm' => '2'
  80. ));
  81. break;
  82. case PP_PRC_TYPE_IPN:
  83. $aFormData = array_merge($aFormData, array(
  84. 'return' => $this->getReturnUrl(),
  85. 'notify_url' => $this->getReturnDataUrl($aCartInfo['vendor_id']),
  86. 'rm' => '1'
  87. ));
  88. break;
  89. }
  90. $this->log('Initialize Checkout:');
  91. $this->log($aFormData);
  92. $this->_oModule->_oTemplate->displayPageCodeRedirect($sActionURL, $aFormData);
  93. exit;
  94. }
  95. public function finalizeCheckout(&$aData)
  96. {
  97. $aResult = $this->_registerCheckout($aData);
  98. if(!isset($aResult['code']) || (int)$aResult['code'] != BX_PAYMENT_RESULT_SUCCESS) {
  99. $this->log('Finalize Checkout: Failed');
  100. $this->log($aData);
  101. $this->log($aResult);
  102. }
  103. return $aResult;
  104. }
  105. /**
  106. *
  107. * @param $aData - data from payment provider.
  108. * @return array with results.
  109. */
  110. protected function _registerCheckout(&$aData)
  111. {
  112. if(empty($this->_aOptions) && !empty($aData['item_number']))
  113. $this->_aOptions = $this->getOptionsByPending((int)$aData['item_number']);
  114. if(empty($this->_aOptions))
  115. return array('code' => 1, 'message' => $this->_sLangsPrefix . 'err_incorrect_provider');
  116. $iPrcType = (int)$this->getOption('prc_type');
  117. if((in_array($iPrcType, array(PP_PRC_TYPE_DIRECT, PP_PRC_TYPE_IPN)) && !isset($aData['txn_id'])) || ($iPrcType == PP_PRC_TYPE_PDT && !isset($aData['tx'])))
  118. return array('code' => 2, 'message' => $this->_sLangsPrefix . 'pp_err_no_data_given');
  119. $aPending = array();
  120. $aResult = $this->_validateCheckout($aData, $aPending);
  121. if(empty($aPending) || !is_array($aPending))
  122. return $aResult;
  123. if(!empty($aPending['order']) || !empty($aPending['error_code']) || !empty($aPending['error_msg']) || (int)$aPending['processed'] != 0)
  124. return array('code' => 11, 'message' => $this->_sLangsPrefix . 'err_already_processed');
  125. //--- Update pending transaction
  126. $this->_oModule->_oDb->updateOrderPending($aPending['id'], array(
  127. 'order' => $aData['txn_id'],
  128. 'error_code' => $aResult['code'],
  129. 'error_msg' => _t($aResult['message'])
  130. ));
  131. $aResult['pending_id'] = $aPending['id'];
  132. $aResult['client_name'] = _t($this->_sLangsPrefix . 'txt_buyer_name_mask', bx_process_input($aData['first_name']), bx_process_input($aData['last_name']));
  133. $aResult['client_email'] = bx_process_input($aData['payer_email']);
  134. $aResult['paid'] = true;
  135. return $aResult;
  136. }
  137. protected function _validateCheckout(&$aData, &$aPending)
  138. {
  139. $iMode = (int)$this->getOption('mode');
  140. if($iMode == PP_MODE_LIVE) {
  141. $sBusiness = $this->getOption('business');
  142. $sConnectionUrl = 'www.paypal.com';
  143. } else {
  144. $sBusiness = $this->getOption('sandbox');
  145. $sConnectionUrl = 'www.sandbox.paypal.com';
  146. }
  147. $iPrcType = $this->getOption('prc_type');
  148. if($iPrcType == PP_PRC_TYPE_DIRECT || $iPrcType == PP_PRC_TYPE_IPN) {
  149. if($aData['payment_status'] != 'Completed' )
  150. return array('code' => 3, 'message' => $this->_sLangsPrefix . 'pp_err_not_completed');
  151. if($aData['business'] != $sBusiness)
  152. return array('code' => 4, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_business');
  153. $sRequest = 'cmd=_notify-validate';
  154. foreach($aData as $sKey => $sValue) {
  155. if(in_array($sKey, array('cmd', 'r')))
  156. continue;
  157. $sRequest .= '&' . urlencode($sKey) . '=' . urlencode(bx_process_pass($sValue));
  158. }
  159. $aResponse = $this->_readValidationData($sConnectionUrl, $sRequest);
  160. if((int)$aResponse['code'] !== 0)
  161. return $aResponse;
  162. foreach($aResponse['content'] as $iLine => $sLine)
  163. $aResponse['content'][$iLine] = trim($sLine);
  164. if(strcmp($aResponse['content'][0], "INVALID") === 0)
  165. return array('code' => 6, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_transaction');
  166. else if(strcmp($aResponse['content'][0], "VERIFIED") !== 0)
  167. return array('code' => 7, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_verification_status');
  168. }
  169. else if($iPrcType == PP_PRC_TYPE_PDT) {
  170. $sRequest = "cmd=_notify-synch&tx=" . $aData['tx'] . "&at=" . $this->getOption('token');
  171. $aResponse = $this->_readValidationData($sConnectionUrl, $sRequest);
  172. if((int)$aResponse['code'] !== 0)
  173. return $aResponse;
  174. if(strcmp($aResponse['content'][0], "FAIL") === 0)
  175. return array('code' => 6, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_transaction');
  176. else if(strcmp($aResponse['content'][0], "SUCCESS") !== 0)
  177. return array('code' => 7, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_verification_status');
  178. $aKeys = array();
  179. foreach($aResponse['content'] as $sLine)
  180. if(strpos($sLine, '=') !== false) {
  181. list($sKey, $sValue) = explode('=', $sLine);
  182. $aKeys[urldecode($sKey)] = urldecode($sValue);
  183. }
  184. $aData = array_merge($aData, $aKeys);
  185. if($aData['payment_status'] != 'Completed' )
  186. return array('code' => 3, 'message' => $this->_sLangsPrefix . 'pp_err_not_completed');
  187. if($aData['business'] != $sBusiness)
  188. return array('code' => 4, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_business');
  189. }
  190. if(empty($aData['custom']))
  191. return array('code' => 8, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_custom_data');
  192. list($iVendorId, $iPendingId) = $this->_deconstructCustomData($aData['custom']);
  193. if(empty($iVendorId) || empty($iPendingId))
  194. return array('code' => 8, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_custom_data');
  195. $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $iPendingId));
  196. if((int)$iVendorId != (int)$aPending['seller_id'])
  197. return array('code' => 9, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_vendor');
  198. $aVendor = $this->_oModule->getVendorInfo($iVendorId);
  199. $fAmount = (float)$this->_getReceivedAmount($aVendor['currency_code'], $aData);
  200. if($fAmount != (float)$aPending['amount'])
  201. return array('code' => 10, 'message' => $this->_sLangsPrefix . 'pp_err_wrong_amount');
  202. return array('code' => BX_PAYMENT_RESULT_SUCCESS, 'message' => $this->_sLangsPrefix . 'pp_msg_verified');
  203. }
  204. protected function _readValidationData($sConnectionUrl, $sRequest)
  205. {
  206. $this->log('Validation Request: ');
  207. $this->log($sRequest);
  208. $rConnect = curl_init('https://' . $sConnectionUrl . '/cgi-bin/webscr');
  209. curl_setopt($rConnect, CURLOPT_HTTP_VERSION, CURL_HTTP_VERSION_1_1);
  210. curl_setopt($rConnect, CURLOPT_POST, 1);
  211. curl_setopt($rConnect, CURLOPT_RETURNTRANSFER,1);
  212. curl_setopt($rConnect, CURLOPT_POSTFIELDS, $sRequest);
  213. curl_setopt($rConnect, CURLOPT_SSL_VERIFYPEER, 1);
  214. curl_setopt($rConnect, CURLOPT_SSL_VERIFYHOST, 2);
  215. curl_setopt($rConnect, CURLOPT_FORBID_REUSE, 1);
  216. curl_setopt($rConnect, CURLOPT_HTTPHEADER, array('Connection: Close'));
  217. $sResponse = curl_exec($rConnect);
  218. if(curl_errno($rConnect) == 60) { // CURLE_SSL_CACERT
  219. curl_setopt($rConnect, CURLOPT_CAINFO, BX_DIRECTORY_PATH_PLUGINS . 'curl/cacert/cacert.pem');
  220. $sResponse = curl_exec($rConnect);
  221. }
  222. if(!$sResponse) {
  223. $this->log('Validation Data: ' . curl_error($rConnect));
  224. $this->log($sResponse);
  225. curl_close($rConnect);
  226. return array('code' => 5, 'message' => $this->_sLangsPrefix . 'err_cannot_validate');
  227. }
  228. curl_close($rConnect);
  229. $aResponse = explode("\n", $sResponse);
  230. $this->log('Validation Response: ');
  231. $this->log($aResponse);
  232. return array('code' => 0, 'content' => $aResponse);
  233. }
  234. protected function _getReceivedAmount($sCurrencyCode, &$aResultData)
  235. {
  236. $fAmount = 0.00;
  237. $fTax = isset($aResultData['tax']) ? (float)$aResultData['tax'] : 0.00;
  238. if($aResultData['mc_currency'] == $sCurrencyCode && isset($aResultData['payment_gross']) && !empty($aResultData['payment_gross']))
  239. $fAmount = (float)$aResultData['payment_gross'] - $fTax;
  240. else if($aResultData['mc_currency'] == $sCurrencyCode && isset($aResultData['mc_gross']) && !empty($aResultData['mc_gross']))
  241. $fAmount = (float)$aResultData['mc_gross'] - $fTax;
  242. else if($aResultData['settle_currency'] == $sCurrencyCode && isset($aResultData['settle_amount']) && !empty($aResultData['settle_amount']))
  243. $fAmount = (float)$aResultData['settle_amount'] - $fTax;
  244. return $fAmount;
  245. }
  246. protected function _constructCustomData()
  247. {
  248. $aParams = func_get_args();
  249. return urlencode(base64_encode(implode('|', $aParams)));
  250. }
  251. protected function _deconstructCustomData($data)
  252. {
  253. return explode('|', base64_decode(urldecode($data)));
  254. }
  255. }
  256. /** @} */