1
0

BxPaymentProviderBitPay.php 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552
  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. define('BP_STATUS_NEW', 'new');
  12. define('BP_STATUS_PAID', 'paid');
  13. define('BP_STATUS_CONFIRMED', 'confirmed');
  14. define('BP_STATUS_COMPLETE', 'complete');
  15. define('BP_STATUS_EXPIRED', 'expired');
  16. define('BP_STATUS_INVALID', 'invalid');
  17. define('BP_SPEED_HIGH', 'high');
  18. define('BP_SPEED_MEDIUM', 'medium');
  19. define('BP_SPEED_LOW', 'low');
  20. class BxPaymentProviderBitPay extends BxBaseModPaymentProvider implements iBxBaseModPaymentProvider
  21. {
  22. function __construct($aConfig)
  23. {
  24. $this->MODULE = 'bx_payment';
  25. parent::__construct($aConfig);
  26. $this->_bRedirectOnResult = false;
  27. $this->_sLogFile = BX_DIRECTORY_PATH_LOGS . 'bx_pp_' . $this->_sName . '.log';
  28. $this->_initializeOptions();
  29. }
  30. public function initializeCheckout($iPendingId, $aCartInfo)
  31. {
  32. $this->aBpOptions['redirectURL'] .= $aCartInfo['vendor_id'];
  33. $this->aBpOptions['notificationURL'] .= $aCartInfo['vendor_id'];
  34. $this->aBpOptions['notificationURL'] = bx_append_url_params($this->aBpOptions['notificationURL'], array('bxssl' => 1));
  35. switch ($aCartInfo['vendor_currency_code']) {
  36. case 'USD':
  37. case 'EUR':
  38. case 'BTC':
  39. $this->aBpOptions['currency'] = $aCartInfo['vendor_currency_code'];
  40. }
  41. $aPosData = array(
  42. 'vnd' => (string)$aCartInfo['vendor_id'],
  43. 'clt' => (string)$aCartInfo['client_id'],
  44. 'pnd' => (string)$iPendingId
  45. );
  46. $aOptions = array(
  47. 'itemDesc' => 'Payment to ' . $aCartInfo['vendor_profile_name']
  48. );
  49. $aResponse = $this->createInvoice($iPendingId, (float)$aCartInfo['items_price'], $aPosData, $aOptions);
  50. if(!empty($aResponse['error']))
  51. return _t(is_array($aResponse['error']) ? $aResponse['error']['message'] : $aResponse['error']);
  52. header('Location: ' . $aResponse['url']);
  53. exit;
  54. }
  55. public function finalizeCheckout(&$aData)
  56. {
  57. $aData = $this->_verifyNotification();
  58. if($aData === false)
  59. return array('code' => 2, 'message' => _t('_payment_bp_err_no_data_given'));
  60. if(empty($this->_aOptions) && isset($aData['posData']['d']['pnd'])) {
  61. $this->_aOptions = $this->getOptionsByPending($aData['posData']['d']['pnd']);
  62. if(empty($this->_aOptions))
  63. return array('code' => 3, 'message' => _t('_payment_bp_err_no_vendor_given'));
  64. $this->_initializeOptions();
  65. }
  66. $aPosData = $this->_verifyPosData($aData['posData']);
  67. if($aPosData === false)
  68. return array('code' => 4, 'message' => _t('_payment_bp_err_incorrect_data'));
  69. //--- Update pending transaction ---//
  70. $sStatus = $aData['status'];
  71. $sMessage = '';
  72. $sResult = $this->_verifyAmount($aPosData, $aData['price']);
  73. if($sResult === false) {
  74. $sStatus = BP_STATUS_INVALID;
  75. $sMessage = _t('_payment_bp_err_wrong_amount');
  76. }
  77. $iPendingId = (int)$aPosData['pnd'];
  78. $sOrderId = Bx_process_input($aData['id']);
  79. $this->_oModule->_oDb->updateOrderPending($iPendingId, array(
  80. 'order' => $sOrderId,
  81. 'error_code' => $sStatus,
  82. 'error_msg' => $sMessage
  83. ));
  84. $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $iPendingId));
  85. if((int)$aPending['processed'] != 0)
  86. return array('code' => 6, 'message' => _t('_payment_bp_err_already_processed'));
  87. //--- Process purchased items in the database if STATUS became CONFIRMED (HIGH and MEDIUM speed), COMPLETE (LOW speed)
  88. $sSpeed = $this->aBpOptions['transactionSpeed'];
  89. if(!((in_array($sSpeed, array(BP_SPEED_HIGH, BP_SPEED_MEDIUM)) && $sStatus == BP_STATUS_CONFIRMED) || ($sSpeed == BP_SPEED_LOW && $sStatus == BP_STATUS_COMPLETE)))
  90. return array('code' => 7, 'message' => _t('_payment_bp_err_no_confirmation_given'));
  91. return array(
  92. 'code' => BX_PAYMENT_RESULT_SUCCESS,
  93. 'message' => '',
  94. 'pending_id' => $iPendingId,
  95. 'paid' => true
  96. );
  97. }
  98. public function finalizedCheckout()
  99. {
  100. return array(
  101. 'message' => _t('_payment_bp_msg_checkout_finished')
  102. );
  103. }
  104. /**
  105. *
  106. * Creates BitPay invoice via Bitpay::curl.
  107. *
  108. * @param string $orderId, string $price, string $posData, array $options
  109. * @return array $response
  110. * @throws Exception $e
  111. *
  112. */
  113. public function createInvoice($orderId, $price, $posData, $options = array()) {
  114. // $orderId: Used to display an orderID to the buyer. In the account summary view, this value is used to
  115. // identify a ledger entry if present. Maximum length is 100 characters.
  116. //
  117. // $price: by default, $price is expressed in the currency you set in Bitpay::aOptions['currency'].
  118. //
  119. // $posData: this field is included in status updates or requests to get an invoice. It is intended to be used by
  120. // the merchant to uniquely identify an order associated with an invoice in their system. Aside from that, Bit-Pay does
  121. // not use the data in this field. The data in this field can be anything that is meaningful to the merchant.
  122. // Maximum length is 100 characters.
  123. //
  124. // Note: Using the posData hash option will APPEND the hash to the posData field and could push you over the 100
  125. // character limit.
  126. //
  127. // $options keys can include any of:
  128. // 'itemDesc', 'itemCode', 'notificationEmail', 'notificationURL', 'redirectURL', 'apiKey'
  129. // 'currency', 'physical', 'fullNotifications', 'transactionSpeed', 'buyerName',
  130. // 'buyerAddress1', 'buyerAddress2', 'buyerCity', 'buyerState', 'buyerZip', 'buyerEmail', 'buyerPhone'
  131. //
  132. // If a given option is not provided here, the value of that option will default to what is found in Bitpay::aOptions
  133. try {
  134. $options = array_merge($this->aBpOptions, $options); // $options override any options found in Bitpay::aOptions
  135. $pos = array('d' => $posData);
  136. if ($this->aBpOptions['verifyPos'])
  137. $pos['h'] = $this->_hash(serialize($posData), $options['apiKey']);
  138. $options['posData'] = json_encode($pos);
  139. if(strlen($options['posData']) > 100)
  140. return array('error' => '_payment_bp_err_posdata_exceed_limit');
  141. $options['orderID'] = $orderId;
  142. $options['price'] = $price;
  143. $postOptions = array('orderID', 'itemDesc', 'itemCode', 'notificationEmail', 'notificationURL', 'redirectURL',
  144. 'posData', 'price', 'currency', 'physical', 'fullNotifications', 'transactionSpeed', 'buyerName',
  145. 'buyerAddress1', 'buyerAddress2', 'buyerCity', 'buyerState', 'buyerZip', 'buyerEmail', 'buyerPhone');
  146. foreach($postOptions as $o) {
  147. if (array_key_exists($o, $options))
  148. $post[$o] = $options[$o];
  149. }
  150. $post = json_encode($post);
  151. $response = $this->_curl('https://bitpay.com/api/invoice/', $options['apiKey'], $post);
  152. if($this->aBpOptions['useLogging']) {
  153. $this->log('Create Invoice: ');
  154. $this->log('-- Data: ' . $post);
  155. $this->log('Response: ');
  156. $this->log($response);
  157. }
  158. return $response;
  159. }
  160. catch (Exception $e) {
  161. if($this->aBpOptions['useLogging'])
  162. $this->log('Error: ' . $e->getMessage());
  163. return array('error' => $e->getMessage());
  164. }
  165. }
  166. /**
  167. *
  168. * Retrieves an invoice from BitPay. $options can include 'apiKey'
  169. *
  170. * @param string $invoiceId, boolean $apiKey
  171. * @return mixed $json
  172. * @throws Exception $e
  173. *
  174. */
  175. public function getInvoice($invoiceId, $apiKey=false) {
  176. try {
  177. if (!$apiKey)
  178. $apiKey = $this->aBpOptions['apiKey'];
  179. $response = $this->_curl('https://bitpay.com/api/invoice/'.$invoiceId, $apiKey);
  180. if (is_string($response))
  181. return $response; // error
  182. $response['posData'] = json_decode($response['posData'], true);
  183. $response['posData'] = $response['posData']['d'];
  184. return $response;
  185. }
  186. catch (Exception $e) {
  187. if($this->aBpOptions['useLogging'])
  188. $this->log('Error: ' . $e->getMessage());
  189. return 'Error: ' . $e->getMessage();
  190. }
  191. }
  192. /**
  193. *
  194. * Retrieves a list of all supported currencies
  195. * and returns associative array.
  196. *
  197. * @param none
  198. * @return array $currencies
  199. * @throws Exception $e
  200. *
  201. */
  202. function getCurrencyList() {
  203. $currencies = array();
  204. $rate_url = 'https://bitpay.com/api/rates';
  205. try {
  206. $clist = json_decode(file_get_contents($rate_url),true);
  207. foreach($clist as $key => $value)
  208. $currencies[$value['code']] = $value['name'];
  209. return $currencies;
  210. }
  211. catch (Exception $e) {
  212. if($this->aBpOptions['useLogging'])
  213. $this->log('Error: ' . $e->getMessage());
  214. return 'Error: ' . $e->getMessage();
  215. }
  216. }
  217. /**
  218. *
  219. * Retrieves the current rate based on $code.
  220. * The default code us USD, so calling the
  221. * function without a parameter will return
  222. * the current BTC/USD price.
  223. *
  224. * @param string $code
  225. * @return string $rate
  226. * @throws Exception $e
  227. *
  228. */
  229. public function getRate($code = 'USD') {
  230. $rate_url = 'https://bitpay.com/api/rates';
  231. try {
  232. $clist = json_decode(file_get_contents($rate_url),true);
  233. foreach($clist as $key => $value) {
  234. if($value['code'] == $code)
  235. $rate = number_format($value['rate'], 2, '.', '');
  236. }
  237. return $rate;
  238. }
  239. catch (Exception $e) {
  240. if($this->aBpOptions['useLogging'])
  241. $this->log('Error: ' . $e->getMessage());
  242. return 'Error: ' . $e->getMessage();
  243. }
  244. }
  245. protected function _initializeOptions() {
  246. // REQUIRED Api key you created at bitpay.com
  247. $this->aBpOptions['apiKey'] = $this->getOption('api_key');
  248. // whether to verify POS data by hashing above api key. If set to false, you should
  249. // have some way of verifying that callback data comes from bitpay.com
  250. // note: this option can only be changed here. It cannot be set dynamically.
  251. $this->aBpOptions['verifyPos'] = true;
  252. // email where invoice update notifications should be sent
  253. $this->aBpOptions['notificationEmail'] = $this->getOption('notification_email');
  254. // url where bit-pay server should send update notifications. See API doc for more details.
  255. // example: $bpNotificationUrl = 'http://www.example.com/callback.php';
  256. $this->aBpOptions['notificationURL'] = $this->_oModule->_oConfig->getUrl('URL_RETURN_DATA', array(), true) . $this->_sName . '/';
  257. // url where the customer should be directed to after paying for the order
  258. // example: $bpNotificationUrl = 'http://www.example.com/confirmation.php';
  259. $this->aBpOptions['redirectURL'] = BX_DOL_URL_ROOT . $this->_oModule->_oConfig->getBaseUri() . 'act_checkout_finished/' . $this->_sName . '/';
  260. // This is the currency used for the price setting. A list of other pricing
  261. // currencies supported is found at bitpay.com
  262. $this->aBpOptions['currency'] = 'BTC';
  263. // Indicates whether anything is to be shipped with the order
  264. // (if false, the buyer will be informed that nothing is to be shipped)
  265. $this->aBpOptions['physical'] = false;
  266. // If set to false, then notificaitions are only
  267. // sent when an invoice is confirmed (according the the
  268. // transactionSpeed setting). If set to true, then a notification
  269. // will be sent on every status change
  270. $this->aBpOptions['fullNotifications'] = $this->getOption('full_notifications') == 'on';
  271. // transaction speed: low/medium/high. See API docs for more details.
  272. $this->aBpOptions['transactionSpeed'] = $this->getOption('transaction_speed');
  273. // Change to 'true' if you would like automatic logging of invoices and errors.
  274. // Otherwise you will have to call the bpLog function manually to log any information.
  275. $this->aBpOptions['useLogging'] = true;
  276. }
  277. /**
  278. *
  279. * Call from your notification handler to convert $_POST data to an object containing invoice data
  280. *
  281. * @param boolean $apiKey
  282. * @return mixed $json
  283. * @throws Exception $e
  284. *
  285. */
  286. protected function _verifyNotification() {
  287. try {
  288. $this->log('Notification received: ' . date("m.d.y H:i:s"));
  289. $post = file_get_contents("php://input");
  290. if(!$post) {
  291. $this->log('Error: No post data');
  292. return false;
  293. }
  294. $json = json_decode($post, true);
  295. $this->log('-- Data: ' . $post);
  296. $this->log($json);
  297. if(is_string($json))
  298. return false;
  299. if(!array_key_exists('posData', $json)) {
  300. $this->log('Error: No posData');
  301. return false;
  302. }
  303. $json['posData'] = json_decode($json['posData'], true);
  304. if(empty($json['posData']) || !is_array($json['posData'])) {
  305. $this->log('Error: Empty posData');
  306. return false;
  307. }
  308. return $json;
  309. }
  310. catch (Exception $e) {
  311. if($this->aBpOptions['useLogging'])
  312. $this->log('Error: ' . $e->getMessage());
  313. return false;
  314. }
  315. }
  316. /**
  317. *
  318. * Call from your notification handler to verify posData
  319. *
  320. * @param array $aPosData
  321. * @return boolean
  322. *
  323. */
  324. protected function _verifyPosData($aPosData) {
  325. if(!$this->aBpOptions['verifyPos']) {
  326. if(empty($aPosData['d']) || !is_array($aPosData['d'])) {
  327. $this->log('Error: Payment data cannot be found.');
  328. return false;
  329. }
  330. return $aPosData['d'];
  331. }
  332. if($this->_hash(serialize($aPosData['d']), $this->aBpOptions['apiKey']) != $aPosData['h']) {
  333. $this->log('Error: Authentication failed (bad posData hash).');
  334. return false;
  335. }
  336. return $aPosData['d'];
  337. }
  338. protected function _verifyAmount($aPosData, $fAmount) {
  339. $iPendingId = (int)$aPosData['pnd'];
  340. $aPending = $this->_oModule->_oDb->getOrderPending(array('type' => 'id', 'id' => $iPendingId));
  341. if(empty($aPending) || !is_array($aPending) || $fAmount != (float)$aPending['amount'])
  342. return false;
  343. return true;
  344. }
  345. /**
  346. *
  347. * Decodes JSON response and returns
  348. * associative array.
  349. *
  350. * @param string $response
  351. * @return array $arrResponse
  352. * @throws Exception $e
  353. *
  354. */
  355. protected function _decodeResponse($response) {
  356. try {
  357. if (empty($response) || !(is_string($response)))
  358. return array('error' => 'BxPmtBitPay::_decodeResponse expects a string parameter.');
  359. return json_decode($response, true);
  360. }
  361. catch (Exception $e) {
  362. if($this->aBpOptions['useLogging'])
  363. $this->log('Error: ' . $e->getMessage());
  364. return array('error' => $e->getMessage());
  365. }
  366. }
  367. /**
  368. *
  369. * Generates a base64 encoded keyed hash.
  370. *
  371. * @param string $data, string $key
  372. * @return string $hmac
  373. * @throws Exception $e
  374. *
  375. */
  376. protected function _hash($data, $key) {
  377. try {
  378. $hmac = base64_encode(hash_hmac('sha256', $data, $key, TRUE));
  379. return strtr($hmac, array('+' => '-', '/' => '_', '=' => ''));
  380. }
  381. catch (Exception $e) {
  382. if($this->aBpOptions['useLogging'])
  383. $this->log('Error: ' . $e->getMessage());
  384. return 'Error: ' . $e->getMessage();
  385. }
  386. }
  387. /**
  388. *
  389. * Handles post/get to BitPay via curl.
  390. *
  391. * @param string $url, string $apiKey, boolean $post
  392. * @return mixed $response
  393. * @throws Exception $e
  394. *
  395. */
  396. protected function _curl($url, $apiKey, $post = false) {
  397. if(!isset($url) || trim($url) == '' || !isset($apiKey) || trim($apiKey) == '') {
  398. // Invalid parameter specified
  399. if($this->aBpOptions['useLogging'])
  400. $this->log('Error: You must supply non-empty url and apiKey parameters.');
  401. return array('error' => 'You must supply non-empty url and apiKey parameters.');
  402. }
  403. try {
  404. $curl = curl_init();
  405. $length = 0;
  406. if ($post) {
  407. curl_setopt($curl, CURLOPT_POST, 1);
  408. curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
  409. $length = strlen($post);
  410. }
  411. $uname = base64_encode($apiKey);
  412. if($uname) {
  413. $header = array(
  414. 'Content-Type: application/json',
  415. 'Content-Length: ' . $length,
  416. 'Authorization: Basic ' . $uname,
  417. );
  418. curl_setopt($curl, CURLOPT_URL, $url);
  419. curl_setopt($curl, CURLOPT_PORT, 443);
  420. curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
  421. curl_setopt($curl, CURLOPT_TIMEOUT, 10);
  422. curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC ) ;
  423. curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); // verify certificate
  424. curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); // check existence of CN and verify that it matches hostname
  425. curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
  426. curl_setopt($curl, CURLOPT_FORBID_REUSE, 1);
  427. curl_setopt($curl, CURLOPT_FRESH_CONNECT, 1);
  428. $responseString = curl_exec($curl);
  429. if($responseString == false) {
  430. $response = array('error' => curl_error($curl));
  431. if($this->aBpOptions['useLogging'])
  432. $this->log('Error: ' . curl_error($curl));
  433. }
  434. else {
  435. $response = json_decode($responseString, true);
  436. if (!$response) {
  437. $response = array('error' => 'invalid json: '.$responseString);
  438. if($this->aBpOptions['useLogging'])
  439. $this->log('Error - Invalid JSON: ' . $responseString);
  440. }
  441. }
  442. curl_close($curl);
  443. return $response;
  444. }
  445. else {
  446. curl_close($curl);
  447. if($this->aBpOptions['useLogging'])
  448. $this->log('Invalid data found in apiKey value passed to Bitpay::curl method. (Failed: base64_encode(apikey))');
  449. return array('error' => 'Invalid data found in apiKey value passed to Bitpay::curl method. (Failed: base64_encode(apikey))');
  450. }
  451. }
  452. catch (Exception $e) {
  453. @curl_close($curl);
  454. if($this->aBpOptions['useLogging'])
  455. $this->log('Error: ' . $e->getMessage());
  456. return array('error' => $e->getMessage());
  457. }
  458. }
  459. }
  460. /** @} */