diff --git a/include/config/config.xml b/include/config/config.xml index e193e6f6d..c06053dd2 100755 --- a/include/config/config.xml +++ b/include/config/config.xml @@ -138,12 +138,12 @@ - ***REMOVED*** - ***REMOVED*** + _KEY_ + _KEY_ - ***REMOVED*** - ***REMOVED*** + _KEY_ + _KEY_ diff --git a/include/crunchbutton.php b/include/crunchbutton.php index eb16fe455..e21389316 100755 --- a/include/crunchbutton.php +++ b/include/crunchbutton.php @@ -100,6 +100,7 @@ spl_autoload_register(function ($className) { \Buzz\Bootstrap::init(); \Github\Bootstrap::init(); \Mailgun\Bootstrap::init(); +\Stripe\Bootstrap::init(); $configFile = $GLOBALS['config']['dirs']['config'].'config.demo.xml'; if (file_exists($GLOBALS['config']['dirs']['config'].'config.xml')) { diff --git a/include/library/Cana/Stripe.php b/include/library/Cana/_Stripe.php similarity index 100% rename from include/library/Cana/Stripe.php rename to include/library/Cana/_Stripe.php diff --git a/include/library/Cana/Stripe/Account.php b/include/library/Cana/_Stripe/Account.php similarity index 100% rename from include/library/Cana/Stripe/Account.php rename to include/library/Cana/_Stripe/Account.php diff --git a/include/library/Cana/Stripe/ApiConnectionError.php b/include/library/Cana/_Stripe/ApiConnectionError.php similarity index 100% rename from include/library/Cana/Stripe/ApiConnectionError.php rename to include/library/Cana/_Stripe/ApiConnectionError.php diff --git a/include/library/Cana/Stripe/ApiError.php b/include/library/Cana/_Stripe/ApiError.php similarity index 100% rename from include/library/Cana/Stripe/ApiError.php rename to include/library/Cana/_Stripe/ApiError.php diff --git a/include/library/Cana/Stripe/ApiRequestor.php b/include/library/Cana/_Stripe/ApiRequestor.php similarity index 100% rename from include/library/Cana/Stripe/ApiRequestor.php rename to include/library/Cana/_Stripe/ApiRequestor.php diff --git a/include/library/Cana/Stripe/ApiResource.php b/include/library/Cana/_Stripe/ApiResource.php similarity index 100% rename from include/library/Cana/Stripe/ApiResource.php rename to include/library/Cana/_Stripe/ApiResource.php diff --git a/include/library/Cana/Stripe/ApplicationFee.php b/include/library/Cana/_Stripe/ApplicationFee.php similarity index 100% rename from include/library/Cana/Stripe/ApplicationFee.php rename to include/library/Cana/_Stripe/ApplicationFee.php diff --git a/include/library/Cana/Stripe/AttachedObject.php b/include/library/Cana/_Stripe/AttachedObject.php similarity index 100% rename from include/library/Cana/Stripe/AttachedObject.php rename to include/library/Cana/_Stripe/AttachedObject.php diff --git a/include/library/Cana/Stripe/AuthenticationError.php b/include/library/Cana/_Stripe/AuthenticationError.php similarity index 100% rename from include/library/Cana/Stripe/AuthenticationError.php rename to include/library/Cana/_Stripe/AuthenticationError.php diff --git a/include/library/Cana/Stripe/Balance.php b/include/library/Cana/_Stripe/Balance.php similarity index 100% rename from include/library/Cana/Stripe/Balance.php rename to include/library/Cana/_Stripe/Balance.php diff --git a/include/library/Cana/Stripe/BalanceTransaction.php b/include/library/Cana/_Stripe/BalanceTransaction.php similarity index 100% rename from include/library/Cana/Stripe/BalanceTransaction.php rename to include/library/Cana/_Stripe/BalanceTransaction.php diff --git a/include/library/Cana/Stripe/Card.php b/include/library/Cana/_Stripe/Card.php similarity index 100% rename from include/library/Cana/Stripe/Card.php rename to include/library/Cana/_Stripe/Card.php diff --git a/include/library/Cana/Stripe/CardError.php b/include/library/Cana/_Stripe/CardError.php similarity index 100% rename from include/library/Cana/Stripe/CardError.php rename to include/library/Cana/_Stripe/CardError.php diff --git a/include/library/Cana/Stripe/Charge.php b/include/library/Cana/_Stripe/Charge.php similarity index 100% rename from include/library/Cana/Stripe/Charge.php rename to include/library/Cana/_Stripe/Charge.php diff --git a/include/library/Cana/Stripe/Coupon.php b/include/library/Cana/_Stripe/Coupon.php similarity index 100% rename from include/library/Cana/Stripe/Coupon.php rename to include/library/Cana/_Stripe/Coupon.php diff --git a/include/library/Cana/Stripe/Customer.php b/include/library/Cana/_Stripe/Customer.php similarity index 100% rename from include/library/Cana/Stripe/Customer.php rename to include/library/Cana/_Stripe/Customer.php diff --git a/include/library/Cana/Stripe/Error.php b/include/library/Cana/_Stripe/Error.php similarity index 100% rename from include/library/Cana/Stripe/Error.php rename to include/library/Cana/_Stripe/Error.php diff --git a/include/library/Cana/Stripe/Event.php b/include/library/Cana/_Stripe/Event.php similarity index 100% rename from include/library/Cana/Stripe/Event.php rename to include/library/Cana/_Stripe/Event.php diff --git a/include/library/Cana/Stripe/InvalidRequestError.php b/include/library/Cana/_Stripe/InvalidRequestError.php similarity index 100% rename from include/library/Cana/Stripe/InvalidRequestError.php rename to include/library/Cana/_Stripe/InvalidRequestError.php diff --git a/include/library/Cana/Stripe/Invoice.php b/include/library/Cana/_Stripe/Invoice.php similarity index 100% rename from include/library/Cana/Stripe/Invoice.php rename to include/library/Cana/_Stripe/Invoice.php diff --git a/include/library/Cana/Stripe/InvoiceItem.php b/include/library/Cana/_Stripe/InvoiceItem.php similarity index 100% rename from include/library/Cana/Stripe/InvoiceItem.php rename to include/library/Cana/_Stripe/InvoiceItem.php diff --git a/include/library/Cana/Stripe/List.php b/include/library/Cana/_Stripe/List.php similarity index 100% rename from include/library/Cana/Stripe/List.php rename to include/library/Cana/_Stripe/List.php diff --git a/include/library/Cana/Stripe/Object.php b/include/library/Cana/_Stripe/Object.php similarity index 100% rename from include/library/Cana/Stripe/Object.php rename to include/library/Cana/_Stripe/Object.php diff --git a/include/library/Cana/Stripe/Plan.php b/include/library/Cana/_Stripe/Plan.php similarity index 100% rename from include/library/Cana/Stripe/Plan.php rename to include/library/Cana/_Stripe/Plan.php diff --git a/include/library/Cana/Stripe/Recipient.php b/include/library/Cana/_Stripe/Recipient.php similarity index 100% rename from include/library/Cana/Stripe/Recipient.php rename to include/library/Cana/_Stripe/Recipient.php diff --git a/include/library/Cana/Stripe/SingletonApiResource.php b/include/library/Cana/_Stripe/SingletonApiResource.php similarity index 100% rename from include/library/Cana/Stripe/SingletonApiResource.php rename to include/library/Cana/_Stripe/SingletonApiResource.php diff --git a/include/library/Cana/Stripe/Token.php b/include/library/Cana/_Stripe/Token.php similarity index 100% rename from include/library/Cana/Stripe/Token.php rename to include/library/Cana/_Stripe/Token.php diff --git a/include/library/Cana/Stripe/Transfer.php b/include/library/Cana/_Stripe/Transfer.php similarity index 100% rename from include/library/Cana/Stripe/Transfer.php rename to include/library/Cana/_Stripe/Transfer.php diff --git a/include/library/Cana/Stripe/Util.php b/include/library/Cana/_Stripe/Util.php similarity index 100% rename from include/library/Cana/Stripe/Util.php rename to include/library/Cana/_Stripe/Util.php diff --git a/include/library/Cana/Stripe/Util/Set.php b/include/library/Cana/_Stripe/Util/Set.php similarity index 100% rename from include/library/Cana/Stripe/Util/Set.php rename to include/library/Cana/_Stripe/Util/Set.php diff --git a/include/library/Crunchbutton/App.php b/include/library/Crunchbutton/App.php index 0f1e1f16c..18913118d 100755 --- a/include/library/Crunchbutton/App.php +++ b/include/library/Crunchbutton/App.php @@ -157,9 +157,6 @@ class Crunchbutton_App extends Cana_App { ->config($config) ->postInit($params); - require_once c::config()->dirs->library . '/Cana/Stripe.php'; - Stripe::setApiKey(c::config()->stripe->dev->secret); - switch ($_SERVER['SERVER_NAME']) { case 'spicywithdelivery.com': case 'beta.spicywithdelivery.com': @@ -462,6 +459,16 @@ class Crunchbutton_App extends Cana_App { } return $this->_balanced; } + + public function stripe() { + if (!$this->_stripe) { + \Stripe\Stripe::setApiKey(c::config()->stripe->{c::getEnv()}->secret);; + $this->_stripe = true; + } + return $this->_stripe; + } + + public function lob($d = true) { if (!$this->_lob) { diff --git a/include/library/Crunchbutton/Charge/Balanced.php b/include/library/Crunchbutton/Charge/Balanced.php index 61cec82c7..fd31d4f32 100644 --- a/include/library/Crunchbutton/Charge/Balanced.php +++ b/include/library/Crunchbutton/Charge/Balanced.php @@ -20,7 +20,8 @@ class Crunchbutton_Charge_Balanced extends Cana_Model { $c = $this->_card->debits->create([ 'amount' => $params['amount'] * 100, 'appears_on_statement_as' => 'Crunchbutton', - 'description' => $params['restaurant']->name + 'description' => $params['restaurant']->name, + 'statement_descriptor' => $params['restaurant']->statementName() ]); } catch (Exception $e) { diff --git a/include/library/Crunchbutton/Charge/Stripe.php b/include/library/Crunchbutton/Charge/Stripe.php index 09ab68c11..6a2d4b461 100644 --- a/include/library/Crunchbutton/Charge/Stripe.php +++ b/include/library/Crunchbutton/Charge/Stripe.php @@ -2,98 +2,103 @@ class Crunchbutton_Charge_Stripe extends Crunchbutton_Charge { public function __construct($params = []) { - + $this->_customer = $params['customer_id']; + $this->_card = $params['card_id']; } public function charge($params = []) { - $env = c::getEnv(); - - Stripe::setApiKey(c::config()->stripe->{$env}->secret); + c::stripe(); $success = false; - $reason = false; - - $user = $params[ 'user' ]; - - // Start with no customer id - $customer_id = false; // The user changed its card or it is a new one - if( $params['card']['id'] ){ - // The first thing we need to do is check customer - $token = $params['card']['uri']; - // lets see if the customer exists - if ( !$user || !$user->payment_type()->stripe_id ) { - // if there is no user, create one - try { - $customer = Stripe_Customer::create( array( - 'description' => "Crunchbutton", - 'card' => $token - ) ); - } catch ( Exception $e ) { - print_r( $e ); - die('creating customer error: 1'); - } - } elseif ( $user && $user->payment_type()->stripe_id ) { - // if there is already a user, update it - try { - $customer = Stripe_Customer::retrieve( $user->payment_type()->stripe_id ); - $customer->card = $token; - $customer->save(); - } catch ( Exception $e ) { - print_r( $e ); - die('creating customer error: 2'); - } - } - $customer_id = $customer->id; - } - // If we don't have a card token it means the user is already a customer - else if( $user->payment_type()->stripe_id ) { - $customer_id = $user->payment_type()->stripe_id; - } - - // yay, we have a valid customer - if( $customer_id ){ - // Now we have to charge it + if ($params['card']) { + + // create a customer if it doesnt exist + if (!$this->_customer) { try { - $charge = Stripe_Charge::create([ - 'amount' => $params['amount'] * 100, - 'currency' => 'usd', - 'customer' => $customer_id, - 'description' => $params['restaurant']->name, - ] ); - } - // Shit happens - catch(Stripe_CardError $e) { - Log::debug( [ 'card error' => 'card declined', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); - $errors[] = 'Your card was declined. Please try again!'; - } catch (Stripe_InvalidRequestError $e) { - Log::debug( [ 'card error' => 'invalid request', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); - $errors[] = 'Please update your credit card information.'; - } catch (Stripe_AuthenticationError $e) { - Log::debug( [ 'card error' => 'auth error', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); - $errors[] = 'Please update your credit card information.'; - } catch (Stripe_ApiConnectionError $e) { - Log::debug( [ 'card error' => 'api connection', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); - $errors[] = 'Please update your credit card information.'; - } catch (Stripe_Error $e) { - Log::debug( [ 'card error' => 'api connection', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); - $errors[] = 'Please update your credit card information.'; - } catch (Exception $e) { - $errors[] = 'Not enough card information.'; - } + $customer = \Stripe\Customer::create([ + 'description' => $params['name'], + 'email' => $params['email'], + 'source' => $params['card']['uri'] + ]); - if ( $charge->paid && !$charge->refunded ) { - $success = true; - $txn = $charge->id; - } - } - if (!$success && !$errors) { - $errors[] = 'Not enough card information.'; + } catch ( Exception $e ) { + $errors[] = 'Could not create customer with processor.'; + } + $this->_customer = $customer->id; + + // there is already a customer + } else { + try { + $customer = \Stripe\Customer::retrieve($this->_customer); + $customer->card = $params['card']['token']; + $customer->save(); + + } catch ( Exception $e ) { + $errors[] = 'Could not retrieve or save customer to processor.'; + } + } + + $this->_card = $params['card']['id']; } - return [ 'status' => $success, 'txn' => $txn, 'errors' => $errors, 'customer' => $customer ]; + // Now we have to charge it + try { + $charge = \Stripe\Charge::create([ + 'amount' => $params['amount'] * 100, + 'currency' => 'usd', + 'customer' => $this->_customer, + //'source' => $this->_card, + 'description' => $params['restaurant']->name, + 'capture' => c::config()->site->config('processor_payments_capture') ? true : false, + 'statement_descriptor' => $params['restaurant']->statementName() + ]); + + } catch(\Stripe\Stripe_CardError $e) { + Log::debug( [ 'card error' => 'card declined', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); + $errors[] = 'Your card was declined. Please try again!'; + + } catch (\Stripe\Stripe_InvalidRequestError $e) { + Log::debug( [ 'card error' => 'invalid request', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); + $errors[] = 'Please update your credit card information.'; + + } catch (\Stripe\Stripe_AuthenticationError $e) { + Log::debug( [ 'card error' => 'auth error', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); + $errors[] = 'Please update your credit card information.'; + + } catch (\Stripe\Stripe_ApiConnectionError $e) { + Log::debug( [ 'card error' => 'api connection', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); + $errors[] = 'Please update your credit card information.'; + + } catch (\Stripe\Stripe_Error $e) { + Log::debug( [ 'card error' => 'api connection', 'Exception' => $e->getJsonBody(), 'type' => 'stripe error' ]); + $errors[] = 'Please update your credit card information.'; + + } catch (Exception $e) { + //tripe\Error\InvalidRequest + //Stripe\Error\Card + print_r($e); + $errors[] = 'Error processing credit card.'; + } + + if ($charge && $charge->paid && !$charge->refunded) { + $success = true; + $txn = $charge->id; + } + + if (!$success && !$errors) { + $errors[] = 'Completly vague payment error. Contact support and complain. We love complaints.'."\n\n".'angrycustomers@_DOMAIN_'; + } + + return [ + 'status' => $success, + 'txn' => $txn, + 'errors' => $errors, + 'customer' => $this->_customer, + 'card' => $this->_card + ]; } } \ No newline at end of file diff --git a/include/library/Crunchbutton/Order.php b/include/library/Crunchbutton/Order.php index 4b3832197..ff2f76713 100644 --- a/include/library/Crunchbutton/Order.php +++ b/include/library/Crunchbutton/Order.php @@ -564,11 +564,9 @@ class Crunchbutton_Order extends Crunchbutton_Order_Trackchange { switch (Crunchbutton_User_Payment_Type::processor()) { case 'stripe': - $payment_type->stripe_id = $this->_customer->id; + $payment_type->stripe_id = $this->_paymentType; break; - case 'balanced': - default: $payment_type->balanced_id = $this->_paymentType->id; break; } @@ -949,32 +947,34 @@ class Crunchbutton_Order extends Crunchbutton_Order_Trackchange { case 'card': $user = c::user()->id_user ? c::user() : null; + $processorId = c::config()->site->config('processor_payments') == 'balanced' ? 'balanced_id' : 'stripe_id'; - if( $user ){ + if ($user) { $paymentType = $user->payment_type(); } - if (!$this->_card['id'] && !$paymentType->id_user_payment_type && $user->balanced_id) { - // user only has a balanced customer id, not a payment. copy payment type over - $paymentType = (new User_Payment_Type([ - 'id_user' => $user->id_user, - 'active' => 1, -// 'stripe_id' => $user->stripe_id, - 'balanced_id' => $user->balanced_id, - 'card' => $user->card, - 'card_exp_month' => $user->card_exp_month, - 'card_exp_year' => $user->card_exp_year, - 'date' => date('Y-m-d H:i:s') - ]))->save(); + // #5243 - if using stripe, get the stripe id from balanced, and update the paymenttype + if (c::config()->site->config('processor_payments') == 'stripe' && $paymentType->balanced_id && !$paymentType->stripe_id) { + $balancedCard = Crunchbutton_Balanced_Card::byId($paymentType->balanced_id); + print_r($balancedCard); + exit; } + // use a stored users card and the apporiate payment type + if (!$this->_card['id'] && $paymentType->id_user_payment_type) { - // use a stored users card and the apporiate payment type - if ($paymentType->balanced_id) { + if (c::config()->site->config('processor_payments') == 'stripe' && $paymentType->stripe_id) { + $charge = new Charge_Stripe([ + 'card_id' => $paymentType->stripe_id, + 'customer_id' => $user->stripe_id + ]); + + } elseif (c::config()->site->config('processor_payments') == 'balanced' && $paymentType->balanced_id) { if (substr($paymentType->balanced_id,0,2) != 'CC') { // we have stored the customer and not the payment type. need to fix that + // @todo: i dont really understand wtf this is for - devin $cards = Crunchbutton_Balanced_Account::byId($paymentType->balanced_id)->cards; if (get_class($cards) == 'RESTful\Collection') { foreach ($cards as $card) { @@ -996,23 +996,19 @@ class Crunchbutton_Order extends Crunchbutton_Order_Trackchange { $charge = new Charge_Balanced([ 'card_id' => $paymentType->balanced_id ]); - - } elseif ($paymentType->stripe_id) { - $charge = new Charge_Stripe([ - 'stripe_id' => $paymentType->stripe_id - ]); + } else { + die('processor mismatch'); } } + // create the objects with no params if (!$charge) { switch (Crunchbutton_User_Payment_Type::processor()) { case 'balanced': $charge = new Charge_Balanced(); break; case 'stripe': - $charge = new Charge_Stripe([ - 'stripe_id' => $user->stripe_id - ]); + $charge = new Charge_Stripe(); break; } } @@ -1029,6 +1025,7 @@ class Crunchbutton_Order extends Crunchbutton_Order_Trackchange { 'card' => $this->_card, 'name' => $this->name, 'address' => $this->address, + 'email' => $user->email, 'phone' => $this->phone, 'user' => $user, 'restaurant' => $this->restaurant() diff --git a/include/library/Crunchbutton/Restaurant.php b/include/library/Crunchbutton/Restaurant.php index e9f13dee9..a66d18a94 100644 --- a/include/library/Crunchbutton/Restaurant.php +++ b/include/library/Crunchbutton/Restaurant.php @@ -154,8 +154,17 @@ class Crunchbutton_Restaurant extends Cana_Table_Trackchange { return $phone; } - public function shortName() { - return $this->short_name ? $this->short_name : $this->name; + // name that appears on credit card statement + public function statementName() { + if (!isset($this->_statementName)) { + $name = $this->short_name ? $this->short_name : $this->name; + $name = preg_replace('/[^a-z ]/i','',$name); + if (strlen($name) > 22) { + $name = str_replace(' ', '', $name); + } + $this->_statementName = strtoupper(substr($name, 0, 22)); + } + return $this->_statementName; } public function _hasOption($option, $options) { diff --git a/include/library/Crunchbutton/User/Payment/Type.php b/include/library/Crunchbutton/User/Payment/Type.php index b43791164..e64d29389 100644 --- a/include/library/Crunchbutton/User/Payment/Type.php +++ b/include/library/Crunchbutton/User/Payment/Type.php @@ -3,18 +3,27 @@ class Crunchbutton_User_Payment_Type extends Cana_Table { public function processor() { - return c::config()->processor; + return c::config()->site->config('processor_payments')->value; } - public function getUserPaymentType( $id_user = null ){ - $id_user = ( $id_user ) ? $id_user : c::user()->id_user; - if( $id_user ){ - $where = ' AND ' . Crunchbutton_User_Payment_Type::processor() . '_id IS NOT NULL'; - $payment = Crunchbutton_User_Payment_Type::q( 'SELECT * FROM user_payment_type WHERE id_user = ? AND active = true ' . $where . ' ORDER BY id_user_payment_type DESC LIMIT 1', [$id_user]); - if( $payment->id_user_payment_type ){ + public function getUserPaymentType($id_user = null) { + $id_user = $id_user ? $id_user : c::user()->id_user; + + if ($id_user) { + $payment = Crunchbutton_User_Payment_Type::q(' + SELECT * FROM user_payment_type + WHERE + id_user = ? + AND active = true + AND ' . Crunchbutton_User_Payment_Type::processor() . '_id IS NOT NULL + ORDER BY id_user_payment_type DESC LIMIT 1 + ', [$id_user]); + + if ($payment->id_user_payment_type) { return $payment; } } + return false; } diff --git a/include/library/Stripe/Account.php b/include/library/Stripe/Account.php new file mode 100755 index 000000000..0684a55cb --- /dev/null +++ b/include/library/Stripe/Account.php @@ -0,0 +1,62 @@ +_save(); + } + + /** + * @param array|null $params + * @param array|string|null $opts + * + * @return Account[] + */ + public static function all($params = null, $opts = null) + { + return self::_all($params, $opts); + } +} diff --git a/include/library/Stripe/ApiRequestor.php b/include/library/Stripe/ApiRequestor.php new file mode 100755 index 000000000..c30951533 --- /dev/null +++ b/include/library/Stripe/ApiRequestor.php @@ -0,0 +1,471 @@ +_apiKey = $apiKey; + if (!$apiBase) { + $apiBase = Stripe::$apiBase; + } + $this->_apiBase = $apiBase; + } + + /** + * @param string|mixed $value A string to UTF8-encode. + * + * @return string|mixed The UTF8-encoded string, or the object passed in if + * it wasn't a string. + */ + public static function utf8($value) + { + if (is_string($value) && mb_detect_encoding($value, "UTF-8", true) != "UTF-8") { + return utf8_encode($value); + } else { + return $value; + } + } + + private static function _encodeObjects($d) + { + if ($d instanceof ApiResource) { + return self::utf8($d->id); + } elseif ($d === true) { + return 'true'; + } elseif ($d === false) { + return 'false'; + } elseif (is_array($d)) { + $res = array(); + foreach ($d as $k => $v) { + $res[$k] = self::_encodeObjects($v); + } + return $res; + } else { + return self::utf8($d); + } + } + + /** + * @param array $arr An map of param keys to values. + * @param string|null $prefix (It doesn't look like we ever use $prefix...) + * + * @return string A querystring, essentially. + */ + public static function encode($arr, $prefix = null) + { + if (!is_array($arr)) { + return $arr; + } + + $r = array(); + foreach ($arr as $k => $v) { + if (is_null($v)) { + continue; + } + + if ($prefix && $k && !is_int($k)) { + $k = $prefix."[".$k."]"; + } elseif ($prefix) { + $k = $prefix."[]"; + } + + if (is_array($v)) { + $r[] = self::encode($v, $k, true); + } else { + $r[] = urlencode($k)."=".urlencode($v); + } + } + + return implode("&", $r); + } + + /** + * @param string $method + * @param string $url + * @param array|null $params + * @param array|null $headers + * + * @return array An array whose first element is the response and second + * element is the API key used to make the request. + */ + public function request($method, $url, $params = null, $headers = null) + { + if (!$params) { + $params = array(); + } + if (!$headers) { + $headers = array(); + } + list($rbody, $rcode, $myApiKey) = + $this->_requestRaw($method, $url, $params, $headers); + $resp = $this->_interpretResponse($rbody, $rcode); + return array($resp, $myApiKey); + } + + /** + * @param string $rbody A JSON string. + * @param int $rcode + * @param array $resp + * + * @throws Error\InvalidRequest if the error is caused by the user. + * @throws Error\Authentication if the error is caused by a lack of + * permissions. + * @throws Error\Card if the error is the error code is 402 (payment + * required) + * @throws Error\Api otherwise. + */ + public function handleApiError($rbody, $rcode, $resp) + { + if (!is_array($resp) || !isset($resp['error'])) { + $msg = "Invalid response object from API: $rbody " + . "(HTTP response code was $rcode)"; + throw new Error\Api($msg, $rcode, $rbody, $resp); + } + + $error = $resp['error']; + $msg = isset($error['message']) ? $error['message'] : null; + $param = isset($error['param']) ? $error['param'] : null; + $code = isset($error['code']) ? $error['code'] : null; + + switch ($rcode) { + case 400: + if ($code == 'rate_limit') { + throw new Error\RateLimit($msg, $param, $rcode, $rbody, $resp); + } + + // intentional fall-through + case 404: + throw new Error\InvalidRequest($msg, $param, $rcode, $rbody, $resp); + case 401: + throw new Error\Authentication($msg, $rcode, $rbody, $resp); + case 402: + throw new Error\Card($msg, $param, $code, $rcode, $rbody, $resp); + default: + throw new Error\Api($msg, $rcode, $rbody, $resp); + } + } + + private function _requestRaw($method, $url, $params, $headers) + { + if (!array_key_exists($this->_apiBase, self::$_preFlight) || + !self::$_preFlight[$this->_apiBase]) { + self::$_preFlight[$this->_apiBase] = $this->checkSslCert($this->_apiBase); + } + + $myApiKey = $this->_apiKey; + if (!$myApiKey) { + $myApiKey = Stripe::$apiKey; + } + + if (!$myApiKey) { + $msg = 'No API key provided. (HINT: set your API key using ' + . '"Stripe::setApiKey()". You can generate API keys from ' + . 'the Stripe web interface. See https://stripe.com/api for ' + . 'details, or email support@stripe.com if you have any questions.'; + throw new Error\Authentication($msg); + } + + $absUrl = $this->_apiBase.$url; + $params = self::_encodeObjects($params); + $langVersion = phpversion(); + $uname = php_uname(); + $ua = array( + 'bindings_version' => Stripe::VERSION, + 'lang' => 'php', + 'lang_version' => $langVersion, + 'publisher' => 'stripe', + 'uname' => $uname, + ); + $defaultHeaders = array( + 'X-Stripe-Client-User-Agent' => json_encode($ua), + 'User-Agent' => 'Stripe/v1 PhpBindings/' . Stripe::VERSION, + 'Authorization' => 'Bearer ' . $myApiKey, + ); + if (Stripe::$apiVersion) { + $defaultHeaders['Stripe-Version'] = Stripe::$apiVersion; + } + $hasFile = false; + $hasCurlFile = class_exists('\CURLFile', false); + foreach ($params as $k => $v) { + if (is_resource($v)) { + $hasFile = true; + $params[$k] = self::_processResourceParam($v, $hasCurlFile); + } elseif ($hasCurlFile && $v instanceof \CURLFile) { + $hasFile = true; + } + } + + if ($hasFile) { + $defaultHeaders['Content-Type'] = 'multipart/form-data'; + } else { + $defaultHeaders['Content-Type'] = 'application/x-www-form-urlencoded'; + } + + $combinedHeaders = array_merge($defaultHeaders, $headers); + $rawHeaders = array(); + + foreach ($combinedHeaders as $header => $value) { + $rawHeaders[] = $header . ': ' . $value; + } + + list($rbody, $rcode) = $this->_curlRequest( + $method, + $absUrl, + $rawHeaders, + $params, + $hasFile + ); + return array($rbody, $rcode, $myApiKey); + } + + private function _processResourceParam($resource, $hasCurlFile) + { + if (get_resource_type($resource) !== 'stream') { + throw new Error\Api( + 'Attempted to upload a resource that is not a stream' + ); + } + + $metaData = stream_get_meta_data($resource); + if ($metaData['wrapper_type'] !== 'plainfile') { + throw new Error\Api( + 'Only plainfile resource streams are supported' + ); + } + + if ($hasCurlFile) { + // We don't have the filename or mimetype, but the API doesn't care + return new \CURLFile($metaData['uri']); + } else { + return '@'.$metaData['uri']; + } + } + + private function _interpretResponse($rbody, $rcode) + { + try { + $resp = json_decode($rbody, true); + } catch (Exception $e) { + $msg = "Invalid response body from API: $rbody " + . "(HTTP response code was $rcode)"; + throw new Error\Api($msg, $rcode, $rbody); + } + + if ($rcode < 200 || $rcode >= 300) { + $this->handleApiError($rbody, $rcode, $resp); + } + return $resp; + } + + private function _curlRequest($method, $absUrl, $headers, $params, $hasFile) + { + $curl = curl_init(); + $method = strtolower($method); + $opts = array(); + if ($method == 'get') { + if ($hasFile) { + throw new Error\Api( + "Issuing a GET request with a file parameter" + ); + } + $opts[CURLOPT_HTTPGET] = 1; + if (count($params) > 0) { + $encoded = self::encode($params); + $absUrl = "$absUrl?$encoded"; + } + } elseif ($method == 'post') { + $opts[CURLOPT_POST] = 1; + $opts[CURLOPT_POSTFIELDS] = $hasFile ? $params : self::encode($params); + } elseif ($method == 'delete') { + $opts[CURLOPT_CUSTOMREQUEST] = 'DELETE'; + if (count($params) > 0) { + $encoded = self::encode($params); + $absUrl = "$absUrl?$encoded"; + } + } else { + throw new Error\Api("Unrecognized method $method"); + } + + $absUrl = self::utf8($absUrl); + $opts[CURLOPT_URL] = $absUrl; + $opts[CURLOPT_RETURNTRANSFER] = true; + $opts[CURLOPT_CONNECTTIMEOUT] = 30; + $opts[CURLOPT_TIMEOUT] = 80; + $opts[CURLOPT_RETURNTRANSFER] = true; + $opts[CURLOPT_HTTPHEADER] = $headers; + if (!Stripe::$verifySslCerts) { + $opts[CURLOPT_SSL_VERIFYPEER] = false; + } + + curl_setopt_array($curl, $opts); + $rbody = curl_exec($curl); + + if (!defined('CURLE_SSL_CACERT_BADFILE')) { + define('CURLE_SSL_CACERT_BADFILE', 77); // constant not defined in PHP + } + + $errno = curl_errno($curl); + if ($errno == CURLE_SSL_CACERT || + $errno == CURLE_SSL_PEER_CERTIFICATE || + $errno == CURLE_SSL_CACERT_BADFILE + ) { + array_push( + $headers, + 'X-Stripe-Client-Info: {"ca":"using Stripe-supplied CA bundle"}' + ); + $cert = $this->caBundle(); + curl_setopt($curl, CURLOPT_HTTPHEADER, $headers); + curl_setopt($curl, CURLOPT_CAINFO, $cert); + $rbody = curl_exec($curl); + } + + if ($rbody === false) { + $errno = curl_errno($curl); + $message = curl_error($curl); + curl_close($curl); + $this->handleCurlError($errno, $message); + } + + $rcode = curl_getinfo($curl, CURLINFO_HTTP_CODE); + curl_close($curl); + return array($rbody, $rcode); + } + + /** + * @param number $errno + * @param string $message + * @throws ApiConnectionError + */ + public function handleCurlError($errno, $message) + { + $apiBase = $this->_apiBase; + switch ($errno) { + case CURLE_COULDNT_CONNECT: + case CURLE_COULDNT_RESOLVE_HOST: + case CURLE_OPERATION_TIMEOUTED: + $msg = "Could not connect to Stripe ($apiBase). Please check your " + . "internet connection and try again. If this problem persists, " + . "you should check Stripe's service status at " + . "https://twitter.com/stripestatus, or"; + break; + case CURLE_SSL_CACERT: + case CURLE_SSL_PEER_CERTIFICATE: + $msg = "Could not verify Stripe's SSL certificate. Please make sure " + . "that your network is not intercepting certificates. " + . "(Try going to $apiBase in your browser.) " + . "If this problem persists,"; + break; + default: + $msg = "Unexpected error communicating with Stripe. " + . "If this problem persists,"; + } + $msg .= " let us know at support@stripe.com."; + + $msg .= "\n\n(Network error [errno $errno]: $message)"; + throw new Error\ApiConnection($msg); + } + + /** + * Preflight the SSL certificate presented by the backend. This isn't 100% + * bulletproof, in that we're not actually validating the transport used to + * communicate with Stripe, merely that the first attempt to does not use a + * revoked certificate. + * + * Unfortunately the interface to OpenSSL doesn't make it easy to check the + * certificate before sending potentially sensitive data on the wire. This + * approach raises the bar for an attacker significantly. + */ + private function checkSslCert($url) + { + if (!function_exists('stream_context_get_params') || + !function_exists('stream_socket_enable_crypto')) { + error_log( + 'Warning: This version of PHP does not support checking SSL ' . + 'certificates Stripe cannot guarantee that the server has a ' . + 'certificate which is not blacklisted.' + ); + return true; + } + + $url = parse_url($url); + $port = isset($url["port"]) ? $url["port"] : 443; + $url = "ssl://{$url["host"]}:{$port}"; + + $sslContext = stream_context_create( + array('ssl' => array( + 'capture_peer_cert' => true, + 'verify_peer' => true, + 'cafile' => $this->caBundle(), + )) + ); + $result = stream_socket_client( + $url, + $errno, + $errstr, + 30, + STREAM_CLIENT_CONNECT, + $sslContext + ); + if (($errno !== 0 && $errno !== null) || $result === false) { + throw new Error\ApiConnection( + 'Could not connect to Stripe (' . $url . '). Please check your ' . + 'internet connection and try again. If this problem persists, ' . + 'you should check Stripe\'s service status at ' . + 'https://twitter.com/stripestatus. Reason was: ' . $errstr + ); + } + + $params = stream_context_get_params($result); + + $cert = $params['options']['ssl']['peer_certificate']; + + openssl_x509_export($cert, $pemCert); + + if (self::isBlackListed($pemCert)) { + throw new Error\ApiConnection( + 'Invalid server certificate. You tried to connect to a server that ' . + 'has a revoked SSL certificate, which means we cannot securely send ' . + 'data to that server. Please email support@stripe.com if you need ' . + 'help connecting to the correct API server.' + ); + } + + return true; + } + + /** + * Checks if a valid PEM encoded certificate is blacklisted + * @return boolean + */ + public static function isBlackListed($certificate) + { + $certificate = trim($certificate); + $lines = explode("\n", $certificate); + + // Kludgily remove the PEM padding + array_shift($lines); + array_pop($lines); + + $derCert = base64_decode(implode("", $lines)); + $fingerprint = sha1($derCert); + return in_array($fingerprint, self::$_blacklistedCerts); + } + + private function caBundle() + { + return dirname(__FILE__) . '/../../../ssl/ca-certificates.crt'; + } +} diff --git a/include/library/Stripe/ApiResource.php b/include/library/Stripe/ApiResource.php new file mode 100755 index 000000000..f80aeb85e --- /dev/null +++ b/include/library/Stripe/ApiResource.php @@ -0,0 +1,161 @@ + true, 'Stripe-Version' => true); + + public static function baseUrl() + { + return Stripe::$apiBase; + } + + /** + * @return ApiResource The refreshed resource. + */ + public function refresh() + { + $requestor = new ApiRequestor($this->_opts->apiKey, static::baseUrl()); + $url = $this->instanceUrl(); + + list($response, $this->_opts->apiKey) = $requestor->request( + 'get', + $url, + $this->_retrieveOptions, + $this->_opts->headers + ); + $this->refreshFrom($response, $this->_opts); + return $this; + } + + /** + * @return string The name of the class, with namespacing and underscores + * stripped. + */ + public static function className() + { + $class = get_called_class(); + // Useful for namespaces: Foo\Charge + if ($postfixNamespaces = strrchr($class, '\\')) { + $class = substr($postfixNamespaces, 1); + } + // Useful for underscored 'namespaces': Foo_Charge + if ($postfixFakeNamespaces = strrchr($class, '')) { + $class = $postfixFakeNamespaces; + } + if (substr($class, 0, strlen('Stripe')) == 'Stripe') { + $class = substr($class, strlen('Stripe')); + } + $class = str_replace('_', '', $class); + $name = urlencode($class); + $name = strtolower($name); + return $name; + } + + /** + * @return string The endpoint URL for the given class. + */ + public static function classUrl() + { + $base = static::className(); + return "/v1/${base}s"; + } + + /** + * @return string The full API URL for this API resource. + */ + public function instanceUrl() + { + $id = $this['id']; + if ($id === null) { + $class = get_called_class(); + $message = "Could not determine which URL to request: " + . "$class instance has invalid ID: $id"; + throw new Error\InvalidRequest($message, null); + } + $id = ApiRequestor::utf8($id); + $base = static::classUrl(); + $extn = urlencode($id); + return "$base/$extn"; + } + + private static function _validateParams($params = null) + { + if ($params && !is_array($params)) { + $message = "You must pass an array as the first argument to Stripe API " + . "method calls. (HINT: an example call to create a charge " + . "would be: \"Stripe\\Charge::create(array('amount' => 100, " + . "'currency' => 'usd', 'card' => array('number' => " + . "4242424242424242, 'exp_month' => 5, 'exp_year' => 2015)))\")"; + throw new Error\Api($message); + } + } + + protected function _request($method, $url, $params = array(), $options = null) + { + $opts = $this->_opts->merge($options); + return static::_staticRequest($method, $url, $params, $opts); + } + + protected static function _staticRequest($method, $url, $params, $options) + { + $opts = Util\RequestOptions::parse($options); + $requestor = new ApiRequestor($opts->apiKey, static::baseUrl()); + list($response, $opts->apiKey) = $requestor->request($method, $url, $params, $opts->headers); + foreach ($opts->headers as $k => $v) { + if (!array_key_exists($k, self::$HEADERS_TO_PERSIST)) { + unset($opts->headers[$k]); + } + } + return array($response, $opts); + } + + protected static function _retrieve($id, $options = null) + { + $opts = Util\RequestOptions::parse($options); + $instance = new static($id, $opts); + $instance->refresh(); + return $instance; + } + + protected static function _all($params = null, $options = null) + { + self::_validateParams($params); + $url = static::classUrl(); + + list($response, $opts) = static::_staticRequest('get', $url, $params, $options); + return Util\Util::convertToStripeObject($response, $opts); + } + + protected static function _create($params = null, $options = null) + { + self::_validateParams($params); + $base = static::baseUrl(); + $url = static::classUrl(); + + list($response, $opts) = static::_staticRequest('post', $url, $params, $options); + return Util\Util::convertToStripeObject($response, $opts); + } + + protected function _save($options = null) + { + $params = $this->serializeParameters(); + if (count($params) > 0) { + $url = $this->instanceUrl(); + list($response, $opts) = $this->_request('post', $url, $params, $options); + $this->refreshFrom($response, $opts); + } + return $this; + } + + protected function _delete($params = null, $options = null) + { + self::_validateParams($params); + + $url = $this->instanceUrl(); + list($response, $opts) = $this->_request('delete', $url, $params, $options); + $this->refreshFrom($response, $opts); + return $this; + } +} diff --git a/include/library/Stripe/ApplicationFee.php b/include/library/Stripe/ApplicationFee.php new file mode 100755 index 000000000..41674e499 --- /dev/null +++ b/include/library/Stripe/ApplicationFee.php @@ -0,0 +1,53 @@ +instanceUrl() . '/refund'; + list($response, $opts) = $this->_request('post', $url, $params, $opts); + $this->refreshFrom($response, $opts); + return $this; + } +} diff --git a/include/library/Stripe/ApplicationFeeRefund.php b/include/library/Stripe/ApplicationFeeRefund.php new file mode 100755 index 000000000..7f4e94ff2 --- /dev/null +++ b/include/library/Stripe/ApplicationFeeRefund.php @@ -0,0 +1,39 @@ +_save($opts); + } +} diff --git a/include/library/Stripe/AttachedObject.php b/include/library/Stripe/AttachedObject.php new file mode 100755 index 000000000..be3868639 --- /dev/null +++ b/include/library/Stripe/AttachedObject.php @@ -0,0 +1,25 @@ +_values), array_keys($properties)); + // Don't unset, but rather set to null so we send up '' for deletion. + foreach ($removed as $k) { + $this->$k = null; + } + + foreach ($properties as $k => $v) { + $this->$k = $v; + } + } +} diff --git a/include/library/Stripe/Balance.php b/include/library/Stripe/Balance.php new file mode 100755 index 000000000..d07231f6c --- /dev/null +++ b/include/library/Stripe/Balance.php @@ -0,0 +1,16 @@ +_delete($params, $opts); + } + + /** + * @param array|string|null $opts + * + * @return BitcoinReceiver The saved Bitcoin Receiver item. + */ + public function save($opts = null) + { + return $this->_save($opts); + } +} diff --git a/include/library/Stripe/BitcoinTransaction.php b/include/library/Stripe/BitcoinTransaction.php new file mode 100755 index 000000000..a7414e3f8 --- /dev/null +++ b/include/library/Stripe/BitcoinTransaction.php @@ -0,0 +1,8 @@ +_delete($params, $opts); + } + + /** + * @param array|string|null $opts + * + * @return Card The saved card. + */ + public function save($opts = null) + { + return $this->_save($opts); + } +} diff --git a/include/library/Stripe/Charge.php b/include/library/Stripe/Charge.php new file mode 100755 index 000000000..60d36151e --- /dev/null +++ b/include/library/Stripe/Charge.php @@ -0,0 +1,132 @@ +_save($options); + } + + /** + * @param array|null $params + * @param array|string|null $options + * + * @return Charge The refunded charge. + */ + public function refund($params = null, $options = null) + { + $url = $this->instanceUrl() . '/refund'; + list($response, $opts) = $this->request('post', $url, $params, $options); + $this->refreshFrom($response, $opts); + return $this; + } + + /** + * @param array|null $params + * @param array|string|null $options + * + * @return Charge The captured charge. + */ + public function capture($params = null, $options = null) + { + $url = $this->instanceUrl() . '/capture'; + list($response, $opts) = $this->_request('post', $url, $params, $options); + $this->refreshFrom($response, $opts); + return $this; + } + + /** + * @param array|null $params + * @param array|string|null $options + * + * @return array The updated dispute. + */ + public function updateDispute($params = null, $options = null) + { + $url = $this->instanceUrl() . '/dispute'; + list($response, $opts) = $this->_request('post', $url, $params, $options); + $this->refreshFrom(array('dispute' => $response), $opts, true); + return $this->dispute; + } + + /** + * @param array|string|null $options + * + * @return Charge The updated charge. + */ + public function closeDispute($options = null) + { + $url = $this->instanceUrl() . '/dispute/close'; + list($response, $opts) = $this->_request('post', $url, null, $options); + $this->refreshFrom($response, $opts); + return $this; + } + + /** + * @param array|string|null $opts + * + * @return Charge The updated charge. + */ + public function markAsFraudulent($opts = null) + { + $params = array('fraud_details' => array('user_report' => 'fraudulent')); + $url = $this->instanceUrl(); + list($response, $opts) = $this->_request('post', $url, $params, $opts); + $this->refreshFrom($response, $opts); + return $this; + } + + /** + * @param array|string|null $opts + * + * @return Charge The updated charge. + */ + public function markAsSafe($opts = null) + { + $params = array('fraud_details' => array('user_report' => 'safe')); + $url = $this->instanceUrl(); + list($response, $opts) = $this->_request('post', $url, $params, $opts); + $this->refreshFrom($response, $opts); + return $this; + } +} diff --git a/include/library/Stripe/Collection.php b/include/library/Stripe/Collection.php new file mode 100755 index 000000000..a42771536 --- /dev/null +++ b/include/library/Stripe/Collection.php @@ -0,0 +1,56 @@ +extractPathAndUpdateParams($params); + + list($response, $opts) = $this->_request('get', $url, $params, $opts); + return Util\Util::convertToStripeObject($response, $opts); + } + + public function create($params = null, $opts = null) + { + list($url, $params) = $this->extractPathAndUpdateParams($params); + + list($response, $opts) = $this->_request('post', $url, $params, $opts); + return Util\Util::convertToStripeObject($response, $opts); + } + + public function retrieve($id, $params = null, $opts = null) + { + list($url, $params) = $this->extractPathAndUpdateParams($params); + + $id = ApiRequestor::utf8($id); + $extn = urlencode($id); + list($response, $opts) = $this->_request( + 'get', + "$url/$extn", + $params, + $opts + ); + return Util\Util::convertToStripeObject($response, $opts); + } + + private function extractPathAndUpdateParams($params) + { + $url = parse_url($this->url); + if (!isset($url['path'])) { + throw new Error\Api("Could not parse list url into parts: $url"); + } + + if (isset($url['query'])) { + // If the URL contains a query param, parse it out into $params so they + // don't interact weirdly with each other. + $query = array(); + parse_str($url['query'], $query); + // PHP 5.2 doesn't support the ?: operator :( + $params = array_merge($params ? $params : array(), $query); + } + + return array($url['path'], $params); + } +} diff --git a/include/library/Stripe/Coupon.php b/include/library/Stripe/Coupon.php new file mode 100755 index 000000000..eb2353ef8 --- /dev/null +++ b/include/library/Stripe/Coupon.php @@ -0,0 +1,60 @@ +_delete($params, $opts); + } + + /** + * @param array|string|null $opts + * + * @return Coupon The saved coupon. + */ + public function save($opts = null) + { + return $this->_save($opts); + } + + /** + * @param array|null $params + * @param array|string|null $opts + * + * @return Coupon[] + */ + public static function all($params = null, $opts = null) + { + return self::_all($params, $opts); + } +} diff --git a/include/library/Stripe/Customer.php b/include/library/Stripe/Customer.php new file mode 100755 index 000000000..b759f4eb7 --- /dev/null +++ b/include/library/Stripe/Customer.php @@ -0,0 +1,158 @@ +_save($opts); + } + + /** + * @param array|null $params + * @param array|string|null $opts + * + * @return Customer The deleted customer. + */ + public function delete($params = null, $opts = null) + { + return $this->_delete($params, $opts); + } + + /** + * @param array|null $params + * + * @return InvoiceItem The resulting invoice item. + */ + public function addInvoiceItem($params = null) + { + if (!$params) { + $params = array(); + } + $params['customer'] = $this->id; + $ii = InvoiceItem::create($params, $this->_opts); + return $ii; + } + + /** + * @param array|null $params + * + * @return array An array of the customer's Invoices. + */ + public function invoices($params = null) + { + if (!$params) { + $params = array(); + } + $params['customer'] = $this->id; + $invoices = Invoice::all($params, $this->_opts); + return $invoices; + } + + /** + * @param array|null $params + * + * @return array An array of the customer's InvoiceItems. + */ + public function invoiceItems($params = null) + { + if (!$params) { + $params = array(); + } + $params['customer'] = $this->id; + $iis = InvoiceItem::all($params, $this->_opts); + return $iis; + } + + /** + * @param array|null $params + * + * @return array An array of the customer's Charges. + */ + public function charges($params = null) + { + if (!$params) { + $params = array(); + } + $params['customer'] = $this->id; + $charges = Charge::all($params, $this->_opts); + return $charges; + } + + /** + * @param array|null $params + * + * @return Subscription The updated subscription. + */ + public function updateSubscription($params = null) + { + $url = $this->instanceUrl() . '/subscription'; + list($response, $opts) = $this->_request('post', $url, $params); + $this->refreshFrom(array('subscription' => $response), $opts, true); + return $this->subscription; + } + + /** + * @param array|null $params + * + * @return Subscription The cancelled subscription. + */ + public function cancelSubscription($params = null) + { + $url = $this->instanceUrl() . '/subscription'; + list($response, $opts) = $this->_request('delete', $url, $params); + $this->refreshFrom(array('subscription' => $response), $opts, true); + return $this->subscription; + } + + /** + * @param array|null $params + * + * @return Customer The updated customer. + */ + public function deleteDiscount() + { + $url = $this->instanceUrl() . '/discount'; + list($response, $opts) = $this->_request('delete', $url); + $this->refreshFrom(array('discount' => null), $opts, true); + } +} diff --git a/include/library/Stripe/Error/Api.php b/include/library/Stripe/Error/Api.php new file mode 100755 index 000000000..dc7d069ef --- /dev/null +++ b/include/library/Stripe/Error/Api.php @@ -0,0 +1,7 @@ +httpStatus = $httpStatus; + $this->httpBody = $httpBody; + $this->jsonBody = $jsonBody; + } + + public function getHttpStatus() + { + return $this->httpStatus; + } + + public function getHttpBody() + { + return $this->httpBody; + } + + public function getJsonBody() + { + return $this->jsonBody; + } +} diff --git a/include/library/Stripe/Error/Card.php b/include/library/Stripe/Error/Card.php new file mode 100755 index 000000000..9f8ca2290 --- /dev/null +++ b/include/library/Stripe/Error/Card.php @@ -0,0 +1,19 @@ +param = $param; + $this->code = $code; + } +} diff --git a/include/library/Stripe/Error/InvalidRequest.php b/include/library/Stripe/Error/InvalidRequest.php new file mode 100755 index 000000000..e29a70072 --- /dev/null +++ b/include/library/Stripe/Error/InvalidRequest.php @@ -0,0 +1,17 @@ +param = $param; + } +} diff --git a/include/library/Stripe/Error/RateLimit.php b/include/library/Stripe/Error/RateLimit.php new file mode 100755 index 000000000..ebb055f55 --- /dev/null +++ b/include/library/Stripe/Error/RateLimit.php @@ -0,0 +1,16 @@ +_save($opts); + } + + /** + * @return Invoice The paid invoice. + */ + public function pay($opts = null) + { + $url = $this->instanceUrl() . '/pay'; + list($response, $opts) = $this->_request('post', $url, null, $opts); + $this->refreshFrom($response, $opts); + return $this; + } +} diff --git a/include/library/Stripe/InvoiceItem.php b/include/library/Stripe/InvoiceItem.php new file mode 100755 index 000000000..d1407b562 --- /dev/null +++ b/include/library/Stripe/InvoiceItem.php @@ -0,0 +1,60 @@ +_save($opts); + } + + /** + * @param array|null $params + * @param array|string|null $opts + * + * @return InvoiceItem The deleted invoice item. + */ + public function delete($params = null, $opts = null) + { + return $this->_delete($params, $opts); + } +} diff --git a/include/library/Stripe/Object.php b/include/library/Stripe/Object.php new file mode 100755 index 000000000..7c524c708 --- /dev/null +++ b/include/library/Stripe/Object.php @@ -0,0 +1,247 @@ +_opts = $opts ? $opts : new Util\RequestOptions(); + $this->_values = array(); + $this->_unsavedValues = new Util\Set(); + $this->_transientValues = new Util\Set(); + + $this->_retrieveOptions = array(); + if (is_array($id)) { + foreach ($id as $key => $value) { + if ($key != 'id') { + $this->_retrieveOptions[$key] = $value; + } + } + $id = $id['id']; + } + + if ($id !== null) { + $this->id = $id; + } + } + + // Standard accessor magic methods + public function __set($k, $v) + { + if ($v === "") { + throw new InvalidArgumentException( + 'You cannot set \''.$k.'\'to an empty string. ' + .'We interpret empty strings as NULL in requests. ' + .'You may set obj->'.$k.' = NULL to delete the property' + ); + } + + if (self::$nestedUpdatableAttributes->includes($k) + && isset($this->$k) && is_array($v)) { + $this->$k->replaceWith($v); + } else { + // TODO: may want to clear from $_transientValues (Won't be user-visible). + $this->_values[$k] = $v; + } + if (!self::$permanentAttributes->includes($k)) { + $this->_unsavedValues->add($k); + } + } + + public function __isset($k) + { + return isset($this->_values[$k]); + } + public function __unset($k) + { + unset($this->_values[$k]); + $this->_transientValues->add($k); + $this->_unsavedValues->discard($k); + } + public function __get($k) + { + if (array_key_exists($k, $this->_values)) { + return $this->_values[$k]; + } else if ($this->_transientValues->includes($k)) { + $class = get_class($this); + $attrs = join(', ', array_keys($this->_values)); + $message = "Stripe Notice: Undefined property of $class instance: $k. " + . "HINT: The $k attribute was set in the past, however. " + . "It was then wiped when refreshing the object " + . "with the result returned by Stripe's API, " + . "probably as a result of a save(). The attributes currently " + . "available on this object are: $attrs"; + error_log($message); + return null; + } else { + $class = get_class($this); + error_log("Stripe Notice: Undefined property of $class instance: $k"); + return null; + } + } + + // ArrayAccess methods + public function offsetSet($k, $v) + { + $this->$k = $v; + } + + public function offsetExists($k) + { + return array_key_exists($k, $this->_values); + } + + public function offsetUnset($k) + { + unset($this->$k); + } + public function offsetGet($k) + { + return array_key_exists($k, $this->_values) ? $this->_values[$k] : null; + } + + public function keys() + { + return array_keys($this->_values); + } + + /** + * This unfortunately needs to be public to be used in Util\Util + * + * @param array $values + * @param array $opts + * + * @return Object The object constructed from the given values. + */ + public static function constructFrom($values, $opts) + { + $obj = new static(isset($values['id']) ? $values['id'] : null); + $obj->refreshFrom($values, $opts); + return $obj; + } + + /** + * Refreshes this object using the provided values. + * + * @param array $values + * @param array $opts + * @param boolean $partial Defaults to false. + */ + public function refreshFrom($values, $opts, $partial = false) + { + $this->_opts = $opts; + + // Wipe old state before setting new. This is useful for e.g. updating a + // customer, where there is no persistent card parameter. Mark those values + // which don't persist as transient + if ($partial) { + $removed = new Util\Set(); + } else { + $removed = array_diff(array_keys($this->_values), array_keys($values)); + } + + foreach ($removed as $k) { + if (self::$permanentAttributes->includes($k)) { + continue; + } + + unset($this->$k); + } + + foreach ($values as $k => $v) { + if (self::$permanentAttributes->includes($k) && isset($this[$k])) { + continue; + } + + if (self::$nestedUpdatableAttributes->includes($k) && is_array($v)) { + $this->_values[$k] = AttachedObject::constructFrom($v, $opts); + } else { + $this->_values[$k] = Util\Util::convertToStripeObject($v, $opts); + } + + $this->_transientValues->discard($k); + $this->_unsavedValues->discard($k); + } + } + + /** + * @return array A recursive mapping of attributes to values for this object, + * including the proper value for deleted attributes. + */ + public function serializeParameters() + { + $params = array(); + if ($this->_unsavedValues) { + foreach ($this->_unsavedValues->toArray() as $k) { + $v = $this->$k; + if ($v === null) { + $v = ''; + } + + $params[$k] = $v; + } + } + + // Get nested updates. + foreach (self::$nestedUpdatableAttributes->toArray() as $property) { + if (isset($this->$property) && $this->$property instanceof Object) { + $params[$property] = $this->$property->serializeParameters(); + } + } + + return $params; + } + + public function __toJSON() + { + if (defined('JSON_PRETTY_PRINT')) { + return json_encode($this->__toArray(true), JSON_PRETTY_PRINT); + } else { + return json_encode($this->__toArray(true)); + } + } + + public function __toString() + { + $class = get_class($this); + return $class . ' JSON: ' . $this->__toJSON(); + } + + public function __toArray($recursive = false) + { + if ($recursive) { + return Util\Util::convertStripeObjectToArray($this->_values); + } else { + return $this->_values; + } + } +} + +Object::init(); diff --git a/include/library/Stripe/Plan.php b/include/library/Stripe/Plan.php new file mode 100755 index 000000000..12342d4e6 --- /dev/null +++ b/include/library/Stripe/Plan.php @@ -0,0 +1,60 @@ +_delete($params, $opts); + } + + /** + * @param array|string|null $opts + * + * @return Plan The saved plan. + */ + public function save($opts = null) + { + return $this->_save($opts); + } + + /** + * @param array|null $params + * @param array|string|null $opts + * + * @return Plan[] + */ + public static function all($params = null, $opts = null) + { + return self::_all($params, $opts); + } +} diff --git a/include/library/Stripe/Recipient.php b/include/library/Stripe/Recipient.php new file mode 100755 index 000000000..cdb785cd8 --- /dev/null +++ b/include/library/Stripe/Recipient.php @@ -0,0 +1,75 @@ +_save($opts); + } + + /** + * @param array|null $params + * + * @return Recipient The deleted recipient. + */ + public function delete($params = null, $opts = null) + { + return $this->_delete($params, $opts); + } + + + /** + * @param array|null $params + * + * @return array An array of the recipient's Transfers. + */ + public function transfers($params = null) + { + if ($params === null) { + $params = array(); + } + $params['recipient'] = $this->id; + $transfers = Transfer::all($params, $this->_opts); + return $transfers; + } +} diff --git a/include/library/Stripe/Refund.php b/include/library/Stripe/Refund.php new file mode 100755 index 000000000..d0a01aa19 --- /dev/null +++ b/include/library/Stripe/Refund.php @@ -0,0 +1,39 @@ +_save($opts); + } +} diff --git a/include/library/Stripe/SingletonApiResource.php b/include/library/Stripe/SingletonApiResource.php new file mode 100755 index 000000000..6dd7910bf --- /dev/null +++ b/include/library/Stripe/SingletonApiResource.php @@ -0,0 +1,31 @@ +refresh(); + return $instance; + } + + /** + * @return string The endpoint associated with this singleton class. + */ + public static function classUrl() + { + $base = static::className(); + return "/v1/${base}"; + } + + /** + * @return string The endpoint associated with this singleton API resource. + */ + public function instanceUrl() + { + return static::classUrl(); + } +} diff --git a/include/library/Stripe/Stripe.php b/include/library/Stripe/Stripe.php new file mode 100755 index 000000000..68e6346ea --- /dev/null +++ b/include/library/Stripe/Stripe.php @@ -0,0 +1,74 @@ +_delete($params, $opts); + } + + /** + * @param array|string|null $opts + * + * @return Subscription The saved subscription. + */ + public function save($opts = null) + { + return $this->_save($opts); + } + + /** + * @return Subscription The updated subscription. + */ + public function deleteDiscount() + { + $url = $this->instanceUrl() . '/discount'; + list($response, $opts) = $this->_request('delete', $url); + $this->refreshFrom(array('discount' => null), $opts, true); + } +} diff --git a/include/library/Stripe/Token.php b/include/library/Stripe/Token.php new file mode 100755 index 000000000..432e22bd8 --- /dev/null +++ b/include/library/Stripe/Token.php @@ -0,0 +1,28 @@ +instanceUrl() . '/reversals'; + list($response, $opts) = $this->request('post', $url, $params, $options); + $this->refreshFrom($response, $opts); + return $this; + } + + /** + * @return Transfer The canceled transfer. + */ + public function cancel() + { + $url = $this->instanceUrl() . '/cancel'; + list($response, $opts) = $this->_request('post', $url); + $this->refreshFrom($response, $opts); + return $this; + } + + /** + * @param array|string|null $opts + * + * @return Transfer The saved transfer. + */ + public function save($opts = null) + { + return $this->_save($opts); + } +} diff --git a/include/library/Stripe/TransferReversal.php b/include/library/Stripe/TransferReversal.php new file mode 100755 index 000000000..28fb776c7 --- /dev/null +++ b/include/library/Stripe/TransferReversal.php @@ -0,0 +1,39 @@ +_save($opts); + } +} diff --git a/include/library/Stripe/Util/RequestOptions.php b/include/library/Stripe/Util/RequestOptions.php new file mode 100755 index 000000000..14af2b8a0 --- /dev/null +++ b/include/library/Stripe/Util/RequestOptions.php @@ -0,0 +1,79 @@ +apiKey = $key; + $this->headers = $headers; + } + + /** + * Unpacks an options array and merges it into the existing RequestOptions + * object. + * @param array|string|null $options a key => value array + * + * @return RequestOptions + */ + public function merge($options) + { + $other_options = self::parse($options); + if ($other_options->apiKey === null) { + $other_options->apiKey = $this->apiKey; + } + $other_options->headers = array_merge($this->headers, $other_options->headers); + return $other_options; + } + + /** + * Unpacks an options array into an RequestOptions object + * @param array|string|null $options a key => value array + * + * @return RequestOptions + */ + public static function parse($options) + { + if ($options instanceof self) { + return $options; + } + + if (is_null($options)) { + return new RequestOptions(null, array()); + } + + if (is_string($options)) { + return new RequestOptions($options, array()); + } + + if (is_array($options)) { + $headers = array(); + $key = null; + if (array_key_exists('api_key', $options)) { + $key = $options['api_key']; + } + if (array_key_exists('idempotency_key', $options)) { + $headers['Idempotency-Key'] = $options['idempotency_key']; + } + if (array_key_exists('stripe_account', $options)) { + $headers['Stripe-Account'] = $options['stripe_account']; + } + if (array_key_exists('stripe_version', $options)) { + $headers['Stripe-Version'] = $options['stripe_version']; + } + return new RequestOptions($key, $headers); + } + + $message = 'The second argument to Stripe API method calls is an ' + . 'optional per-request apiKey, which must be a string, or ' + . 'per-request options, which must be an array. (HINT: you can set ' + . 'a global apiKey by "Stripe::setApiKey()")'; + throw new Error\Api($message); + } +} diff --git a/include/library/Stripe/Util/Set.php b/include/library/Stripe/Util/Set.php new file mode 100755 index 000000000..2a500cd63 --- /dev/null +++ b/include/library/Stripe/Util/Set.php @@ -0,0 +1,44 @@ +_elts = array(); + foreach ($members as $item) { + $this->_elts[$item] = true; + } + } + + public function includes($elt) + { + return isset($this->_elts[$elt]); + } + + public function add($elt) + { + $this->_elts[$elt] = true; + } + + public function discard($elt) + { + unset($this->_elts[$elt]); + } + + public function toArray() + { + return array_keys($this->_elts); + } + + public function getIterator() + { + return new ArrayIterator($this->toArray()); + } +} diff --git a/include/library/Stripe/Util/Util.php b/include/library/Stripe/Util/Util.php new file mode 100755 index 000000000..d52798a66 --- /dev/null +++ b/include/library/Stripe/Util/Util.php @@ -0,0 +1,102 @@ + $v) { + // FIXME: this is an encapsulation violation + if ($k[0] == '_') { + continue; + } + if ($v instanceof Object) { + $results[$k] = $v->__toArray(true); + } elseif (is_array($v)) { + $results[$k] = self::convertStripeObjectToArray($v); + } else { + $results[$k] = $v; + } + } + return $results; + } + + /** + * Converts a response from the Stripe API to the corresponding PHP object. + * + * @param array $resp The response from the Stripe API. + * @param array $opts + * @return Object|array + */ + public static function convertToStripeObject($resp, $opts) + { + $types = array( + 'account' => 'Stripe\\Account', + 'card' => 'Stripe\\Card', + 'charge' => 'Stripe\\Charge', + 'coupon' => 'Stripe\\Coupon', + 'customer' => 'Stripe\\Customer', + 'list' => 'Stripe\\Collection', + 'invoice' => 'Stripe\\Invoice', + 'invoiceitem' => 'Stripe\\InvoiceItem', + 'event' => 'Stripe\\Event', + 'file' => 'Stripe\\FileUpload', + 'token' => 'Stripe\\Token', + 'transfer' => 'Stripe\\Transfer', + 'plan' => 'Stripe\\Plan', + 'recipient' => 'Stripe\\Recipient', + 'refund' => 'Stripe\\Refund', + 'subscription' => 'Stripe\\Subscription', + 'fee_refund' => 'Stripe\\ApplicationFeeRefund', + 'bitcoin_receiver' => 'Stripe\\BitcoinReceiver', + 'bitcoin_transaction' => 'Stripe\\BitcoinTransaction', + ); + if (self::isList($resp)) { + $mapped = array(); + foreach ($resp as $i) { + array_push($mapped, self::convertToStripeObject($i, $opts)); + } + return $mapped; + } elseif (is_array($resp)) { + if (isset($resp['object']) && is_string($resp['object']) && isset($types[$resp['object']])) { + $class = $types[$resp['object']]; + } else { + $class = 'Stripe\\Object'; + } + return $class::constructFrom($resp, $opts); + } else { + return $resp; + } + } +} diff --git a/www/assets/js/tokenizeCard.js b/www/assets/js/tokenizeCard.js index c3a296286..a09255e54 100644 --- a/www/assets/js/tokenizeCard.js +++ b/www/assets/js/tokenizeCard.js @@ -3,34 +3,33 @@ console.log('App.tokenizeCard'); var processor = ( App.config.processor && App.config.processor.type ) ? App.config.processor.type : false; console.debug('Processor: ', processor); - var legacy_card = { - card_number: card.number, - expiration_month: card.expiration_month, - expiration_year: card.expiration_year, - security_code: null - }; - - if (App.isPhoneGap) { - //card = legacy_card; - } - switch(processor) { case 'stripe': - App.tokenizeCard_stripe( legacy_card, complete ); + App.tokenizeCard_stripe( card, complete ); break; case 'balanced': App.tokenizeCard_balanced( card, complete ); - break; + break; default: + App.alert('There was an error communicating with our payment processor.'); console.log( 'Processor error::', App.config.processor ); - break; + break; } }; App.tokenizeCard_stripe = function( card, complete ) { - var res = { status: false }; - var card = { number: card.card_number, exp_month: card.expiration_month, exp_year: card.expiration_year }; + var res = { + status: false + }; + + var card = { + number: card.number, + exp_month: card.expiration_month, + exp_year: card.expiration_year + }; + Stripe.card.createToken( card , function( status, response ){ + console.debug('Recieved response from stripe: ', status, response); if ( response.error ) { switch( response.error.code ){ case 'incorrect_number': @@ -70,7 +69,7 @@ App.tokenizeCard_stripe = function( card, complete ) { id : response.card.id, uri: response.id, lastfour: response.card.last4, - card_type: response.card.type, + card_type: response.card.brand.toLowerCase(), month: response.card.exp_month, year: response.card.exp_year, status : true