Cheetah
ChPmtBitPay.php
Go to the documentation of this file.
1 <?php
2 
8 require_once("ChPmtProvider.php");
9 
10 define('BP_STATUS_NEW', 'new');
11 define('BP_STATUS_PAID', 'paid');
12 define('BP_STATUS_CONFIRMED', 'confirmed');
13 define('BP_STATUS_COMPLETE', 'complete');
14 define('BP_STATUS_EXPIRED', 'expired');
15 define('BP_STATUS_INVALID', 'invalid');
16 
17 define('BP_SPEED_HIGH', 'high');
18 define('BP_SPEED_MEDIUM', 'medium');
19 define('BP_SPEED_LOW', 'low');
20 
22 {
26  function __construct($oDb, $oConfig, $aConfig)
27  {
28  parent::__construct($oDb, $oConfig, $aConfig);
29  $this->_bRedirectOnResult = false;
30 
31  $this->_initializeOptions();
32  }
33 
34  function initializeCheckout($iPendingId, $aCartInfo, $bRecurring = false, $iRecurringDays = 0)
35  {
36  $this->aBpOptions['redirectURL'] .= $aCartInfo['vendor_id'];
37 
38  $this->aBpOptions['notificationURL'] .= $aCartInfo['vendor_id'];
39  $this->aBpOptions['notificationURL'] = ch_append_url_params($this->aBpOptions['notificationURL'], array('bxssl' => 1));
40 
41  switch ($aCartInfo['vendor_currency_code']) {
42  case 'USD':
43  case 'EUR':
44  case 'CAD':
45  case 'AUD':
46  case 'GBP':
47  case 'MXN':
48  case 'NZD':
49  case 'ZAR':
50  case 'BTC':
51  $this->aBpOptions['currency'] = $aCartInfo['vendor_currency_code'];
52  }
53 
54  $aPosData = array(
55  'vnd' => (string)$aCartInfo['vendor_id'],
56  'clt' => (string)$aCartInfo['client_id'],
57  'pnd' => (string)$iPendingId
58  );
59  $aOptions = array(
60  'itemDesc' => 'Payment to ' . $aCartInfo['vendor_profile_name']
61  );
62  $aResponse = $this->createInvoice($iPendingId, (float)$aCartInfo['items_price'], $aPosData, $aOptions);
63  if(!empty($aResponse['error']))
64  return _t(is_array($aResponse['error']) ? $aResponse['error']['message'] : $aResponse['error']);
65 
66  header('Location: ' . $aResponse['url']);
67  exit;
68  }
69 
70  function finalizeCheckout(&$aData)
71  {
72  $aData = $this->_verifyNotification();
73  if($aData === false)
74  return array('code' => 2, 'message' => _t('_payment_bp_err_no_data_given'));
75 
76  if(empty($this->_aOptions) && isset($aData['posData']['d']['pnd'])) {
77  $this->_aOptions = $this->getOptionsByPending($aData['posData']['d']['pnd']);
78  if(empty($this->_aOptions))
79  return array('code' => 3, 'message' => _t('_payment_bp_err_no_vendor_given'));
80 
81  $this->_initializeOptions();
82  }
83 
84  $aPosData = $this->_verifyPosData($aData['posData']);
85  if($aPosData === false)
86  return array('code' => 4, 'message' => _t('_payment_bp_err_incorrect_data'));
87 
88  //--- Update pending transaction ---//
89  $sStatus = $aData['status'];
90  $sMessage = '';
91 
92  $sResult = $this->_verifyAmount($aPosData, $aData['price']);
93  if($sResult === false) {
95  $sMessage = _t('_payment_bp_err_wrong_amount');
96  }
97 
98  $iPendingId = (int)$aPosData['pnd'];
99  $sOrderId = process_db_input($aData['id']);
100  $this->_oDb->updatePending($iPendingId, array(
101  'order' => $sOrderId,
102  'error_code' => $sStatus,
103  'error_msg' => $sMessage
104  ));
105 
106  $aPending = $this->_oDb->getPending(array('type' => 'id', 'id' => $iPendingId));
107  if((int)$aPending['processed'] != 0)
108  return array('code' => 6, 'message' => _t('_payment_bp_err_already_processed'));
109 
110  //--- Process purchased items in the database if STATUS became CONFIRMED (HIGH and MEDIUM speed), COMPLETE (LOW speed)
111  $sSpeed = $this->aBpOptions['transactionSpeed'];
112  if((in_array($sSpeed, array(BP_SPEED_HIGH, BP_SPEED_MEDIUM)) && $sStatus == BP_STATUS_CONFIRMED) || ($sSpeed == BP_SPEED_LOW && $sStatus == BP_STATUS_COMPLETE))
113  return array('code' => 1, 'message' => '', 'pending_id' => $iPendingId);
114 
115  return array('code' => 7, 'message' => _t('_payment_bp_err_no_confirmation_given'));
116  }
117 
118  function checkoutFinished()
119  {
120  return array(
121  'message' => _t('_payment_bp_msg_checkout_finished')
122  );
123  }
124 
134  public function createInvoice($orderId, $price, $posData, $options = array()) {
135  // $orderId: Used to display an orderID to the buyer. In the account summary view, this value is used to
136  // identify a ledger entry if present. Maximum length is 100 characters.
137  //
138  // $price: by default, $price is expressed in the currency you set in Bitpay::aOptions['currency'].
139  //
140  // $posData: this field is included in status updates or requests to get an invoice. It is intended to be used by
141  // the merchant to uniquely identify an order associated with an invoice in their system. Aside from that, Bit-Pay does
142  // not use the data in this field. The data in this field can be anything that is meaningful to the merchant.
143  // Maximum length is 100 characters.
144  //
145  // Note: Using the posData hash option will APPEND the hash to the posData field and could push you over the 100
146  // character limit.
147  //
148  // $options keys can include any of:
149  // 'itemDesc', 'itemCode', 'notificationEmail', 'notificationURL', 'redirectURL', 'apiKey'
150  // 'currency', 'physical', 'fullNotifications', 'transactionSpeed', 'buyerName',
151  // 'buyerAddress1', 'buyerAddress2', 'buyerCity', 'buyerState', 'buyerZip', 'buyerEmail', 'buyerPhone'
152  //
153  // If a given option is not provided here, the value of that option will default to what is found in Bitpay::aOptions
154 
155  try {
156  $options = array_merge($this->aBpOptions, $options); // $options override any options found in Bitpay::aOptions
157  $pos = array('d' => $posData);
158 
159  if ($this->aBpOptions['verifyPos'])
160  $pos['h'] = $this->_hash(serialize($posData), $options['apiKey']);
161 
162  $options['posData'] = json_encode($pos);
163  if(strlen($options['posData']) > 100)
164  return array('error' => '_payment_bp_err_posdata_exceed_limit');
165 
166  $options['orderID'] = $orderId;
167  $options['price'] = $price;
168 
169  $postOptions = array('orderID', 'itemDesc', 'itemCode', 'notificationEmail', 'notificationURL', 'redirectURL',
170  'posData', 'price', 'currency', 'physical', 'fullNotifications', 'transactionSpeed', 'buyerName',
171  'buyerAddress1', 'buyerAddress2', 'buyerCity', 'buyerState', 'buyerZip', 'buyerEmail', 'buyerPhone');
172 
173  foreach($postOptions as $o) {
174  if (array_key_exists($o, $options))
175  $post[$o] = $options[$o];
176  }
177  $post = json_encode($post);
178 
179  $response = $this->_curl('https://bitpay.com/api/invoice/', $options['apiKey'], $post);
180 
181  if($this->aBpOptions['useLogging']) {
182  $this->_log('Create Invoice: ');
183  $this->_log('-- Data: ' . $post);
184  $this->_log('Response: ');
185  $this->_log($response);
186  }
187 
188  return $response;
189 
190  }
191  catch (Exception $e) {
192  if($this->aBpOptions['useLogging'])
193  $this->_log('Error: ' . $e->getMessage());
194 
195  return array('error' => $e->getMessage());
196  }
197  }
198 
208  public function getInvoice($invoiceId, $apiKey=false) {
209  try {
210  if (!$apiKey)
211  $apiKey = $this->aBpOptions['apiKey'];
212 
213  $response = $this->_curl('https://bitpay.com/api/invoice/'.$invoiceId, $apiKey);
214 
215  if (is_string($response))
216  return $response; // error
217 
218  $response['posData'] = json_decode($response['posData'], true);
219  $response['posData'] = $response['posData']['d'];
220 
221  return $response;
222  }
223  catch (Exception $e) {
224  if($this->aBpOptions['useLogging'])
225  $this->_log('Error: ' . $e->getMessage());
226 
227  return 'Error: ' . $e->getMessage();
228  }
229  }
230 
241  function getCurrencyList() {
242  $currencies = array();
243  $rate_url = 'https://bitpay.com/api/rates';
244 
245  try {
246  $clist = json_decode(file_get_contents($rate_url),true);
247 
248  foreach($clist as $key => $value)
249  $currencies[$value['code']] = $value['name'];
250 
251  return $currencies;
252  }
253  catch (Exception $e) {
254  if($this->aBpOptions['useLogging'])
255  $this->_log('Error: ' . $e->getMessage());
256 
257  return 'Error: ' . $e->getMessage();
258  }
259  }
260 
273  public function getRate($code = 'USD') {
274  $rate_url = 'https://bitpay.com/api/rates';
275 
276  try {
277  $clist = json_decode(file_get_contents($rate_url),true);
278 
279  foreach($clist as $key => $value) {
280  if($value['code'] == $code)
281  $rate = number_format($value['rate'], 2, '.', '');
282  }
283 
284  return $rate;
285  }
286  catch (Exception $e) {
287  if($this->aBpOptions['useLogging'])
288  $this->_log('Error: ' . $e->getMessage());
289  return 'Error: ' . $e->getMessage();
290  }
291  }
292 
293  protected function _initializeOptions() {
294  // REQUIRED Api key you created at bitpay.com
295  $this->aBpOptions['apiKey'] = $this->getOption('api_key');
296 
297  // whether to verify POS data by hashing above api key. If set to false, you should
298  // have some way of verifying that callback data comes from bitpay.com
299  // note: this option can only be changed here. It cannot be set dynamically.
300  $this->aBpOptions['verifyPos'] = true;
301 
302  // email where invoice update notifications should be sent
303  $this->aBpOptions['notificationEmail'] = $this->getOption('notification_email');
304 
305  // url where bit-pay server should send update notifications. See API doc for more details.
306  // example: $bpNotificationUrl = 'http://www.example.com/callback.php';
307  $this->aBpOptions['notificationURL'] = $this->_oConfig->getDataReturnUrl(true) . $this->_sName . '/';
308 
309  // url where the customer should be directed to after paying for the order
310  // example: $bpNotificationUrl = 'http://www.example.com/confirmation.php';
311  $this->aBpOptions['redirectURL'] = CH_WSB_URL_ROOT . $this->_oConfig->getBaseUri() . 'act_checkout_finished/' . $this->_sName . '/';
312 
313  // This is the currency used for the price setting. A list of other pricing
314  // currencies supported is found at bitpay.com
315  $this->aBpOptions['currency'] = 'BTC';
316 
317  // Indicates whether anything is to be shipped with the order
318  // (if false, the buyer will be informed that nothing is to be shipped)
319  $this->aBpOptions['physical'] = false;
320 
321  // If set to false, then notificaitions are only
322  // sent when an invoice is confirmed (according the the
323  // transactionSpeed setting). If set to true, then a notification
324  // will be sent on every status change
325  $this->aBpOptions['fullNotifications'] = $this->getOption('full_notifications') == 'on';
326 
327  // transaction speed: low/medium/high. See API docs for more details.
328  $this->aBpOptions['transactionSpeed'] = $this->getOption('transaction_speed');
329 
330  // Logfile for use by the bpLog function. Note: ensure the web server process has write access
331  // to this file and/or directory!
332  $this->aBpOptions['logFile'] = $GLOBALS['dir']['tmp'] . 'ch_payment_bp.log';
333 
334  // Change to 'true' if you would like automatic logging of invoices and errors.
335  // Otherwise you will have to call the bpLog function manually to log any information.
336  $this->aBpOptions['useLogging'] = true;
337  }
338 
348  protected function _verifyNotification() {
349  try {
350  $this->_log('Notification received: ' . date("m.d.y H:i:s"));
351 
352  $post = file_get_contents("php://input");
353  if(!$post) {
354  $this->_log('Error: No post data');
355  return false;
356  }
357 
358  $json = json_decode($post, true);
359  $this->_log('-- Data: ' . $post);
360  $this->_log($json);
361 
362  if(is_string($json))
363  return false;
364 
365  if(!array_key_exists('posData', $json)) {
366  $this->_log('Error: No posData');
367  return false;
368  }
369 
370  $json['posData'] = json_decode($json['posData'], true);
371  if(empty($json['posData']) || !is_array($json['posData'])) {
372  $this->_log('Error: Empty posData');
373  return false;
374  }
375 
376  return $json;
377  }
378  catch (Exception $e) {
379  if($this->aBpOptions['useLogging'])
380  $this->_log('Error: ' . $e->getMessage());
381 
382  return false;
383  }
384  }
385 
394  protected function _verifyPosData($aPosData) {
395  if(!$this->aBpOptions['verifyPos']) {
396  if(empty($aPosData['d']) || !is_array($aPosData['d'])) {
397  $this->_log('Error: Payment data cannot be found.');
398  return false;
399  }
400 
401  return $aPosData['d'];
402  }
403 
404  if($this->_hash(serialize($aPosData['d']), $this->aBpOptions['apiKey']) != $aPosData['h']) {
405  $this->_log('Error: Authentication failed (bad posData hash).');
406  return false;
407  }
408 
409  return $aPosData['d'];
410  }
411 
412  protected function _verifyAmount($aPosData, $fAmount) {
413  $iPendingId = (int)$aPosData['pnd'];
414 
415  $aPending = $this->_oDb->getPending(array('type' => 'id', 'id' => $iPendingId));
416  if(empty($aPending) || !is_array($aPending) || $fAmount != (float)$aPending['amount'])
417  return false;
418 
419  return true;
420  }
421 
432  protected function _decodeResponse($response) {
433  try {
434  if (empty($response) || !(is_string($response)))
435  return array('error' => 'ChPmtBitPay::_decodeResponse expects a string parameter.');
436 
437  return json_decode($response, true);
438  }
439  catch (Exception $e) {
440  if($this->aBpOptions['useLogging'])
441  $this->_log('Error: ' . $e->getMessage());
442 
443  return array('error' => $e->getMessage());
444  }
445  }
446 
456  protected function _hash($data, $key) {
457  try {
458  $hmac = base64_encode(hash_hmac('sha256', $data, $key, TRUE));
459  return strtr($hmac, array('+' => '-', '/' => '_', '=' => ''));
460  }
461  catch (Exception $e) {
462  if($this->aBpOptions['useLogging'])
463  $this->_log('Error: ' . $e->getMessage());
464 
465  return 'Error: ' . $e->getMessage();
466  }
467  }
468 
478  protected function _curl($url, $apiKey, $post = false) {
479  if(!isset($url) || trim($url) == '' || !isset($apiKey) || trim($apiKey) == '') {
480  // Invalid parameter specified
481  if($this->aBpOptions['useLogging'])
482  $this->_log('Error: You must supply non-empty url and apiKey parameters.');
483 
484  return array('error' => 'You must supply non-empty url and apiKey parameters.');
485  }
486 
487  try {
488  $curl = curl_init();
489  $length = 0;
490 
491  if ($post) {
492  curl_setopt($curl, CURLOPT_POST, 1);
493  curl_setopt($curl, CURLOPT_POSTFIELDS, $post);
494  $length = strlen($post);
495  }
496 
497  $uname = base64_encode($apiKey);
498 
499  if($uname) {
500  $header = array(
501  'Content-Type: application/json',
502  'Content-Length: ' . $length,
503  'Authorization: Basic ' . $uname,
504  );
505 
506  curl_setopt($curl, CURLOPT_URL, $url);
507  curl_setopt($curl, CURLOPT_PORT, 443);
508  curl_setopt($curl, CURLOPT_HTTPHEADER, $header);
509  curl_setopt($curl, CURLOPT_TIMEOUT, 10);
510  curl_setopt($curl, CURLOPT_HTTPAUTH, CURLAUTH_BASIC ) ;
511  curl_setopt($curl, CURLOPT_SSL_VERIFYPEER, 1); // verify certificate
512  curl_setopt($curl, CURLOPT_SSL_VERIFYHOST, 2); // check existence of CN and verify that it matches hostname
513  curl_setopt($curl, CURLOPT_RETURNTRANSFER, 1);
514  curl_setopt($curl, CURLOPT_FORBID_REUSE, 1);
515  curl_setopt($curl, CURLOPT_FRESH_CONNECT, 1);
516 
517  $responseString = curl_exec($curl);
518 
519  if($responseString == false) {
520  $response = array('error' => curl_error($curl));
521  if($this->aBpOptions['useLogging'])
522  $this->_log('Error: ' . curl_error($curl));
523  }
524  else {
525  $response = json_decode($responseString, true);
526  if (!$response) {
527  $response = array('error' => 'invalid json: '.$responseString);
528  if($this->aBpOptions['useLogging'])
529  $this->_log('Error - Invalid JSON: ' . $responseString);
530  }
531  }
532 
533  curl_close($curl);
534  return $response;
535  }
536  else {
537  curl_close($curl);
538 
539  if($this->aBpOptions['useLogging'])
540  $this->_log('Invalid data found in apiKey value passed to Bitpay::curl method. (Failed: base64_encode(apikey))');
541 
542  return array('error' => 'Invalid data found in apiKey value passed to Bitpay::curl method. (Failed: base64_encode(apikey))');
543  }
544  }
545  catch (Exception $e) {
546  @curl_close($curl);
547  if($this->aBpOptions['useLogging'])
548  $this->_log('Error: ' . $e->getMessage());
549  return array('error' => $e->getMessage());
550  }
551  }
552 
563  protected function _log($contents) {
564  try {
565  if(isset($this->aBpOptions['logFile']) && $this->aBpOptions['logFile'] != '') {
566  $file = $this->aBpOptions['logFile'];
567  }
568  else {
569  // Fallback to using a default logfile name in case the variable is
570  // missing or not set.
571  $file = dirname(__FILE__) . '/bplog.txt';
572  }
573 
574  file_put_contents($file, date('m-d H:i:s').": ", FILE_APPEND);
575 
576  if (is_array($contents))
577  $contents = var_export($contents, true);
578  else if (is_object($contents))
579  $contents = json_encode($contents);
580 
581  file_put_contents($file, $contents."\n", FILE_APPEND);
582  }
583  catch (Exception $e) {
584  echo 'Error: ' . $e->getMessage();
585  }
586  }
587 }
process_db_input
process_db_input($sText, $iStripTags=0)
Definition: utils.inc.php:256
header
</code > Be careful enabling this directive if you have a redirector script that does not use the< code > Location</code > HTTP header
Definition: URI.MungeResources.txt:10
ChPmtBitPay\_log
_log($contents)
Definition: ChPmtBitPay.php:563
$sMessage
$sMessage
Definition: actions.inc.php:17
ChPmtProvider
Definition: ChPmtProvider.php:9
BP_STATUS_COMPLETE
const BP_STATUS_COMPLETE
Definition: ChPmtBitPay.php:13
ChPmtBitPay\__construct
__construct($oDb, $oConfig, $aConfig)
Definition: ChPmtBitPay.php:26
TRUE
URI MungeSecretKey $secret_key</pre >< p > If the output is TRUE
Definition: URI.MungeSecretKey.txt:17
ChPmtBitPay\getCurrencyList
getCurrencyList()
Definition: ChPmtBitPay.php:241
$sResult
$sResult
Definition: advanced_settings.php:26
ChPmtBitPay\_decodeResponse
_decodeResponse($response)
Definition: ChPmtBitPay.php:432
ChPmtBitPay\_hash
_hash($data, $key)
Definition: ChPmtBitPay.php:456
ChPmtBitPay\_initializeOptions
_initializeOptions()
Definition: ChPmtBitPay.php:293
php
$url
URI MungeSecretKey $url
Definition: URI.MungeSecretKey.txt:14
ChPmtBitPay\_verifyPosData
_verifyPosData($aPosData)
Definition: ChPmtBitPay.php:394
BP_SPEED_MEDIUM
const BP_SPEED_MEDIUM
Definition: ChPmtBitPay.php:18
BP_SPEED_HIGH
const BP_SPEED_HIGH
Definition: ChPmtBitPay.php:17
exit
exit
Definition: cart.php:21
BP_SPEED_LOW
const BP_SPEED_LOW
Definition: ChPmtBitPay.php:19
ChPmtBitPay\createInvoice
createInvoice($orderId, $price, $posData, $options=array())
Definition: ChPmtBitPay.php:134
BP_STATUS_INVALID
const BP_STATUS_INVALID
Definition: ChPmtBitPay.php:15
ChPmtProvider\getOptionsByPending
getOptionsByPending($iPendingId)
Definition: ChPmtProvider.php:47
ch_append_url_params
ch_append_url_params($sUrl, $mixedParams)
Definition: utils.inc.php:1697
ChPmtBitPay\checkoutFinished
checkoutFinished()
Definition: ChPmtBitPay.php:118
$oDb
global $oDb
Definition: db.inc.php:39
ChPmtBitPay
Definition: ChPmtBitPay.php:22
ChPmtBitPay\_verifyNotification
_verifyNotification()
Definition: ChPmtBitPay.php:348
ChPmtBitPay\_curl
_curl($url, $apiKey, $post=false)
Definition: ChPmtBitPay.php:478
_t
_t($key, $arg0="", $arg1="", $arg2="")
Definition: languages.inc.php:509
BP_STATUS_CONFIRMED
const BP_STATUS_CONFIRMED
Definition: ChPmtBitPay.php:12
ChPmtBitPay\getInvoice
getInvoice($invoiceId, $apiKey=false)
Definition: ChPmtBitPay.php:208
$aConfig
$aConfig
Definition: config.php:8
ChPmtBitPay\finalizeCheckout
finalizeCheckout(&$aData)
Definition: ChPmtBitPay.php:70
ChPmtBitPay\getRate
getRate($code='USD')
Definition: ChPmtBitPay.php:273
empty
Attr AllowedRel this is empty
Definition: Attr.AllowedRel.txt:7
$o
$o
Definition: cmd.php:193
ChPmtBitPay\initializeCheckout
initializeCheckout($iPendingId, $aCartInfo, $bRecurring=false, $iRecurringDays=0)
Definition: ChPmtBitPay.php:34
as
as
Definition: Filter.ExtractStyleBlocks.Escaping.txt:10
ChPmtBitPay\_verifyAmount
_verifyAmount($aPosData, $fAmount)
Definition: ChPmtBitPay.php:412
$sStatus
$sStatus
Definition: actions.inc.php:11
$GLOBALS
$GLOBALS['iAdminPage']
Definition: advanced_settings.php:10
ChPmtProvider\getOption
getOption($sName)
Definition: ChPmtProvider.php:55