commit 0e1cd66d2de3f2ee2f519c4a6bbbed3c9a2507aa Author: Matt Date: Sat Feb 18 18:52:44 2017 -0600 Fork 7.x-2.x-dev from drupal.org. diff --git a/LICENSE.txt b/LICENSE.txt new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/LICENSE.txt @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/README.txt b/README.txt new file mode 100644 index 0000000..2e42089 --- /dev/null +++ b/README.txt @@ -0,0 +1,93 @@ +This is an Ubercart payment gateway module for Stripe. + +Versions of the Stripe PHP Library and Stripe API that this module currently +supports are found in uc_stripe_libraries_info() in uc_stripe.module. + +Installation and Setup +====================== + +a) Install and enable the module in the normal way for Drupal. + +b) Visit your Ubercart Store Administration page, Configuration +section, and enable the gateway under the Payment Gateways. +(admin/store/settings/payment/edit/gateways) + +c) On that page, provide your Stripe API keys, from +https://dashboard.stripe.com/account/apikeys + +d) Download and install the Stripe PHP Library version 2.2.0 or >=3.13.0 +from https://github.com/stripe/stripe-php/releases. The recommended technique is +to use the command + +drush ldl stripe + +If you don't use "drush ldl stripe", download and install the Stripe library in +sites/all/libraries/stripe such that the path to VERSION +is sites/all/libraries/stripe/VERSION. YOU MUST CLEAR THE CACHE AFTER +CHANGING THE STRIPE PHP LIBRARY. The Libraries module caches its memory of +libraries like the Stripe Library. (Version 2.2.0 support is maintained for +existing users; version 3.13.0+ supports PHP 7 and will get ongoing support.) +(With the latest version of the libraries module you can use the command: + +e) If you are using recurring payments, install version 2.x +of the Ubercart Recurring module: +http://drupal.org/project/uc_recurring +and set up as described below. + +f) Every site dealing with credit cards in any way should be using https. It's +your responsibility to make this happen. (Actually, almost every site should +be https everywhere at this time in the web's history.) + +g) If you want Stripe to attempt to validate zip/postal codes, you must enable +that feature on your *Stripe* account settings. Click the checkbox for +"Decline Charges that fail zip code verification" on the "Account info" page. +(You must be collecting billing address information for this to work, of course.) + +h) The uc_credit setting "Validate credit card numbers at checkout" must be +disabled on admin/store/settings/payment/method/credit - uc_credit never sees +the credit card number, so cannot properly validate it (and we don't want it to +ever know the credit card number.) + +Upgrading from uc_stripe 6.x-1.x or 7.x-1.x +=========================================== + +7.x-2.x does not use Stripe subscriptions for recurring payments, but instead +uses the uc_recurring module. This means you have control of recurring +transactions without having to manage them on the Stripe dashboard. (Credit +card numbers and sensitive data are *not* stored on your site; only the Stripe +customer ID is stored.) + +The upgrade hooks, however, must move the customer id stored in the obsolete +uc_recurring_stripe table into the user table. When this happens the old +record in the uc_recurring_stripe table will have its plan changed to +_obsolete. This just prevents an import from happening more than once +and gives you backout options if you wanted to downgrade. + +If you were using 1.x of this module and want to cancel existing subscriptions +which were configured via the Stripe api (since they are now managed via +uc_recurring) a drush command is provided to cancel these. Use "drush help subcancel" +for more information. + +Recurring Payments Setup +======================== + +You'll need the Ubercart Recurring module: +http://drupal.org/project/uc_recurring installed. It is not +listed as a dependency for this Stripe payment module because +this module could be used without recurring payments. +But it is a dependency to use the recurring payments piece of +this module. Note that this module does *not* use Stripe subscriptions. +Instead, recurring payments are managed by uc_recurring, which does not +retain any valid CC info, only the stripe customer id. + +Recurring payments require automatically triggered renewals using +uc_recurring_trigger_renewals ("Enabled triggered renewals" must be enabled +on admin/store/settings/payment/edit/recurring) + +If you were using Stripe subscriptions in v1 of this module, you may have to +disable those subscriptions in order to not double-charge your customers. + +uc_stripe 6.x-2.x and 7.x-2.x were based on Bitcookie's work (thanks!) which was posted at +http://bitcookie.com/blog/pci-compliant-ubercart-and-stripe-js +from discussion in the uc_stripe issue queue, +https://www.drupal.org/node/1467886 diff --git a/css/uc_stripe.css b/css/uc_stripe.css new file mode 100644 index 0000000..0f5fa0a --- /dev/null +++ b/css/uc_stripe.css @@ -0,0 +1,32 @@ +.poweredbystripe { + margin-top: 10px; + float: right; + background: #017aff; + filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#78b8ff', endColorstr='#017aff'); + background-image: -moz-linear-gradient(-90deg, #78b8ff, #017aff); + background-image: -webkit-gradient(linear, left top, left bottom, from(#78b8ff), to(#017aff)); + height: 27px; + padding: 0 10px; + line-height: 27px; + display: block; + color: #fff; + vertical-align: top; + font: bold 12px Arial, Helvetica, Verdana, sans-serif; + font-weight: bold; + font-size: 12px; + text-shadow: 0 -1px 0 rgba(0,0,0,0.5); + -moz-border-radius: 30px; + -webkit-border-radius: 30px; + border-radius: 30px; + -moz-box-shadow: inset 0 1px 0 #98c9ff; + -webkit-box-shadow: inset 0 1px 0 #98c9ff; + box-shadow: inset 0 1px 0 #98c9ff; +} + +a.poweredbylink:hover { + text-decoration: none; +} + +#uc_stripe_messages.hidden {display: none;} + +.stripe-warning {color: red; font-style: oblique; } diff --git a/images/logo_bare.png b/images/logo_bare.png new file mode 100644 index 0000000..139b9c0 Binary files /dev/null and b/images/logo_bare.png differ diff --git a/js/uc_stripe.js b/js/uc_stripe.js new file mode 100644 index 0000000..d30b53a --- /dev/null +++ b/js/uc_stripe.js @@ -0,0 +1,171 @@ +/** + * @file + * uc_stripe.js + * + * Handles all interactions with Stripe on the client side for PCI-DSS compliance + */ +(function ($) { + + Drupal.behaviors.uc_stripe = { + attach: function (context) { + + // Map stripe names to (partial) Ubercart field names; Ubercart names add "billing_" or "shipping_" on the front. + const address_field_mapping = { + "address_line1": "street1", + "address_line2": "street2", + "address_city": "city", + "address_state": "zone", + "address_zip": "postal_code", + "address_country": "country" + }; + var submitButton = $('.uc-cart-checkout-form #edit-continue'); + + var cc_container = $('.payment-details-credit'); + var cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); + var cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); + + // Make sure that when the page is being loaded the token value is reset + // Browser or other caching might do otherwise. + $("[name='panes[payment][details][stripe_token]']").val('default'); + + $('span#stripe-nojs-warning').parent().hide(); + + // JS must enable the button; otherwise form might disclose cc info. It starts disabled + submitButton.attr('disabled', false); + + // When this behavior fires, we can clean the form so it will behave properly, + // Remove 'name' from sensitive form elements so there's no way they can be submitted. + cc_num.removeAttr('name').removeAttr('disabled'); + $('div.form-item-panes-payment-details-cc-number').removeClass('form-disabled'); + cc_cvv.removeAttr('name').removeAttr('disabled'); + var cc_val_val = cc_num.val(); + if (cc_val_val && cc_val_val.indexOf('Last 4')) { + cc_num.val(''); + } + + submitButton.click(function (e) { + + // We must find the various fields again, because they may have been swapped + // in by ajax action of the form. + cc_container = $('.payment-details-credit'); + cc_num = cc_container.find(':input[id*="edit-panes-payment-details-cc-numbe"]'); + cc_cvv = cc_container.find(':input[id*="edit-panes-payment-details-cc-cv"]'); + + // If not credit card processing or no token field, just let the submit go on + // Also continue if we've received the tokenValue + var tokenField = $("[name='panes[payment][details][stripe_token]']"); + if (!$("div.payment-details-credit").length || !tokenField.length || tokenField.val().indexOf('tok_') == 0) { + return true; + } + + // If we've requested and are waiting for token, prevent any further submit + if (tokenField.val() == 'requested') { + return false; // Prevent any submit processing until token is received + } + + // Go ahead and request the token + tokenField.val('requested'); + + try { + var name = undefined; + + if ($(':input[name="panes[billing][billing_first_name]"]').length) { + name = $(':input[name="panes[billing][billing_first_name]"]').val() + " " + $(':input[name="panes[billing][billing_last_name]"]').val(); + } + if (typeof name === "undefined" && $(':input[name="panes[delivery][delivery_first_name]"]').length) { + name = $(':input[name="panes[delivery][delivery_first_name]"]').val() + " " + $(':input[name="panes[delivery][delivery_last_name]"]').val(); + } + + var params = { + number: cc_num.val(), + cvc: cc_cvv.val(), + exp_month: $(':input[name="panes[payment][details][cc_exp_month]"]').val(), + exp_year: $(':input[name="panes[payment][details][cc_exp_year]"]').val(), + name: name + }; + + // Translate the Ubercart billing/shipping fields to Stripe values + for (var key in address_field_mapping) { + const prefixes = ['billing', 'delivery']; + for (var i = 0; i < prefixes.length; i++) { + var prefix = prefixes[i]; + var uc_field_name = prefix + '_' + address_field_mapping[key]; + var location = ':input[name="panes[' + prefix + '][' + uc_field_name + ']"]'; + if ($(location).length) { + params[key] = $(location).val(); + if ($(location).attr('type') == 'select-one') { + params[key] = $(location + " option:selected").text(); + } + break; // break out of billing/shipping loop because we got the info + } + } + } + + Stripe.createToken(params, function (status, response) { + + if (response.error) { + + // Show the errors on the form + $('#uc_stripe_messages') + .removeClass("hidden") + .text(response.error.message); + $('#edit-stripe-messages').val(response.error.message); + + // Make the fields visible again for retry + cc_num + .css('visibility', 'visible') + .val('') + .attr('name', 'panes[payment][details][cc_number]'); + cc_cvv + .css('visibility', 'visible') + .val('') + .attr('name', 'panes[payment][details][cc_cvv]'); + + + // Turn off the throbber + $('.ubercart-throbber').remove(); + // Remove the bogus copy of the submit button added in uc_cart.js ucSubmitOrderThrobber + submitButton.next().remove(); + // And show the hidden original button which has the behavior attached to it. + submitButton.show(); + + tokenField.val('default'); // Make sure token field set back to default + + } else { + // token contains id, last4, and card type + var token = response.id; + + // Insert the token into the form so it gets submitted to the server + tokenField.val(token); + + // Since we're now submitting, make sure that uc_credit doesn't + // find values it objects to; after "fixing" set the name back on the + // form element. + cc_num + .css('visibility', 'hidden') + .val('555555555555' + response.card.last4) + .attr('name', 'panes[payment][details][cc_number]'); + cc_cvv + .css('visibility', 'hidden') + .val('999') + .attr('name', 'panes[payment][details][cc_cvv]'); + + // now actually submit to Drupal. The only "real" things going + // are the token and the expiration date. + submitButton.click(); + } + }); + } catch (e) { + $('#uc_stripe_messages') + .removeClass("hidden") + .text(e.message); + $('#edit-stripe-messages').val(e.message); + } + + // Prevent processing until we get the token back + return false; + }); + } + }; + +}(jQuery)); diff --git a/uc_stripe.drush.inc b/uc_stripe.drush.inc new file mode 100644 index 0000000..bf85856 --- /dev/null +++ b/uc_stripe.drush.inc @@ -0,0 +1,99 @@ + "Delete subscriptions created by uc_stripe v1 and existing in uc_recurring_stripe table.", + 'arguments' => array( + 'order_id' => 'Order ID from uc_stripe_recurring table', + ), + 'options' => array( + 'dry-run' => 'Dry run - does not actually execute, but show what would be done', + ), + 'examples' => array( + 'drush subscription-cancel', + 'drush subscription-cancel --dry-run', + 'drush subscription-cancel ', + ), + 'aliases' => array('subcancel'), + ); + + return $items; +} + + +/** + * Command callback + * + * Delete active subscriptions found in uc_recurring_stripe table. + */ +function drush_uc_stripe_subscription_cancel($order_id = 'all') { + + $dry_run = drush_get_option('dry-run'); + + if (!db_table_exists('uc_recurring_stripe')) { + drush_print('No uc_recurring_stripe table exists, exiting.'); + return; + } + + $library = libraries_load('stripe'); + $info = libraries_info('stripe'); + if (!$library['loaded'] || !array_key_exists(\Stripe\Stripe::VERSION, $info['versions'])) { + drush_print("Failed to load one of the supported Stripe PHP library versions: " . join(', ', array_keys($info['versions']))); + return; + } + + if ($dry_run) { + drush_print('dry-run is set so not deleting any subscriptions'); + } + _uc_stripe_prepare_api(); + $query = db_select('uc_recurring_stripe', 'urs', array('fetch' => PDO::FETCH_ASSOC)) + ->fields('urs', array('uid', 'customer_id', 'order_id' )); + + if ($order_id != 'all') { + $query->condition('order_id', ':id'); + } + $result = $query->execute(); + + foreach ($result as $item) { + $customer = NULL; + $account = user_load($item['uid']); + + try { + $customer = \Stripe\Customer::retrieve($item['customer_id']); + $subscriptions = $customer->subscriptions->all(); + } catch (Exception $e) { + drush_print("Failed to retrieve customer subscriptions for {$item['customer_id']} (user:$account->name}. Message: {$e->getMessage()}"); + continue; + } + + $count = count($subscriptions['data']); + + $msg = "Retrieved Stripe Customer with $count subscriptions, uid:{$item['uid']} username:{$account->name} order:{$item['order_id']} stripe customer id:{$item['customer_id']}"; + drush_print($msg); + foreach ($subscriptions['data'] as $sub) { + drush_print(" Subscription: {$sub->id} {$sub->status} {$sub->plan->name} {$sub->plan->id}"); + if (empty($dry_run) && drush_confirm('Are you sure you want to cancel this subscription?')) { + try { + $sub->cancel(); + drush_print(" Cancelled subscription {$sub->id}"); + } catch (Exception $e) { + drush_print(" FAILED to cancel subscription {$sub->id} Message: {$e->getMessage()}"); + } + } + } + } +} diff --git a/uc_stripe.info b/uc_stripe.info new file mode 100644 index 0000000..cf4cc9f --- /dev/null +++ b/uc_stripe.info @@ -0,0 +1,16 @@ +name = Ubercart Stripe +description = Ubercart payment gateway for Stripe. +dependencies[] = uc_payment +dependencies[] = uc_credit +dependencies[] = libraries +package = Ubercart - payment +core = 7.x +php = 5.3 + + +; Information added by Drupal.org packaging script on 2016-10-03 +version = "7.x-2.2+1-dev" +core = "7.x" +project = "uc_stripe" +datestamp = "1475516941" + diff --git a/uc_stripe.install b/uc_stripe.install new file mode 100644 index 0000000..95d8d0f --- /dev/null +++ b/uc_stripe.install @@ -0,0 +1,183 @@ + $t('cURL'), + 'value' => $has_curl ? $t('Enabled') : $t('Not found'), + ); + + if (!$has_curl) { + $requirements['uc_stripe_curl']['severity'] = REQUIREMENT_ERROR; + $requirements['uc_stripe_curl']['description'] = $t("The Stripe API requires the PHP cURL library.", array('!curl_url' => 'http://php.net/manual/en/curl.setup.php')); + } + + // libraries_info() doesn't work until the module is installed, so allow to be + // installed without the library. Then warn at that point. + if ($phase != 'install') { + $php_api_version = _uc_stripe_load_library(); + $requirements['uc_stripe_api'] = array( + 'title' => $t('Stripe PHP Library'), + 'value' => $t('Version !version', array('!version' => $php_api_version)), + ); + if (empty($php_api_version)) { + $requirements['uc_stripe_api']['value'] = $t('Not installed or wrong version'); + $requirements['uc_stripe_api']['severity'] = REQUIREMENT_ERROR; + $requirements['uc_stripe_api']['description'] = $t('Please install a compatible version of the Stripe PHP Library versions as described in the README.txt'); + } + } + + + $requirements['uc_stripe_keys'] = array( + 'title' => $t('Stripe API Keys'), + 'value' => $t('Configured'), + ); + if ($phase == 'runtime' && !_uc_stripe_check_api_keys()) { + $requirements['uc_stripe_keys']['title'] = $t('Stripe API Keys.'); + $requirements['uc_stripe_keys']['value'] = $t('Not configured'); + $requirements['uc_stripe_keys']['severity'] = REQUIREMENT_ERROR; + $requirements['uc_stripe_keys']['description'] = $t('The Stripe API keys are not fully configured.'); + } + + // Make sure they don't enable the "Check credit" + if ($phase == 'runtime' && variable_get('uc_credit_validate_numbers', FALSE)) { + $requirements['uc_stripe_validate_numbers'] = array( + 'title' => t('Stripe Credit Card Validation'), + 'value' => t('Enabled'), + 'severity' => REQUIREMENT_ERROR, + 'description' => t("uc_credit's 'Validate credit card numbers at checkout' option must be disabled when using uc_stripe, as uc_credit never sees the card number."), + ); + } + + return $requirements; +} + +/** + * Load the PHP API + * + * @return Stripe version number as string or FALSE if failed to load + */ +function _uc_stripe_load_library() { + module_load_include('module', 'uc_stripe'); + + if (($library = libraries_load('stripe')) && !empty($library['loaded']) && class_exists('\Stripe\Stripe')) { + return \Stripe\Stripe::VERSION; + } + watchdog('uc_stripe', 'Stripe PHP Library not installed or wrong version'); + return FALSE; + +} + +/** + * Implements hook_install(). + */ +function uc_stripe_install() { + // This turns ON the uc_recurring cron task to renew. We want this + // ON because the renewal payments are handled by uc_recurring and NOT the stripe gateway + variable_set('uc_recurring_trigger_renewals', TRUE); + + // Stripe does cc validation, so uc_credit must not... especially because + // uc_credit gets a bogus cc number. + variable_set('uc_credit_validate_numbers', FALSE); +} + +/** + * Enable triggered renewals, as uc_recurring manages renewals with this version. + */ +function uc_stripe_update_7201(&$sandbox) { + variable_set('uc_recurring_trigger_renewals', TRUE); + variable_set('uc_credit_validate_numbers', FALSE); + + return 'Enabled uc_recurring triggered renewals (uc_recurring_trigger_renewals) and required uc_checkout_skip_review'; +} + +/** + * Move customer IDs from uc_recurring_stripe into account + */ +function uc_stripe_update_7202(&$sandbox) { + $ret = array(); + $sandbox['per_run'] = 100; // users per run + $sandbox['#finished'] = 0; + + if (db_table_exists('uc_recurring_stripe')) { + if (!isset($sandbox['progress'])) { + $sandbox['progress'] = 0; + $sandbox['max'] = db_query(' + SELECT COUNT(DISTINCT(u.uid)) + FROM {users} u JOIN {uc_recurring_stripe} urs + ON (u.uid = urs.uid) + WHERE urs.active = 1 + ')->fetchField(); + } + + + _uc_stripe_move_customer_id($sandbox); + return "Updated {$sandbox['progress']} of {$sandbox['max']} uc_recurring_stripe rows into user objects"; + } + else { + return 'Old uc_recurring_stripe table did not exist, no action taken.'; + } +} + +/** + * Make sure cached library information is not used. + */ +function uc_stripe_update_7203(&$sandbox) { + cache_clear_all('stripe', 'cache_libraries'); +} + +/** + * Move customer ids from uc_recurring_stripe into user account + */ +function _uc_stripe_move_customer_id(&$sandbox) { + + // Find the users with stripe customer ids that are active + $query = ' + SELECT DISTINCT(urs.uid) + FROM {users} u JOIN {uc_recurring_stripe} urs + ON (u.uid = urs.uid) + WHERE urs.active = 1'; + $result = db_query_range($query, 0, $sandbox['per_run'], + array(), + array('fetch' => PDO::FETCH_ASSOC)); + + foreach ($result as $item) { + + $sandbox['progress']++; + $stripe_customer_id = db_query_range(' + SELECT urs.customer_id + FROM {uc_recurring_stripe} urs + WHERE urs.uid = :uid AND urs.active = 1 + ORDER BY urs.rfid DESC + ', 0, 1, array(':uid' => $item['uid']))->fetchField(); + $account = user_load($item['uid']); + // Set to inactive every subscription for this uid + db_update('uc_recurring_stripe') + ->fields( + array('active' => 0) + ) + ->condition('uid', $item['uid']) + ->execute(); + + if (empty($account->data['uc_stripe_customer_id'])) { + user_save($account, array('data' => array('uc_stripe_customer_id' => $stripe_customer_id))); + } + } + + if ($sandbox['progress'] >= $sandbox['max'] || $result->rowCount() == 0) { + $sandbox['#finished'] = 1; + } else { + $sandbox['#finished'] = $sandbox['progress'] / $sandbox['max']; + } +} diff --git a/uc_stripe.module b/uc_stripe.module new file mode 100644 index 0000000..b39552e --- /dev/null +++ b/uc_stripe.module @@ -0,0 +1,785 @@ + 'Stripe PHP Library', + 'vendor url' => 'http://stripe.com', + 'download url' => 'https://github.com/stripe/stripe-php/releases', + 'download file url' => 'https://github.com/stripe/stripe-php/archive/v3.20.0.tar.gz', + 'version arguments' => array( + 'file' => 'VERSION', + 'pattern' => '/(\d+\.\d+\.\d+)/', + ), + 'versions' => array( + '2.2.0' => array( + 'files' => array( + 'php' => array( + 'lib/Util/RequestOptions.php', + 'lib/Util/Set.php', + 'lib/Util/Util.php', + 'lib/Object.php', + 'lib/ApiResource.php', + 'lib/Account.php', + 'lib/ExternalAccount.php', + 'lib/AlipayAccount.php', + 'lib/ApiRequestor.php', + 'lib/ApplicationFee.php', + 'lib/ApplicationFeeRefund.php', + 'lib/AttachedObject.php', + 'lib/SingletonApiResource.php', + 'lib/Balance.php', + 'lib/BalanceTransaction.php', + 'lib/BankAccount.php', + 'lib/BitcoinReceiver.php', + 'lib/BitcoinTransaction.php', + 'lib/Card.php', + 'lib/Charge.php', + 'lib/Collection.php', + 'lib/Coupon.php', + 'lib/Customer.php', + 'lib/Error/Base.php', + 'lib/Error/Api.php', + 'lib/Error/ApiConnection.php', + 'lib/Error/Authentication.php', + 'lib/Error/Card.php', + 'lib/Error/InvalidRequest.php', + 'lib/Error/RateLimit.php', + 'lib/Event.php', + 'lib/FileUpload.php', + 'lib/HttpClient/ClientInterface.php', + 'lib/HttpClient/CurlClient.php', + 'lib/Invoice.php', + 'lib/InvoiceItem.php', + 'lib/Plan.php', + 'lib/Recipient.php', + 'lib/Refund.php', + 'lib/Stripe.php', + 'lib/Subscription.php', + 'lib/Token.php', + 'lib/Transfer.php', + 'lib/TransferReversal.php', + ), + ), + 'stripe_api_version' => '2015-06-15', + ), + '3.0' => array( + 'files' => array( + 'php' => array( + 'init.php', + ), + ), + 'stripe_api_version' => '2016-03-07', + ), + ), + ); + + return $libraries; +} + +/** + * Implements hook_payment_gateway to register this payment gateway + * @return array + */ +function uc_stripe_uc_payment_gateway() { + $gateways = array(); + $gateways[] = array( + 'id' => 'uc_stripe', + 'title' => t('Stripe Gateway'), + 'description' => t('Process card payments using Stripe JS.'), + 'settings' => 'uc_stripe_settings_form', + 'credit' => 'uc_stripe_charge', + ); + return $gateways; +} + +/** + * Implements hook_recurring_info() to integrate with uc_recurring + * + * @return mixed + */ +function uc_stripe_recurring_info() { + $items['uc_stripe'] = array( + 'name' => t('Stripe'), + 'payment method' => 'credit', + 'module' => 'uc_recurring', + 'fee handler' => 'uc_stripe', + 'process callback' => 'uc_stripe_process', + 'renew callback' => 'uc_stripe_renew', + 'cancel callback' => 'uc_stripe_cancel', + 'own handler' => FALSE, + 'menu' => array( + 'charge' => UC_RECURRING_MENU_DEFAULT, + 'edit' => UC_RECURRING_MENU_DEFAULT, + 'cancel' => UC_RECURRING_MENU_DEFAULT, + ), + ); + return $items; +} + + +/** + * Implements hook_form_FORMID_alter() to change the checkout form + * All work as a result is done in JS, the ordinary post does not happen. + * + * @param $form + * @param $form_state + * @param $form_id + */ +function uc_stripe_form_uc_cart_checkout_form_alter(&$form, &$form_state) { + + $payment_form = &$form['panes']['payment']['details']; + + $payment_form['stripe_nojs_warning'] = array( + '#type' => 'item', + '#markup' => '' . t('Sorry, for security reasons your card cannot be processed because Javascript is disabled in your browser.') . '', + '#weight' => -1000, + ); + + $payment_form['stripe_token'] = array( + '#type' => 'hidden', + '#default_value' => 'default', + '#attributes' => array( + 'id' => 'edit-panes-payment-details-stripe-token', + ), + ); + + // Prevent form Credit card fill and submission if javascript has not removed + // the "disabled" attributes.. + // If JS happens to be disabled, we don't want user to be able to enter CC data. + // Note that we can't use '#disabled', as it causes Form API to discard all input, + // so use the disabled attribute instead. + $form['panes']['payment']['details']['cc_number']['#attributes']['disabled'] = 'disabled'; + if (empty($form['actions']['continue']['#attributes'])) { + $form['actions']['continue']['#attributes'] = array(); + } + $form['actions']['continue']['#attributes']['disabled'] = 'disabled'; + + $apikey = variable_get('uc_stripe_testmode', TRUE) ? check_plain(variable_get('uc_stripe_api_key_test_publishable', '')) : check_plain(variable_get('uc_stripe_api_key_live_publishable', '')); + + + // Add custom JS and CSS + $form['#attached']['js']['https://js.stripe.com/v2/'] = array('type' => 'external'); + $form['#attached']['js'][] = array('data' => "Stripe.setPublishableKey('$apikey')", 'type' => 'inline'); + $form['#attached']['js'][] = drupal_get_path('module', 'uc_stripe') . '/js/uc_stripe.js'; + $form['#attached']['css'][] = drupal_get_path('module', 'uc_stripe') . '/css/uc_stripe.css'; + + // Add custom submit which will do saving away of token during submit. + $form['#submit'][] = 'uc_stripe_checkout_form_customsubmit'; + + // Add a section for stripe.js error messages (CC validation, etc.) + $form['panes']['messages'] = array( + '#markup' => "", + ); + + if (uc_credit_default_gateway() == 'uc_stripe') { + if (variable_get('uc_stripe_testmode', TRUE)) { + $form['panes']['testmode'] = array( + '#prefix' => "
", + '#markup' => t("Test mode is ON for the Stripe Payment Gateway. Your card will not be charged. To change this setting, edit the !link", array('!link' => l("Stripe settings", "admin/store/settings/payment/method/credit"))), + '#suffix' => "
", + ); + } + } +} + + +/** + * Implements hook_order_pane to provide the stripe customer info + * + * @return array + */ +function uc_stripe_uc_order_pane() { + $panes[] = array( + 'id' => 'uc_stripe', + 'callback' => 'uc_stripe_order_pane_stripe', + 'title' => t('Stripe Customer Info'), + 'desc' => t("Stripe Information"), + 'class' => 'pos-left', + 'weight' => 3, + 'show' => array('view', 'edit'), + ); + + return $panes; +} + +/** + * Implements hook_uc_checkout_complete() + * + * Saves stripe customer_id into the user->data object + * + * @param $order + * @param $account + */ +function uc_stripe_uc_checkout_complete($order, $account) { + + if ($order->payment_method == "credit") { + // Pull the stripe customer ID from the session. + // It got there in uc_stripe_checkout_form_customsubmit() + $stripe_customer_id = $_SESSION['stripe']['customer_id']; + + $loaded_user = user_load($account->uid); + user_save($loaded_user, array('data' => array('uc_stripe_customer_id' => $stripe_customer_id))); + } +} + + +/** + * Implements uc_order_pane_callback() specified in 'callback' of + * uc_stripe_uc_order_pane() + * + * Returns text for customer id for order pane. + * + * @param $op + * @param $order + * @param $form + * @param $form_state + * @return array + */ +function uc_stripe_order_pane_stripe($op, $order, &$form = NULL, &$form_state = NULL) { + switch ($op) { + case 'view': + $stripe_customer_id = _uc_stripe_get_customer_id($order->uid); + $output = t("Customer ID: ") . $stripe_customer_id; + return array('#markup' => $output); + default: + return; + } +} + +/** + * Provide configuration form for uc_stripe + * + * @return mixed + */ +function uc_stripe_settings_form() { + $form['uc_stripe_settings'] = array( + '#type' => 'fieldset', + '#title' => t('Stripe settings'), + ); + + $form['uc_stripe_settings']['uc_stripe_api_key_test_secret'] = array( + '#type' => 'textfield', + '#title' => t('Test Secret Key'), + '#default_value' => variable_get('uc_stripe_api_key_test_secret', ''), + '#description' => t('Your Development Stripe API Key. Must be the "secret" key, not the "publishable" one.'), + ); + + $form['uc_stripe_settings']['uc_stripe_api_key_test_publishable'] = array( + '#type' => 'textfield', + '#title' => t('Test Publishable Key'), + '#default_value' => variable_get('uc_stripe_api_key_test_publishable', ''), + '#description' => t('Your Development Stripe API Key. Must be the "publishable" key, not the "secret" one.'), + ); + + $form['uc_stripe_settings']['uc_stripe_api_key_live_secret'] = array( + '#type' => 'textfield', + '#title' => t('Live Secret Key'), + '#default_value' => variable_get('uc_stripe_api_key_live_secret', ''), + '#description' => t('Your Live Stripe API Key. Must be the "secret" key, not the "publishable" one.'), + ); + + $form['uc_stripe_settings']['uc_stripe_api_key_live_publishable'] = array( + '#type' => 'textfield', + '#title' => t('Live Publishable Key'), + '#default_value' => variable_get('uc_stripe_api_key_live_publishable', ''), + '#description' => t('Your Live Stripe API Key. Must be the "publishable" key, not the "secret" one.'), + ); + + $form['uc_stripe_settings']['uc_stripe_testmode'] = array( + '#type' => 'checkbox', + '#title' => t('Test mode'), + '#description' => 'Testing Mode: Stripe will use the development API key to process the transaction so the card will not actually be charged.', + '#default_value' => variable_get('uc_stripe_testmode', TRUE), + ); + + $form['uc_stripe_settings']['uc_stripe_poweredby'] = array( + '#type' => 'checkbox', + '#title' => t('Powered by Stripe'), + '#description' => 'Show "powered by Stripe" in shopping cart.', + '#default_value' => variable_get('uc_stripe_poweredby', FALSE), + ); + + return $form; +} + +/** + * Implements hook_form_FORMID_alter() + * + * Add validation function for stripe settings + * + * @param $form + * @param $form_state + */ +function uc_stripe_form_uc_payment_method_settings_form_alter(&$form, &$form_state) { + $form['#validate'][] = 'uc_stripe_settings_form_validate'; +} + +/** + * Validation function and normalize keys (trim spaces) + * + * @param $form + * @param $form_state + */ +function uc_stripe_settings_form_validate($form, &$form_state) { + $elements = array('uc_stripe_api_key_test_secret', 'uc_stripe_api_key_test_publishable', 'uc_stripe_api_key_live_secret', 'uc_stripe_api_key_live_publishable', ); + + if ($form_state['values']['uc_pg_uc_stripe_enabled']) { + foreach ($elements as $element_name) { + $form_state['values'][$element_name] = _uc_stripe_sanitize_key($form_state['values'][$element_name]); + if (!_uc_stripe_validate_key($form_state['values'][$element_name])) { + form_set_error($element_name, t('@name does not appear to be a valid stripe key', array('@name' => $element_name))); + } + } + } + + // Make sure they haven't tried to validate credit card numbers, as uc_stripe will not provide a real one. + if (!empty($form_state['values']['uc_credit_validate_numbers'])) { + form_set_error('uc_credit_validate_numbers', t('When used with Ubercart Stripe, "Validate credit card number at checkout" must be unchecked.')); + } + +} + +/** + * Sanitize and strip whitespace from Stripe keys + * + * @param $key + */ +function _uc_stripe_sanitize_key($key) { + $key = trim($key); + $key = check_plain($key); + return $key; +} + +/** + * Validate Stripe key + * + * @param $key + * @return boolean + */ +function _uc_stripe_validate_key($key) { + $valid = preg_match('/^[a-zA-Z0-9_]+$/', $key); + return $valid; +} + +/** + * Custom submit function to store the stripe token + * + * Since we don't have a user account at this step, we're going to store the token + * in the session. We'll grab the token in the charge callback and use it to charge + * + * @param $form + * @param $form_state + */ +function uc_stripe_checkout_form_customsubmit($form, &$form_state) { + // This submit may be entered on another payment type, so don't set session in that case. + if (!empty($form_state['values']['panes']['payment']['details']['stripe_token'])) { + $_SESSION['stripe']['token'] = $form_state['values']['panes']['payment']['details']['stripe_token']; + } +} + + +/** + * Generic "charge" callback that runs on checkout and via the order's "card" terminal + * + * @param $order_id + * @param $amount + * @param $data + * @return array + */ +function uc_stripe_charge($order_id, $amount, $data) { + global $user; + + // Load the stripe PHP API + + if (!_uc_stripe_prepare_api()) { + $result = array( + 'success' => FALSE, + 'comment' => t('Stripe API not found.'), + 'message' => t('Stripe API not found. Contact the site administrator.'), + 'uid' => $user->uid, + 'order_id' => $order_id, + ); + return $result; + } + + $order = uc_order_load($order_id); + + $context = array( + 'revision' => 'formatted-original', + 'type' => 'amount', + ); + + $options = array( + 'sign' => FALSE, + 'thou' => FALSE, + 'dec' => FALSE, + 'prec' => 2, + ); + + // Format the amount in cents, which is what Stripe wants + $amount = uc_currency_format($amount, FALSE, FALSE, FALSE); + + $stripe_customer_id = FALSE; + + // If the user running the order is not the order's owner + // (like if an admin is processing an order on someone's behalf) + // then load the customer ID from the user object. + // Otherwise, make a brand new customer each time a user checks out. + if ($user->uid != $order->uid) { + $stripe_customer_id = _uc_stripe_get_customer_id($order->uid); + } + + + // Always Create a new customer in stripe for new orders + + if (!$stripe_customer_id) { + + try { + // If the token is not in the user's session, we can't set up a new customer + if (empty($_SESSION['stripe']['token'])) { + throw new Exception('Token not found'); + } + $stripe_token = $_SESSION['stripe']['token']; + + $shipping_info = array(); + if (!empty($order->delivery_postal_code)) { + $shipping_info = array( + 'name' => @"{$order->delivery_first_name} {$order->delivery_last_name}", + 'phone' => @$order->delivery_phone, + ); + + $delivery_country = uc_get_country_data(array('country_id' => $order->delivery_country)); + if ($delivery_country === FALSE) { + $delivery_country = array(0 => array('country_iso_code_2' => 'US')); + } + + $shipping_info['address'] = array( + 'city' => @$order->delivery_city, + 'country' => @$delivery_country[0]['country_iso_code_2'], + 'line1' => @$order->delivery_street1, + 'line2' => @$order->delivery_street2, + 'postal_code' => @$order->delivery_postal_code, + ); + } + + $params = array( + "source" => $stripe_token, + 'description' => "OrderID: {$order->order_id}", + 'email' => "$order->primary_email" + ); + if (!empty($shipping_info)) { + $params['shipping'] = $shipping_info; + } + //Create the customer in stripe + $customer = \Stripe\Customer::create($params); + + // Store the customer ID in the session, + // We'll pick it up later to save it in the database since we might not have a $user object at this point anyway + $stripe_customer_id = $_SESSION['stripe']['customer_id'] = $customer->id; + + } catch (Exception $e) { + $result = array( + 'success' => FALSE, + 'comment' => $e->getCode(), + 'message' => t("Stripe Customer Creation Failed for order !order: !message", array( + "!order" => $order_id, + "!message" => $e->getMessage() + )), + 'uid' => $user->uid, + 'order_id' => $order_id, + ); + + uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin'); + + watchdog('uc_stripe', 'Failed stripe customer creation: @message', array('@message' => $result['message'])); + + return $result; + } + + } + + // Charge the stripe customer the amount in the order + + //--Handle transactions for $0 + + // Stripe can't handle transactions < $0.50, but $0 is a common value + // so we will just return a positive result when the amount is $0. + if ($amount == 0) { + $result = array( + 'success' => TRUE, + 'message' => t('Payment of $0 approved'), + 'uid' => $user->uid, + 'trans_id' => md5(uniqid(rand())), + ); + uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin'); + return $result; + } + + + // Charge the customer + try { + + //Bail if there's no customer ID + if (empty($stripe_customer_id)) { + throw new Exception('No customer ID found'); + } + + $params = array( + "amount" => $amount, + "currency" => strtolower($order->currency), + "customer" => $stripe_customer_id, + "description" => t("Order #@order_id", array("@order_id" => $order_id)), + ); + if (!empty($shipping_info)) { + $params['shipping'] = $shipping_info; + } + + // charge the Customer the amount in the order + $charge = \Stripe\Charge::create($params); + + $formatted_amount = $amount / 100; + $formatted_amount = number_format($formatted_amount, 2); + + $result = array( + 'success' => TRUE, + 'message' => t('Payment of @amount processed successfully, Stripe transaction id @transaction_id.', array('@amount' => $formatted_amount, '@transaction_id' => $charge->id)), + 'comment' => t('Stripe transaction ID: @transaction_id', array('@transaction_id' => $charge->id)), + 'uid' => $user->uid, + ); + + uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin'); + uc_order_comment_save($order_id, $user->uid, $result['message'], 'order', 'completed', FALSE); + + return $result; + + } catch (Exception $e) { + $result = array( + 'success' => FALSE, + 'comment' => $e->getCode(), + 'message' => t("Stripe Charge Failed for order !order: !message", array( + "!order" => $order_id, + "!message" => $e->getMessage() + )), + 'uid' => $user->uid, + 'order_id' => $order_id, + ); + uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin'); + watchdog('uc_stripe', 'Stripe charge failed for order @order, message: @message', array('@order' => $order_id, '@message' => $result['message'])); + + return $result; + } + + // Default / Fallback procedure to fail if the above conditions aren't met + + $result = array( + 'success' => FALSE, + 'comment' => "Stripe Gateway Error", + 'message' => "Stripe Gateway Error", + 'uid' => $user->uid, + 'order_id' => $order_id, + ); + + uc_order_comment_save($order_id, $user->uid, $result['message'], 'admin'); + + watchdog('uc_stripe', 'Stripe gateway error for order @order_id', array('order_id' => $order_id)); + + return $result; +} + +/** + * Handle renewing a recurring fee, called by uc_recurring + * + * Runs when the subscription interval is hit. So once a month or whatever. + * This just charges the stripe customer whatever amount ubercart wants. It does + * not use the Stripe subscription feature. + * + * @param $order + * @param $fee + * @return bool + */ +function uc_stripe_renew($order, &$fee) { + + try { + + //Load the API + _uc_stripe_prepare_api(); + + //Get the customer ID + $stripe_customer_id = _uc_stripe_get_customer_id($order->uid); + + if (empty($stripe_customer_id)) { + throw new Exception('No stripe customer ID found'); + } + + + //Create the charge + $amount = $fee->fee_amount; + $amount = $amount * 100; + + $charge = \Stripe\Charge::create(array( + "amount" => $amount, + "currency" => strtolower($order->currency), + "customer" => $stripe_customer_id + ) + ); + + + uc_payment_enter($order->order_id, $order->payment_method, $order->order_total, $fee->uid, $charge, "Success"); + + $formatted_amount = number_format($fee->fee_amount, 2); + $message = t('Card renewal payment of @amount processed successfully.', array('@amount' => $formatted_amount)); + uc_order_comment_save($fee->order_id, $order->uid, $message, 'order', 'completed', FALSE); + + return TRUE; + + } catch (Exception $e) { + $result = array( + 'success' => FALSE, + 'comment' => $e->getCode(), + 'message' => t("Renewal Failed for order !order: !message", array( + "!order" => $order->order_id, + "!message" => $e->getMessage() + )), + ); + + uc_order_comment_save($order->order_id, $order->uid, $result['message'], 'admin'); + + watchdog('uc_stripe', 'Renewal failed for order @order_id, code=@code, message: @message', array('@order_id' => $order->order_id, '@code' => $result['comment'], '@message' => $result['message'])); + + return FALSE; + } + + +} + +/** + * UC Recurring: Process a new recurring fee. + * This runs when subscriptions are "set up" for the first time. + * There is no action to be taken here except returning TRUE because the customer + * ID is already stored with the user, where it can be accessed when next charge + * takes place. + * + * @param $order + * @param $fee + * @return bool + */ +function uc_stripe_process($order, &$fee) { + return TRUE; +} + +/** + * UC Recurring: Cancel a recurring fee. + * This runs when subscriptions are cancelled + * Since we're handling charge intervals in ubercart, this doesn't need to do anything. + * + * @param $order + * @param $op + * @return bool + */ +function uc_stripe_cancel($order, $op) { + $message = t("Subscription Canceled"); + uc_order_comment_save($order->order_id, $order->uid, $message, 'order', 'completed', FALSE); + return TRUE; +} + + + +/** + * Load stripe API + * + * @return bool + */ +function _uc_stripe_prepare_api() { + + module_load_include('install', 'uc_stripe'); + if (!_uc_stripe_load_library()) { + return FALSE; + } + + if (!_uc_stripe_check_api_keys()) { + watchdog('uc_stripe', 'Stripe API keys are not configured. Payments cannot be made without them.', array(), WATCHDOG_ERROR); + return FALSE; + } + + $secret_key = variable_get('uc_stripe_testmode', TRUE) ? variable_get('uc_stripe_api_key_test_secret', '') : variable_get('uc_stripe_api_key_live_secret', ''); + try { + $library = libraries_load('stripe'); + \Stripe\Stripe::setApiKey($secret_key); + \Stripe\Stripe::setApiVersion($library['stripe_api_version']); + } catch (Exception $e) { + watchdog('uc_stripe', 'Error setting the Stripe API Key. Payments will not be processed: %error', array('%error' => $e->getMessage())); + } + return TRUE; +} + +/** + * Check that all API keys are configured. + * + * @return bool + * TRUE if all 4 keys have a value. + */ +function _uc_stripe_check_api_keys() { + return (variable_get('uc_stripe_api_key_live_publishable', FALSE) && + variable_get('uc_stripe_api_key_live_secret', FALSE) && + variable_get('uc_stripe_api_key_test_publishable', FALSE) && + variable_get('uc_stripe_api_key_test_secret', FALSE)); +} + +/** + * Retrieve the Stripe customer id for a user + * + * @param $uid + * @return bool + */ +function _uc_stripe_get_customer_id($uid) { + + $account = user_load($uid); + + $id = !empty($account->data['uc_stripe_customer_id']) ? $account->data['uc_stripe_customer_id'] : FALSE; + return $id; +} + + +/** + * Implements hook_theme_registry_alter() to make sure that we render + * the entire credit form, including the key returned by JS. + * + * @param $theme_registry + */ +function uc_stripe_theme_registry_alter(&$theme_registry) { + if (!empty($theme_registry['uc_payment_method_credit_form'])) { + $theme_registry['uc_payment_method_credit_form']['function'] = 'uc_stripe_uc_payment_method_credit_form'; + } +} + +/** + * Replace uc_credit's form themeing with our own - adds stripe_token. + * @param $form + * @return string + */ +function uc_stripe_uc_payment_method_credit_form($form) { + $output = drupal_render($form['stripe_nojs_warning']); + $output .= drupal_render($form['config_error']); + $output .= theme('uc_payment_method_credit_form',$form); + $output .= drupal_render($form['stripe_token']); + $output .= drupal_render($form['dummy_image_load']); + + return $output; +} + +