From 26f94f67faafffd95658087674305bb60fda7fa4 Mon Sep 17 00:00:00 2001
From: Matthew Wire <mjw@mjwconsult.co.uk>
Date: Sun, 8 Oct 2017 10:26:37 +0100
Subject: [PATCH] Code cleanup

---
 CRM/Core/Form/Stripe.php    |  16 ----
 CRM/Core/Payment/Stripe.php |  70 +++++---------
 README.md                   |   8 ++
 js/civicrm_stripe.js        | 176 ++++++++++++++++++------------------
 stripe.php                  |   8 +-
 5 files changed, 123 insertions(+), 155 deletions(-)
 delete mode 100644 CRM/Core/Form/Stripe.php

diff --git a/CRM/Core/Form/Stripe.php b/CRM/Core/Form/Stripe.php
deleted file mode 100644
index 828e2ea5..00000000
--- a/CRM/Core/Form/Stripe.php
+++ /dev/null
@@ -1,16 +0,0 @@
-<?php
-
-/*
- * Form Class for Stripe
- */
-
-class CRM_Core_Form_Stripe extends CRM_Core_Form {
-
-  /**
-   * Function to access protected payProcessors array in event registraion forms
-   */
-  public static function get_ppids(&$form) {
-    $payprocessorIds = $form->_paymentProcessors;
-    return $payprocessorIds;
-  }
-}
diff --git a/CRM/Core/Payment/Stripe.php b/CRM/Core/Payment/Stripe.php
index f938097b..74bfc693 100644
--- a/CRM/Core/Payment/Stripe.php
+++ b/CRM/Core/Payment/Stripe.php
@@ -208,7 +208,7 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
    * @param $form - reference to the form object
    */
   public function buildForm(&$form) {
-    $stripe_ppid = self::get_stripe_ppid($form);
+    $stripe_ppid = CRM_Utils_Array::value('id', $form->_paymentProcessor);
 
     // Add the ID to our form so our js can tell if Stripe has been selected.
     $form->addElement('hidden', 'stripe_id', $stripe_ppid, array('id' => 'stripe-id'));
@@ -221,59 +221,19 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
     if (!empty($params[0]['stripe_token'])) {
       $params = $params[0];
     }
-    $stripe_token = (empty($params['stripe_token']) ? NULL : $params['stripe_token']);
+    $stripeToken = (empty($params['stripetoken']) ? NULL : $params['stripetoken']);
 
     // Add some hidden fields for Stripe.
-    if (!$form->elementExists('stripe_token')) {
-      $form->setAttribute('class', $form->getAttribute('class') . ' stripe-payment-form');
-      $form->addElement('hidden', 'stripe_token', $stripe_token, array('id' => 'stripe-token'));
+    if (!empty($stripeToken) && !$form->elementExists('stripetoken')) {
+        $form->addElement('hidden', 'stripetoken', $stripeToken, array('id' => 'stripe-token'));
     }
 
-    // Add the Civi version so we can accommodate different versions in civicrm_stripe.js.
-    if (self::get_civi_version() <= '4.7.0') {
-      $ext_mode = 1;
-    }
-    else {
-      $ext_mode = 2;
-    }
-    $form->addElement('hidden', 'ext_mode', $ext_mode, array('id' => 'ext-mode'));
-
     // Add email field as it would usually be found on donation forms.
     if (!isset($form->_elementIndex['email']) && !empty($form->userEmail)) {
       $form->addElement('hidden', 'email', $form->userEmail, array('id' => 'user-email'));
     }
   }
 
- public static function get_stripe_ppid($form) {
-    if (empty($form->_paymentProcessor)) {
-      return;
-    }
-
-    // When called from admin backend (eg via CRM_Contribute_Form_Contribution, CRM_Member_Form_Membership)
-    // the isBackOffice flag will be set to true.
-    // But if called via webform in CiviCRM 4.7: isBackOffice=NULL and for is of class CRM_Financial_Form_Payment or CRM_Contribute_Form_Contribution
-    // Those don't have a _paymentProcessors array and only have one payprocesssor.
-    if (!empty($form->isBackOffice)
-      || (in_array(get_class($form), array('CRM_Financial_Form_Payment', 'CRM_Contribute_Form_Contribution')))) {
-      return $stripe_ppid = $form->_paymentProcessor['id'];
-    }
-    else {
-      // Find a Stripe pay processor ascociated with this Civi form and find the ID.
-      //   $payProcessors = $form->_paymentProcessors;
-      $payProcessors = CRM_Core_Form_Stripe::get_ppids($form);
-      foreach ($payProcessors as $payProcessor) {
-        if ($payProcessor['class_name'] == 'Payment_Stripe') {
-          return $stripe_ppid = $payProcessor['id'];
-          break;
-        }
-      }
-    }
-    // None of the payprocessors are Stripe.
-    if (empty($stripe_ppid)) {
-      return;
-    }
-  }
-
   /**
    * Given a payment processor id, return the pub key.
    */
@@ -342,8 +302,9 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
     $amount = (int) preg_replace('/[^\d]/', '', strval($amount));
 
     // Use Stripe.js instead of raw card details.
-    if (!empty($params['stripe_token'])) {
-      $card_details = $params['stripe_token'];
+    if (!empty($params['credit_card_number']) && (substr($params['credit_card_number'], 0, 4) === 'tok_')) {
+      $card_details = $params['credit_card_number'];
+      $params['credit_card_number'] = '';
     }
     else {
       CRM_Core_Error::fatal(ts('Stripe.js token was not passed!  Report this message to the site administrator.'));
@@ -816,4 +777,21 @@ class CRM_Core_Payment_Stripe extends CRM_Core_Payment {
   public function doTransferCheckout(&$params, $component) {
     CRM_Core_Error::fatal(ts('Use direct billing instead of Transfer method.'));
   }
+
+  /**
+   * Default payment instrument validation.
+   *
+   * Implement the usual Luhn algorithm via a static function in the CRM_Core_Payment_Form if it's a credit card
+   * Not a static function, because I need to check for payment_type.
+   *
+   * @param array $values
+   * @param array $errors
+   */
+  public function validatePaymentInstrument($values, &$errors) {
+    CRM_Core_Form::validateMandatoryFields($this->getMandatoryFields(), $values, $errors);
+    if ($this->_paymentProcessor['payment_type'] == 1) {
+      // Don't validate credit card details as they are not passed (and stripe does this for us)
+      //CRM_Core_Payment_Form::validateCreditCard($values, $errors, $this->_paymentProcessor['id']);
+    }
+  }
 }
diff --git a/README.md b/README.md
index df41ab0e..3ddf345e 100644
--- a/README.md
+++ b/README.md
@@ -66,3 +66,11 @@ OTHER CREDITS
 -------------
 For bug fixes, new features, and documentiation, thanks to:
 rgburton, Swingline0, BorislavZlatanov, agh1, & jmcclelland
+
+TESTING
+--------
+1. Test webform submission with payment and user-select, single processor.
+2. Test online contribution page with single processor, multi-processor (stripe default, stripe non-default).
+3. Test offline contribution page with single processor, multi-processor (stripe default, stripe non-default).
+4. Test event registration.
+5. Test event registration (cart checkout).
diff --git a/js/civicrm_stripe.js b/js/civicrm_stripe.js
index d42de560..c8dbb85b 100644
--- a/js/civicrm_stripe.js
+++ b/js/civicrm_stripe.js
@@ -24,68 +24,73 @@
         + '</div>');
 
       $submit.removeAttr('disabled').attr('value', buttonText);
-
     }
     else {
       var token = response['id'];
       // Update form with the token & submit.
-      $form.find("input#stripe-token").val(token);
-      $form.find("input#credit_card_number").removeAttr('name');
-      $form.find("input#cvv2").removeAttr('name');
-      $submit.prop('disabled', false);
+      removeCCDetails($form);
+      // We use the credit_card_number field to pass token as this is reliable.
+      // Inserting an input field is unreliable on ajax forms and often gets missed from POST request for some reason.
+      $form.find("input#credit_card_number").val(token);
+
+      // Disable unload event handler
       window.onbeforeunload = null;
+      // This triggers submit without generating a submit event (so we don't run submit handler again)
       $form.get(0).submit();
     }
   }
 
   // Prepare the form.
   $(document).ready(function() {
-    Stripe.setPublishableKey($('#stripe-pub-key').val());
     loadStripeBillingBlock();
   });
 
