CRM-15978 remove enotice introduced in last commit
[civicrm-core.git] / CRM / Core / Payment.php
index e55bc67fb793257a27863f0cd99f9af65d469ef7..60a6e3550934774222d075c3d6e269400f5ea86f 100644 (file)
  | GNU Affero General Public License or the licensing of CiviCRM,     |
  | see the CiviCRM license FAQ at http://civicrm.org/licensing        |
  +--------------------------------------------------------------------+
-*/
+ */
+
+use Civi\Payment\System;
 
 /**
+ * Class CRM_Core_Payment.
  *
- * @package CRM
- * @copyright CiviCRM LLC (c) 2004-2014
- * $Id$
+ * This class is the main class for the payment processor subsystem.
  *
+ * It is the parent class for payment processors. It also holds some IPN related functions
+ * that need to be moved. In particular handlePaymentMethod should be moved to a factory class.
  */
-
 abstract class CRM_Core_Payment {
 
   /**
@@ -41,7 +43,7 @@ abstract class CRM_Core_Payment {
    * FORM   - we collect it on the same page
    * BUTTON - the processor collects it and sends it back to us via some protocol
    */
-  CONST
+  const
     BILLING_MODE_FORM = 1,
     BILLING_MODE_BUTTON = 2,
     BILLING_MODE_NOTIFY = 4;
@@ -52,86 +54,58 @@ abstract class CRM_Core_Payment {
    * credit card
    * direct debit
    * or both
-   *
+   * @todo create option group - nb omnipay uses a 3rd type - transparent redirect cc
    */
-  CONST
+  const
     PAYMENT_TYPE_CREDIT_CARD = 1,
     PAYMENT_TYPE_DIRECT_DEBIT = 2;
 
   /**
    * Subscription / Recurring payment Status
    * START, END
-   *
    */
-  CONST
+  const
     RECURRING_PAYMENT_START = 'START',
     RECURRING_PAYMENT_END = 'END';
 
-  /**
-   * We only need one instance of this object. So we use the singleton
-   * pattern and cache the instance in this variable
-   *
-   * @var object
-   * @static
-   */
-  static private $_singleton = NULL;
-
   protected $_paymentProcessor;
 
   /**
-   * @var CRM_Core_Form
-   */
-  protected $_paymentForm = NULL;
-
-  /**
-   * Singleton function used to manage this object
+   * Singleton function used to manage this object.
    *
-   * @param string  $mode the mode of operation: live or test
-   * @param array  $paymentProcessor the details of the payment processor being invoked
-   * @param object $paymentForm deprecated - avoid referring to this if possible. If you have to use it document why as this is scary interaction
-   * @param boolean $force            should we force a reload of this payment object
+   * We will migrate to calling Civi\Payment\System::singleton()->getByProcessor($paymentProcessor)
+   * & Civi\Payment\System::singleton()->getById($paymentProcessor) directly as the main access methods & work
+   * to remove this function all together
    *
-   * @return CRM_Core_Payment
-   * @static
+   * @param string $mode
+   *   The mode of operation: live or test.
+   * @param array $paymentProcessor
+   *   The details of the payment processor being invoked.
+   * @param object $paymentForm
+   *   Deprecated - avoid referring to this if possible. If you have to use it document why as this is scary interaction.
+   * @param bool $force
+   *   Should we force a reload of this payment object.
    *
+   * @return CRM_Core_Payment
+   * @throws \CRM_Core_Exception
    */
-  static function &singleton($mode = 'test', &$paymentProcessor, &$paymentForm = NULL, $force = FALSE) {
+  public static function &singleton($mode = 'test', &$paymentProcessor, &$paymentForm = NULL, $force = FALSE) {
     // make sure paymentProcessor is not empty
     // CRM-7424
     if (empty($paymentProcessor)) {
       return CRM_Core_DAO::$_nullObject;
     }
-
-    $cacheKey = "{$mode}_{$paymentProcessor['id']}_" . (int)isset($paymentForm);
-
-    if (!isset(self::$_singleton[$cacheKey]) || $force) {
-      $config = CRM_Core_Config::singleton();
-      $ext = CRM_Extension_System::singleton()->getMapper();
-      if ($ext->isExtensionKey($paymentProcessor['class_name'])) {
-        $paymentClass = $ext->keyToClass($paymentProcessor['class_name'], 'payment');
-        require_once ($ext->classToPath($paymentClass));
-      }
-      else {
-        $paymentClass = 'CRM_Core_' . $paymentProcessor['class_name'];
-        if (empty($paymentClass)) {
-          throw new CRM_Core_Exception('no class provided');
-        }
-        require_once (str_replace('_', DIRECTORY_SEPARATOR, $paymentClass) . '.php');
-      }
-
-      //load the object.
-      self::$_singleton[$cacheKey] = $paymentClass::singleton($mode, $paymentProcessor);
-    }
-
-    //load the payment form for required processor.
-    //if ($paymentForm !== NULL) {
-      //self::$_singleton[$cacheKey]->setForm($paymentForm);
-    //}
-
-    return self::$_singleton[$cacheKey];
+    //we use two lines because we can't remove the '&singleton' without risking breakage
+    //of extension classes that extend this one
+    $object = Civi\Payment\System::singleton()->getByProcessor($paymentProcessor);
+    return $object;
   }
 
   /**
+   * Log payment notification message to forensic system log.
+   *
+   * @todo move to factory class \Civi\Payment\System (or similar)
+   *
    * @param array $params
    *
    * @return mixed
@@ -150,8 +124,14 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Check if capability is supported
-   * @param string $capability e.g BackOffice, LiveMode, FutureRecurStartDate
+   * Check if capability is supported.
+   *
+   * Capabilities have a one to one relationship with capability-related functions on this class.
+   *
+   * Payment processor classes should over-ride the capability-specific function rather than this one.
+   *
+   * @param string $capability
+   *   E.g BackOffice, LiveMode, FutureRecurStartDate.
    *
    * @return bool
    */
@@ -164,8 +144,13 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Are back office payments supported - e.g paypal standard won't permit you to enter a credit card associated with someone else's login
-   * The intention is to support off-site (other than paypal) & direct debit but that is not all working yet so to reach a 'stable' point we disable
+   * Are back office payments supported.
+   *
+   * e.g paypal standard won't permit you to enter a credit card associated
+   * with someone else's login.
+   * The intention is to support off-site (other than paypal) & direct debit but that is not all working yet so to
+   * reach a 'stable' point we disable.
+   *
    * @return bool
    */
   protected function supportsBackOffice() {
@@ -178,7 +163,8 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Are back office payments supported - e.g paypal standard won't permit you to enter a credit card associated with someone else's login
+   * Are live payments supported - e.g dummy doesn't support this.
+   *
    * @return bool
    */
   protected function supportsLiveMode() {
@@ -186,8 +172,19 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Should the first payment date be configurable when setting up back office recurring payments
+   * Are test payments supported.
+   *
+   * @return bool
+   */
+  protected function supportsTestMode() {
+    return TRUE;
+  }
+
+  /**
+   * Should the first payment date be configurable when setting up back office recurring payments.
+   *
    * We set this to false for historical consistency but in fact most new processors use tokens for recurring and can support this
+   *
    * @return bool
    */
   protected function supportsFutureRecurStartDate() {
@@ -195,38 +192,55 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Setter for the payment form that wants to use the processor
+   * Default payment instrument validation.
    *
-   * @param CRM_Core_Form $paymentForm
+   * 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
    */
-  function setForm(&$paymentForm) {
-    $this->_paymentForm = $paymentForm;
+  public function validatePaymentInstrument($values, &$errors) {
+    if ($this->_paymentProcessor['payment_type'] == 1) {
+      CRM_Core_Payment_Form::validateCreditCard($values, $errors);
+    }
   }
 
   /**
-   * Getter for payment form that is using the processor
+   * Setter for the payment form that wants to use the processor.
+   *
+   * @deprecated
    *
-   * @return CRM_Core_Form  A form object
+   * @param CRM_Core_Form $paymentForm
    */
-  function getForm() {
+  public function setForm(&$paymentForm) {
+    $this->_paymentForm = $paymentForm;
+  }
+
+  /**
+   * Getter for payment form that is using the processor.
+   * @deprecated
+   * @return CRM_Core_Form
+   *   A form object
+   */
+  public function getForm() {
     return $this->_paymentForm;
   }
 
   /**
-   * Getter for accessing member vars
-   *
+   * Getter for accessing member vars.
+   * @todo believe this is unused
    * @param string $name
    *
    * @return null
    */
-  function getVar($name) {
+  public function getVar($name) {
     return isset($this->$name) ? $this->$name : NULL;
   }
 
   /**
-   * Get name for the payment information type
-   *
+   * Get name for the payment information type.
+   * @todo - use option group + name field (like Omnipay does)
    * @return string
    */
   public function getPaymentTypeName() {
@@ -234,8 +248,8 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Get label for the payment information type
-   *
+   * Get label for the payment information type.
+   * @todo - use option group + labels (like Omnipay does)
    * @return string
    */
   public function getPaymentTypeLabel() {
@@ -243,7 +257,7 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Get array of fields that should be displayed on the payment form
+   * Get array of fields that should be displayed on the payment form.
    * @todo make payment type an option value & use it in the function name - currently on debit & credit card work
    * @return array
    * @throws CiviCRM_API3_Exception
@@ -256,7 +270,7 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Get array of fields that should be displayed on the payment form for credit cards
+   * Get array of fields that should be displayed on the payment form for credit cards.
    *
    * @return array
    */
@@ -270,7 +284,7 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Get array of fields that should be displayed on the payment form for direct debits
+   * Get array of fields that should be displayed on the payment form for direct debits.
    *
    * @return array
    */
@@ -284,10 +298,12 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Return an array of all the details about the fields potentially required for payment fields
+   * Return an array of all the details about the fields potentially required for payment fields.
+   *
    * Only those determined by getPaymentFormFields will actually be assigned to the form
    *
-   * @return array field metadata
+   * @return array
+   *   field metadata
    */
   public function getPaymentFormFieldsMetadata() {
     //@todo convert credit card type into an option value
@@ -301,7 +317,7 @@ abstract class CRM_Core_Payment {
         'attributes' => array(
           'size' => 20,
           'maxlength' => 20,
-          'autocomplete' => 'off'
+          'autocomplete' => 'off',
         ),
         'is_required' => TRUE,
       ),
@@ -313,7 +329,7 @@ abstract class CRM_Core_Payment {
         'attributes' => array(
           'size' => 5,
           'maxlength' => 10,
-          'autocomplete' => 'off'
+          'autocomplete' => 'off',
         ),
         'is_required' => CRM_Core_BAO_Setting::getItem(CRM_Core_BAO_Setting::CONTRIBUTE_PREFERENCES_NAME,
           'cvv_backoffice_required',
@@ -325,7 +341,8 @@ abstract class CRM_Core_Payment {
             'rule_message' => ts('Please enter a valid value for your card security code. This is usually the last 3-4 digits on the card\'s signature panel.'),
             'rule_name' => 'integer',
             'rule_parameters' => NULL,
-        )),
+          ),
+        ),
       ),
       'credit_card_exp_date' => array(
         'htmlType' => 'date',
@@ -339,7 +356,8 @@ abstract class CRM_Core_Payment {
             'rule_message' => ts('Card expiration date cannot be a past date.'),
             'rule_name' => 'currentDate',
             'rule_parameters' => TRUE,
-          )),
+          ),
+        ),
       ),
       'credit_card_type' => array(
         'htmlType' => 'select',
@@ -357,7 +375,7 @@ abstract class CRM_Core_Payment {
         'attributes' => array(
           'size' => 20,
           'maxlength' => 34,
-          'autocomplete' => 'on'
+          'autocomplete' => 'on',
         ),
         'is_required' => TRUE,
       ),
@@ -370,14 +388,15 @@ abstract class CRM_Core_Payment {
         'attributes' => array(
           'size' => 20,
           'maxlength' => 34,
-          'autocomplete' => 'off'
+          'autocomplete' => 'off',
         ),
         'rules' => array(
           array(
             'rule_message' => ts('Please enter a valid Bank Identification Number (value must not contain punctuation characters).'),
             'rule_name' => 'nopunctuation',
             'rule_parameters' => NULL,
-        )),
+          ),
+        ),
         'is_required' => TRUE,
       ),
       //e.g. SWIFT-BIC can have maxlength of 11 digits
@@ -389,7 +408,7 @@ abstract class CRM_Core_Payment {
         'attributes' => array(
           'size' => 20,
           'maxlength' => 11,
-          'autocomplete' => 'off'
+          'autocomplete' => 'off',
         ),
         'is_required' => TRUE,
         'rules' => array(
@@ -397,7 +416,8 @@ abstract class CRM_Core_Payment {
             'rule_message' => ts('Please enter a valid Bank Identification Number (value must not contain punctuation characters).'),
             'rule_name' => 'nopunctuation',
             'rule_parameters' => NULL,
-        )),
+          ),
+        ),
       ),
       'bank_name' => array(
         'htmlType' => 'text',
@@ -407,24 +427,29 @@ abstract class CRM_Core_Payment {
         'attributes' => array(
           'size' => 20,
           'maxlength' => 64,
-          'autocomplete' => 'off'
+          'autocomplete' => 'off',
         ),
         'is_required' => TRUE,
 
-      )
+      ),
     );
   }
 
   /**
-   * This function collects all the information from a web/api form and invokes
-   * the relevant payment processor specific functions to perform the transaction
+   * Calling this from outside the payment subsystem is deprecated - use doPayment.
+   *
+   * Does a server to server payment transaction.
    *
-   * @param  array $params assoc array of input parameters for this transaction
+   * Note that doPayment will throw an exception so the code may need to be modified
+   *
+   * @param array $params
+   *   Assoc array of input parameters for this transaction.
    *
-   * @return array the result in an nice formatted array (or an error object)
+   * @return array
+   *   the result in an nice formatted array (or an error object)
    * @abstract
    */
-  abstract function doDirectPayment(&$params);
+  abstract protected function doDirectPayment(&$params);
 
   /**
    * Process payment - this function wraps around both doTransferPayment and doDirectPayment
@@ -434,9 +459,11 @@ abstract class CRM_Core_Payment {
    *
    * @param $component
    *
+   * @return array
+   *   (modified)
    * @throws CRM_Core_Exception
    */
-  public function doPayment(&$params, $component) {
+  public function doPayment(&$params, $component = 'contribute') {
     if ($this->_paymentProcessor['billing_mode'] == 4) {
       $result = $this->doTransferCheckout($params, $component);
     }
@@ -446,22 +473,24 @@ abstract class CRM_Core_Payment {
     if (is_a($result, 'CRM_Core_Error')) {
       throw new CRM_Core_Exception(CRM_Core_Error::getMessages($result));
     }
+    //CRM-15767 - Submit Credit Card Contribution not being saved
+    return $result;
   }
 
   /**
-   * This function checks to see if we have the right config values
+   * This function checks to see if we have the right config values.
    *
-   * @return string the error message if any
-   * @public
+   * @return string
+   *   the error message if any
    */
-  abstract function checkConfig();
+  abstract protected function checkConfig();
 
   /**
    * @param $paymentProcessor
-   *
+   * @todo move to paypal class or remover
    * @return bool
    */
-  static function paypalRedirect(&$paymentProcessor) {
+  public static function paypalRedirect(&$paymentProcessor) {
     if (!$paymentProcessor) {
       return FALSE;
     }
@@ -478,10 +507,10 @@ abstract class CRM_Core_Payment {
   }
 
   /**
+   * @todo move to0 \Civi\Payment\System factory method
    * Page callback for civicrm/payment/ipn
-   * @public
    */
-  static function handleIPN() {
+  public static function handleIPN() {
     self::handlePaymentMethod(
       'PaymentNotification',
       array(
@@ -490,51 +519,53 @@ abstract class CRM_Core_Payment {
         'mode' => @$_GET['mode'],
       )
     );
+    CRM_Utils_System::civiExit();
   }
 
   /**
-   * Payment callback handler. The processor_name or processor_id is passed in.
+   * Payment callback handler.
+   *
+   * The processor_name or processor_id is passed in.
    * Note that processor_id is more reliable as one site may have more than one instance of a
    * processor & ideally the processor will be validating the results
    * Load requested payment processor and call that processor's handle<$method> method
    *
-   * @public
-   * @param $method
+   * @todo move to \Civi\Payment\System factory method
+   *
+   * @param string $method
+   *   'PaymentNotification' or 'PaymentCron'
    * @param array $params
    */
-  static function handlePaymentMethod($method, $params = array()) {
+  public static function handlePaymentMethod($method, $params = array()) {
     if (!isset($params['processor_id']) && !isset($params['processor_name'])) {
       CRM_Core_Error::fatal("Either 'processor_id' or 'processor_name' param is required for payment callback");
     }
     self::logPaymentNotification($params);
 
-    // Query db for processor ..
-    $mode = @$params['mode'];
-
     $sql = "SELECT ppt.class_name, ppt.name as processor_name, pp.id AS processor_id
               FROM civicrm_payment_processor_type ppt
         INNER JOIN civicrm_payment_processor pp
                 ON pp.payment_processor_type_id = ppt.id
-               AND pp.is_active
-               AND pp.is_test = %1";
-    $args[1] = array($mode == 'test' ? 1 : 0, 'Integer');
+               AND pp.is_active";
 
     if (isset($params['processor_id'])) {
       $sql .= " WHERE pp.id = %2";
       $args[2] = array($params['processor_id'], 'Integer');
-      $notfound = "No active instances of payment processor ID#'{$params['processor_id']}'  were found.";
+      $notFound = "No active instances of payment processor ID#'{$params['processor_id']}'  were found.";
     }
     else {
-      $sql .= " WHERE ppt.name = %2";
+      // This is called when processor_name is passed - passing processor_id instead is recommended.
+      $sql .= " WHERE ppt.name = %2 AND pp.is_test = %1";
+      $args[1] = array((CRM_Utils_Array::value('mode', $params) == 'test') ? 1 : 0, 'Integer');
       $args[2] = array($params['processor_name'], 'String');
-      $notfound = "No active instances of the '{$params['processor_name']}' payment processor were found.";
+      $notFound = "No active instances of the '{$params['processor_name']}' payment processor were found.";
     }
 
     $dao = CRM_Core_DAO::executeQuery($sql, $args);
 
-    // Check whether we found anything at all ..
+    // Check whether we found anything at all.
     if (!$dao->N) {
-      CRM_Core_Error::fatal($notfound);
+      CRM_Core_Error::fatal($notFound);
     }
 
     $method = 'handle' . $method;
@@ -558,16 +589,13 @@ abstract class CRM_Core_Payment {
         }
       }
 
-      $paymentProcessor = CRM_Financial_BAO_PaymentProcessor::getPayment($dao->processor_id, $mode);
+      $processorInstance = $processorInstance = Civi\Payment\System::singleton()->getById($dao->processor_id);
 
       // Should never be empty - we already established this processor_id exists and is active.
-      if (empty($paymentProcessor)) {
+      if (empty($processorInstance)) {
         continue;
       }
 
-      // Instantiate PP
-      $processorInstance = $paymentClass::singleton($mode, $paymentProcessor);
-
       // Does PP implement this method, and can we call it?
       if (!method_exists($processorInstance, $method) ||
         !is_callable(array($processorInstance, $method))
@@ -582,74 +610,75 @@ abstract class CRM_Core_Payment {
       $extension_instance_found = TRUE;
     }
 
-    if (!$extension_instance_found) CRM_Core_Error::fatal(
-      "No extension instances of the '{$params['processor_name']}' payment processor were found.<br />" .
-      "$method method is unsupported in legacy payment processors."
-    );
-
-    // Exit here on web requests, allowing just the plain text response to be echoed
-    if ($method == 'handlePaymentNotification') {
-      CRM_Utils_System::civiExit();
+    if (!$extension_instance_found) {
+      CRM_Core_Error::fatal(
+        "No extension instances of the '{$params['processor_name']}' payment processor were found.<br />" .
+        "$method method is unsupported in legacy payment processors."
+      );
     }
   }
 
   /**
    * Check whether a method is present ( & supported ) by the payment processor object.
    *
-   * @param  string $method method to check for.
+   * @param string $method
+   *   Method to check for.
    *
-   * @return boolean
-   * @public
+   * @return bool
    */
-  function isSupported($method = 'cancelSubscription') {
+  public function isSupported($method = 'cancelSubscription') {
     return method_exists(CRM_Utils_System::getClassName($this), $method);
   }
 
   /**
+   * Get url for users to manage this recurring contribution for this processor.
+   *
    * @param int $entityID
    * @param null $entity
    * @param string $action
    *
    * @return string
    */
-  function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') {
+  public function subscriptionURL($entityID = NULL, $entity = NULL, $action = 'cancel') {
     // Set URL
     switch ($action) {
-      case 'cancel' :
+      case 'cancel':
         $url = 'civicrm/contribute/unsubscribe';
         break;
-      case 'billing' :
+
+      case 'billing':
         //in notify mode don't return the update billing url
         if (!$this->isSupported('updateSubscriptionBillingInfo')) {
           return NULL;
         }
         $url = 'civicrm/contribute/updatebilling';
         break;
-      case 'update' :
+
+      case 'update':
         $url = 'civicrm/contribute/updaterecur';
         break;
     }
 
-    $session       = CRM_Core_Session::singleton();
-    $userId        = $session->get('userID');
-    $contactID     = 0;
+    $session = CRM_Core_Session::singleton();
+    $userId = $session->get('userID');
+    $contactID = 0;
     $checksumValue = '';
-    $entityArg     = '';
+    $entityArg = '';
 
     // Find related Contact
     if ($entityID) {
       switch ($entity) {
-        case 'membership' :
+        case 'membership':
           $contactID = CRM_Core_DAO::getFieldValue("CRM_Member_DAO_Membership", $entityID, "contact_id");
           $entityArg = 'mid';
           break;
 
-        case 'contribution' :
+        case 'contribution':
           $contactID = CRM_Core_DAO::getFieldValue("CRM_Contribute_DAO_Contribution", $entityID, "contact_id");
           $entityArg = 'coid';
           break;
 
-        case 'recur' :
+        case 'recur':
           $sql = "
     SELECT con.contact_id
       FROM civicrm_contribution_recur rec
@@ -679,4 +708,5 @@ INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
     // Else default
     return $this->_paymentProcessor['url_recur'];
   }
+
 }