-  CRM.$('input[name="payment_processor_id"]').change(function() {
+  // On the frontend, we have a set of radio buttons. Trigger on change.
+  $('input[name="payment_processor_id"]').change(function() {
+    loadStripeBillingBlock();
+  });
+
+  // On the backend, we have a select.  Trigger on change.
+  $('select#payment_processor_id').change(function() {
     loadStripeBillingBlock();
   });
 
   function loadStripeBillingBlock() {
-    // Check for form marked as a stripe-payment-form by the server.
-    if (!($('form.stripe-payment-form').length)) {
-      // If there isn't one look for it.
-      if ($('.webform-client-form').length) {
-        isWebform = true;
-        $('form.webform-client-form').addClass('stripe-payment-form');
-      }
-      else if ($('#crm-container form').length) {
-        $('#crm-container form').addClass('stripe-payment-form');
-      }
-      else {
-        return;
+    var $stripePubKey = $('#stripe-pub-key');
+    if ($stripePubKey.length) {
+      if (!$().Stripe) {
+        $.getScript('https://js.stripe.com/v2/', function () {
+          Stripe.setPublishableKey($('#stripe-pub-key').val());
+        });
       }
     }
 
-    $form   = $('form.stripe-payment-form');
+    if ($('.webform-client-form').length) {
+      isWebform = true;
+    }
+
+    // Get the form containing payment details
+    $form = CRM.$('input#stripe-pub-key').closest('form');
+
     if (isWebform) {
       $submit = $form.find('.button-primary');
     }
     else {
       $submit = $form.find('input[type="submit"][formnovalidate!="1"]');
 
-      // If CiviDiscount button or field is submitted, flag the form.
-      $form.data('cividiscount-dont-handle', '0');
-      // This is an ugly hack. Really, the code should positively identify the
-      // "real" submit button(s) and only respond to them.  Otherwise, we're
-      // chasing down a potentially endless number of exceptions.  The problem
-      // is that it's unclear if CiviCRM consistently names its submit buttons.
+      // If another submit button on the form is pressed (eg. apply discount)
+      //  add a flag that we can set to stop payment submission
+      $form.data('submit-dont-process', '0');
+      // Find submit buttons with formnovalidate=1 and add an onclick handler to set flag
       $form.find('input[type="submit"][formnovalidate="1"], input[type="submit"].cancel').click( function() {
-        $form.data('cividiscount-dont-handle', 1);
+        $form.data('submit-dont-process', 1);
       });
+      // Add a keypress handler to set flag if enter is pressed
       $form.find('input#discountcode').keypress( function(e) {
-        if (e.which == 13) {
-          $form.data('cividiscount-dont-handle', 1);
+        if (e.which === 13) {
+          $form.data('submit-dont-process', 1);
         }
       });
-      $submit;
     }
 
     // For CiviCRM Webforms.
@@ -94,9 +99,10 @@
         $form.append('<input type="hidden" name="op" id="action" />');
       }
       $(document).keypress(function(event) {
-        if (event.which == 13) {
+        if (event.which === 13) {
+          // Enter was pressed
           event.preventDefault();
-          $submit.click();
+          submit(event);
         }
       });
       $(":submit").click(function() {
@@ -108,49 +114,62 @@
       var webformPrevious = $('input.webform-previous').first().val();
     }
     else {
-      // This is native civicrm form - check for existing token.
-      if ($form.find("input#stripe-token").val()) {
+      // CiviCRM form
+      // If we already have a token hide CC details
+      if ($form.find("input#credit_card_number").val()) {
         $('.credit_card_info-group').hide();
         $('#billing-payment-block').append('<input type="button" value="Edit CC details" id="ccButton" />');
         $('#ccButton').click(function() {
+          // Clear token and show CC details if edit button was clicked
+          // As we use credit_card_number to pass token, make sure it is empty when shown
+          $form.find("input#credit_card_number").val('');
           $('.credit_card_info-group').show();
           $('#ccButton').hide();
-          $form.find('input#stripe-token').val('');
         });
       }
+      else {
+        // As we use credit_card_number to pass token, make sure it is empty when shown
+        $form.find("input#credit_card_number").val('');
+      }
     }
 
     $submit.removeAttr('onclick');
-
     $form.unbind('submit');
 
     // Intercept form submission.
     $form.submit(function (event) {
+      event.preventDefault();
+      submit(event);
+    });
+
+    function submit(event) {
+      // Don't handle submits generated by non-stripe processors
+      if (!$('input#stripe-pub-key').length) {
+        debugging('submit missing stripe-pub-key element');
+        return true;
+      }
       // Don't handle submits generated by the CiviDiscount button.
-      if ($form.data('cividiscount-dont-handle') == 1) {
+      if ($form.data('submit-dont-process') === 1) {
         debugging('debug: pvjwy (Discount is in play)');
         return true;
       }
       if (isWebform) {
         var $processorFields = $('.civicrm-enabled[name$="civicrm_1_contribution_1_contribution_payment_processor_id]"]');
 
-        if ($('#action').attr('value') == webformPrevious) {
-          debugging('wmlfp');
+        if ($('#action').attr('value') === webformPrevious) {
+          // Don't submit if the webform back button was pressed
+          debugging('webform back button');
           return true;
         }
         if ($('#wf-crm-billing-total').length) {
-          if ($('#wf-crm-billing-total').data('data-amount') == '0') {
-            debugging('qplfr');
+          if ($('#wf-crm-billing-total').data('data-amount') === '0') {
+            debugging('webform total is 0');
             return true;
           }
         }
         if ($processorFields.length) {
-          if ($processorFields.filter(':checked').val() == '0') {
-            debugging('evxyh');
-            return true;
-          }
-          if (!($form.find('input[name="stripe_token"]').length)) {
-            debugging('irjfg');
+          if ($processorFields.filter(':checked').val() === '0') {
+            debugging('no payment processor selected');
             return true;
           }
         }
@@ -159,68 +178,39 @@
       buttonText = $submit.attr('value');
       $submit.prop('disabled', true).attr('value', 'Processing');
 
-      // Hide payment if total is 0 and no more participants.
-      if ($('#priceset').length) {
-        additionalParticipants = cj("#additional_participants").val();
-        // The currentTotal is already being calculated in Form/Contribution/Main.tpl.
-        if(typeof currentTotal !== 'undefined') {
-          if (currentTotal == 0 && !additionalParticipants) {
-            // This is also hit when "Going back", but we already have stripe_token.
-            debugging('ozlkf');
-            // This should not happen on Confirm Contribution, but seems to on 4.6 for some reason.
-            //return true;
-          }
-        }
-      }
-
       // Handle multiple payment options and Stripe not being chosen.
       if ($form.find(".crm-section.payment_processor-section").length > 0) {
         var extMode = $('#ext-mode').val();
         var stripeProcessorId = $('#stripe-id').val();
-        // Support for CiviCRM 4.6 and 4.7 multiple payment options
-        if (extMode == 1) {
-          var chosenProcessorId = $form.find('input[name="payment_processor"]:checked').val();
-        }
-        else if (extMode == 2) {
-          var chosenProcessorId = $form.find('input[name="payment_processor_id"]:checked').val();
-        }
+        var chosenProcessorId = $form.find('input[name="payment_processor_id"]:checked').val();
+
         // Bail if we're not using Stripe or are using pay later (option value '0' in payment_processor radio group).
-        if ((chosenProcessorId != stripeProcessorId) || (chosenProcessorId == 0)) {
-          debugging('debug: kfoej (Not a Stripe transaction, or pay-later)');
+        if ((chosenProcessorId !== stripeProcessorId) || (chosenProcessorId === 0)) {
+          debugging('debug: Not a Stripe transaction, or pay-later');
           return true;
         }
       }
       else {
-        debugging('debug: qlmvy (Stripe is the only payprocessor here)');
+        debugging('debug: Stripe is the selected payprocessor');
       }
 
       // Handle reuse of existing token
-      if ($form.find("input#stripe-token").val()) {
-        $form.find("input#credit_card_number").removeAttr('name');
-        $form.find("input#cvv2").removeAttr('name');
-        debugging('debug: zpqef (Re-using Stripe token)');
+      if ($form.find("input#credit_card_number").val()) {
+        removeCCDetails($form);
+        debugging('debug: Re-using Stripe token');
         return true;
       }
 
       // If there's no credit card field, no use in continuing (probably wrong
       // context anyway)
       if (!$form.find('#credit_card_number').length) {
-        debugging('debug: gvzod (No credit card field)');
+        debugging('debug: No credit card field');
         return true;
       }
 
-      event.preventDefault();
-      event.stopPropagation();
+      var cc_month = $form.find('#credit_card_exp_date_M').val();
+      var cc_year = $form.find('#credit_card_exp_date_Y').val();
 
-      // Handle changes introduced in CiviCRM 4.3.
-      if ($form.find('#credit_card_exp_date_M').length > 0) {
-        var cc_month = $form.find('#credit_card_exp_date_M').val();
-        var cc_year = $form.find('#credit_card_exp_date_Y').val();
-      }
-      else {
-        var cc_month = $form.find('#credit_card_exp_date\\[M\\]').val();
-        var cc_year = $form.find('#credit_card_exp_date\\[Y\\]').val();
-      }
       Stripe.card.createToken({
         name:        $form.find('#billing_first_name').val() + ' ' + $form.find('#billing_last_name').val(),
         address_zip: $form.find('#billing_postal_code-5').val(),
@@ -230,14 +220,20 @@
         exp_year:    cc_year
       }, stripeResponseHandler);
 
-      debugging('debug: ywkvh (Getting Stripe token)');
+      debugging('debug: Getting Stripe token');
       return false;
-    });
+    }
   }
 }(cj, CRM));
 
+function removeCCDetails($form) {
+  // Remove the "name" attribute so params are not submitted
+  $form.find("input#credit_card_number").val('0000000000000000');
+  $form.find("input#cvv2").val('000');
+}
+
 function debugging (errorCode) {
   // Uncomment the following to debug unexpected returns.
-  //  console.log(errorCode);
+    console.log(errorCode);
 }
 
diff --git a/stripe.php b/stripe.php
index f8555526..162a4dd4 100644
--- a/stripe.php
+++ b/stripe.php
@@ -3,6 +3,8 @@
 require_once 'stripe.civix.php';
 require_once __DIR__.'/vendor/autoload.php';
 
+use CRM_Stripe_ExtensionUtil as E;
+
 /**
  * Implementation of hook_civicrm_config().
  */
@@ -194,7 +196,7 @@ function stripe_civicrm_managed(&$entities) {
    * @return void
    */
   function stripe_civicrm_alterContent( &$content, $context, $tplName, &$object ) {
-    if($context == 'form' && !empty($object->_paymentProcessor['class_name'])) {
+    if ($context == 'form' && !empty($object->_paymentProcessor['class_name'])) {
       $stripeJSURL = CRM_Core_Resources::singleton()->getUrl('com.drastikbydesign.stripe', 'js/civicrm_stripe.js');
       $content .= "<script src='{$stripeJSURL}'></script>";
     }
@@ -207,9 +209,9 @@ function stripe_civicrm_managed(&$entities) {
  */
 function stripe_civicrm_buildForm($formName, &$form) {
   if (!empty($form->_paymentProcessor['class_name'])) {
-    CRM_Core_Resources::singleton()->addScriptUrl('https://js.stripe.com/v2/');
+    //CRM_Core_Resources::singleton()->addScriptUrl('https://js.stripe.com/v2/', 10, 'page-body');
   }
-  if (($formName == 'CRM_Member_Form_MembershipRenewal') && !empty($form->_paymentProcessor['class_name'])) {
+  if (/*$form->isBackOffice &&*/ !empty($form->_paymentProcessor['class_name'])) {
     // civicrm_stripe.js is not included on backend form renewal unless we add it here.
     CRM_Core_Resources::singleton()->addScriptFile('com.drastikbydesign.stripe', 'js/civicrm_stripe.js');
   }
-- 
GitLab