Merge pull request #15026 from civicrm/5.17
[civicrm-core.git] / CRM / Core / Payment.php
index 2211c7309a13f81f9278b52e1242e3b406841b6d..502d332df305b91535b98bb04b91b191ca6d991f 100644 (file)
@@ -3,7 +3,7 @@
  +--------------------------------------------------------------------+
  | CiviCRM version 5                                                  |
  +--------------------------------------------------------------------+
- | Copyright CiviCRM LLC (c) 2004-2018                                |
+ | Copyright CiviCRM LLC (c) 2004-2019                                |
  +--------------------------------------------------------------------+
  | This file is a part of CiviCRM.                                    |
  |                                                                    |
@@ -80,6 +80,9 @@ abstract class CRM_Core_Payment {
     RECURRING_PAYMENT_START = 'START',
     RECURRING_PAYMENT_END = 'END';
 
+  /**
+   * @var object
+   */
   protected $_paymentProcessor;
 
   /**
@@ -108,9 +111,9 @@ abstract class CRM_Core_Payment {
    *
    * (Deprecated parameter but used in some messages).
    *
+   * @var string
    * @deprecated
    *
-   * @var string
    */
   public $_processorName;
 
@@ -258,7 +261,14 @@ abstract class CRM_Core_Payment {
     }
 
     $log = new CRM_Utils_SystemLogger();
-    $log->alert($message, $_REQUEST);
+    // $_REQUEST doesn't handle JSON, to support providers that POST JSON we need the raw POST data.
+    $rawRequestData = file_get_contents("php://input");
+    if (CRM_Utils_JSON::isValidJSON($rawRequestData)) {
+      $log->alert($message, json_decode($rawRequestData, TRUE));
+    }
+    else {
+      $log->alert($message, $_REQUEST);
+    }
   }
 
   /**
@@ -336,6 +346,15 @@ abstract class CRM_Core_Payment {
     return TRUE;
   }
 
+  /**
+   * Does this payment processor support refund?
+   *
+   * @return bool
+   */
+  public function supportsRefund() {
+    return FALSE;
+  }
+
   /**
    * Should the first payment date be configurable when setting up back office recurring payments.
    *
@@ -375,6 +394,18 @@ abstract class CRM_Core_Payment {
     return FALSE;
   }
 
+  /**
+   * Does this processor support updating billing info for recurring contributions through code.
+   *
+   * If the processor returns true then it must be possible to update billing info from within CiviCRM
+   * that will be updated at the payment processor.
+   *
+   * @return bool
+   */
+  protected function supportsUpdateSubscriptionBillingInfo() {
+    return method_exists(CRM_Utils_System::getClassName($this), 'updateSubscriptionBillingInfo');
+  }
+
   /**
    * Can recurring contributions be set against pledges.
    *
@@ -401,7 +432,8 @@ abstract class CRM_Core_Payment {
    *   - pre_approval_parameters (this will be stored on the calling form & available later)
    *   - redirect_url (if set the browser will be redirected to this.
    */
-  public function doPreApproval(&$params) {}
+  public function doPreApproval(&$params) {
+  }
 
   /**
    * Get any details that may be available to the payment processor due to an approval process having happened.
@@ -414,7 +446,7 @@ abstract class CRM_Core_Payment {
    * @return array
    */
   public function getPreApprovalDetails($storedDetails) {
-    return array();
+    return [];
   }
 
   /**
@@ -497,7 +529,10 @@ abstract class CRM_Core_Payment {
     switch ($context) {
       case 'contributionPageRecurringHelp':
         // require exactly two parameters
-        if (array_keys($params) == array('is_recur_installments', 'is_email_receipt')) {
+        if (array_keys($params) == [
+          'is_recur_installments',
+          'is_email_receipt',
+        ]) {
           $gotText = ts('Your recurring contribution will be processed automatically.');
           if ($params['is_recur_installments']) {
             $gotText .= ' ' . ts('You can specify the number of installments, or you can leave the number of installments blank if you want to make an open-ended commitment. In either case, you can choose to cancel at any time.');
@@ -506,9 +541,23 @@ abstract class CRM_Core_Payment {
             $gotText .= ' ' . ts('You will receive an email receipt for each recurring contribution.');
           }
         }
-        break;
+        return $gotText;
+
+      case 'contributionPageContinueText':
+        if ($params['amount'] <= 0) {
+          return ts('To complete this transaction, click the <strong>Continue</strong> button below.');
+        }
+        if ($this->_paymentProcessor['billing_mode'] == 4) {
+          return ts('Click the <strong>Continue</strong> button to go to %1, where you will select your payment method and complete the contribution.', [$this->_paymentProcessor['payment_processor_type']]);
+        }
+        if ($params['is_payment_to_existing']) {
+          return ts('To complete this transaction, click the <strong>Make Payment</strong> button below.');
+        }
+        return ts('To complete your contribution, click the <strong>Continue</strong> button below.');
+
     }
-    return $gotText;
+    CRM_Core_Error::deprecatedFunctionWarning('Calls to getText must use a supported method');
+    return '';
   }
 
   /**
@@ -559,7 +608,7 @@ abstract class CRM_Core_Payment {
    */
   public function getPaymentFormFields() {
     if ($this->_paymentProcessor['billing_mode'] == 4) {
-      return array();
+      return [];
     }
     return $this->_paymentProcessor['payment_type'] == 1 ? $this->getCreditCardFormFields() : $this->getDirectDebitFormFields();
   }
@@ -590,9 +639,10 @@ abstract class CRM_Core_Payment {
    * @return array
    */
   public function getEditableRecurringScheduleFields() {
-    if (method_exists($this, 'changeSubscriptionAmount')) {
-      return array('amount');
+    if ($this->supports('changeSubscriptionAmount')) {
+      return ['amount'];
     }
+    return [];
   }
 
   /**
@@ -615,7 +665,7 @@ abstract class CRM_Core_Payment {
    * @return array;
    */
   protected function getMandatoryFields() {
-    $mandatoryFields = array();
+    $mandatoryFields = [];
     foreach ($this->getAllFields() as $field_name => $field_spec) {
       if (!empty($field_spec['is_required'])) {
         $mandatoryFields[$field_name] = $field_spec;
@@ -634,18 +684,19 @@ abstract class CRM_Core_Payment {
     $billingFields = array_intersect_key($this->getBillingAddressFieldsMetadata(), array_flip($this->getBillingAddressFields()));
     return array_merge($paymentFields, $billingFields);
   }
+
   /**
    * Get array of fields that should be displayed on the payment form for credit cards.
    *
    * @return array
    */
   protected function getCreditCardFormFields() {
-    return array(
+    return [
       'credit_card_type',
       'credit_card_number',
       'cvv2',
       'credit_card_exp_date',
-    );
+    ];
   }
 
   /**
@@ -654,12 +705,12 @@ abstract class CRM_Core_Payment {
    * @return array
    */
   protected function getDirectDebitFormFields() {
-    return array(
+    return [
       'account_holder',
       'bank_account_number',
       'bank_identification_number',
       'bank_name',
-    );
+    ];
   }
 
   /**
@@ -672,158 +723,164 @@ abstract class CRM_Core_Payment {
    */
   public function getPaymentFormFieldsMetadata() {
     //@todo convert credit card type into an option value
-    $creditCardType = array('' => ts('- select -')) + CRM_Contribute_PseudoConstant::creditCard();
+    $creditCardType = ['' => ts('- select -')] + CRM_Contribute_PseudoConstant::creditCard();
     $isCVVRequired = Civi::settings()->get('cvv_backoffice_required');
     if (!$this->isBackOffice()) {
       $isCVVRequired = TRUE;
     }
-    return array(
-      'credit_card_number' => array(
+    return [
+      'credit_card_number' => [
         'htmlType' => 'text',
         'name' => 'credit_card_number',
         'title' => ts('Card Number'),
-        'attributes' => array(
+        'attributes' => [
           'size' => 20,
           'maxlength' => 20,
           'autocomplete' => 'off',
           'class' => 'creditcard',
-        ),
+        ],
         'is_required' => TRUE,
-      ),
-      'cvv2' => array(
+        // 'description' => '16 digit card number', // If you enable a description field it will be shown below the field on the form
+      ],
+      'cvv2' => [
         'htmlType' => 'text',
         'name' => 'cvv2',
         'title' => ts('Security Code'),
-        'attributes' => array(
+        'attributes' => [
           'size' => 5,
           'maxlength' => 10,
           'autocomplete' => 'off',
-        ),
+        ],
         'is_required' => $isCVVRequired,
-        'rules' => array(
-          array(
+        'rules' => [
+          [
             '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(
+          ],
+        ],
+      ],
+      'credit_card_exp_date' => [
         'htmlType' => 'date',
         'name' => 'credit_card_exp_date',
         'title' => ts('Expiration Date'),
         'attributes' => CRM_Core_SelectValues::date('creditCard'),
         'is_required' => TRUE,
-        'rules' => array(
-          array(
+        'rules' => [
+          [
             'rule_message' => ts('Card expiration date cannot be a past date.'),
             'rule_name' => 'currentDate',
             'rule_parameters' => TRUE,
-          ),
-        ),
-      ),
-      'credit_card_type' => array(
+          ],
+        ],
+        'extra' => ['class' => 'crm-form-select'],
+      ],
+      'credit_card_type' => [
         'htmlType' => 'select',
         'name' => 'credit_card_type',
         'title' => ts('Card Type'),
         'attributes' => $creditCardType,
         'is_required' => FALSE,
-      ),
-      'account_holder' => array(
+      ],
+      'account_holder' => [
         'htmlType' => 'text',
         'name' => 'account_holder',
         'title' => ts('Account Holder'),
-        'attributes' => array(
+        'attributes' => [
           'size' => 20,
           'maxlength' => 34,
           'autocomplete' => 'on',
-        ),
+        ],
         'is_required' => TRUE,
-      ),
+      ],
       //e.g. IBAN can have maxlength of 34 digits
-      'bank_account_number' => array(
+      'bank_account_number' => [
         'htmlType' => 'text',
         'name' => 'bank_account_number',
         'title' => ts('Bank Account Number'),
-        'attributes' => array(
+        'attributes' => [
           'size' => 20,
           'maxlength' => 34,
           'autocomplete' => 'off',
-        ),
-        'rules' => array(
-          array(
+        ],
+        'rules' => [
+          [
             '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
-      'bank_identification_number' => array(
+      'bank_identification_number' => [
         'htmlType' => 'text',
         'name' => 'bank_identification_number',
         'title' => ts('Bank Identification Number'),
-        'attributes' => array(
+        'attributes' => [
           'size' => 20,
           'maxlength' => 11,
           'autocomplete' => 'off',
-        ),
+        ],
         'is_required' => TRUE,
-        'rules' => array(
-          array(
+        'rules' => [
+          [
             '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(
+          ],
+        ],
+      ],
+      'bank_name' => [
         'htmlType' => 'text',
         'name' => 'bank_name',
         'title' => ts('Bank Name'),
-        'attributes' => array(
+        'attributes' => [
           'size' => 20,
           'maxlength' => 64,
           'autocomplete' => 'off',
-        ),
+        ],
         'is_required' => TRUE,
 
-      ),
-      'check_number' => array(
+      ],
+      'check_number' => [
         'htmlType' => 'text',
         'name' => 'check_number',
         'title' => ts('Check Number'),
         'is_required' => FALSE,
         'attributes' => NULL,
-      ),
-      'pan_truncation' => array(
+      ],
+      'pan_truncation' => [
         'htmlType' => 'text',
         'name' => 'pan_truncation',
         'title' => ts('Last 4 digits of the card'),
         'is_required' => FALSE,
-        'attributes' => array(
+        'attributes' => [
           'size' => 4,
           'maxlength' => 4,
           'minlength' => 4,
           'autocomplete' => 'off',
-        ),
-        'rules' => array(
-          array(
+        ],
+        'rules' => [
+          [
             'rule_message' => ts('Please enter valid last 4 digit card number.'),
             'rule_name' => 'numeric',
             'rule_parameters' => NULL,
-          ),
-        ),
-      ),
-      'payment_token' => array(
+          ],
+        ],
+      ],
+      'payment_token' => [
         'htmlType' => 'hidden',
         'name' => 'payment_token',
         'title' => ts('Authorization token'),
         'is_required' => FALSE,
-        'attributes' => ['size' => 10, 'autocomplete' => 'off', 'id' => 'payment_token'],
-      ),
-    );
+        'attributes' => [
+          'size' => 10,
+          'autocomplete' => 'off',
+          'id' => 'payment_token',
+        ],
+      ],
+    ];
   }
 
   /**
@@ -844,9 +901,9 @@ abstract class CRM_Core_Payment {
       $billingLocationID = CRM_Core_BAO_LocationType::getBilling();
     }
     if ($this->_paymentProcessor['billing_mode'] != 1 && $this->_paymentProcessor['billing_mode'] != 3) {
-      return array();
+      return [];
     }
-    return array(
+    return [
       'first_name' => 'billing_first_name',
       'middle_name' => 'billing_middle_name',
       'last_name' => 'billing_last_name',
@@ -855,7 +912,7 @@ abstract class CRM_Core_Payment {
       'country' => "billing_country_id-{$billingLocationID}",
       'state_province' => "billing_state_province_id-{$billingLocationID}",
       'postal_code' => "billing_postal_code-{$billingLocationID}",
-    );
+    ];
   }
 
   /**
@@ -864,7 +921,7 @@ abstract class CRM_Core_Payment {
    * @param int $billingLocationID
    *
    * @return array
-   *    Array of metadata for address fields.
+   *   Array of metadata for address fields.
    */
   public function getBillingAddressFieldsMetadata($billingLocationID = NULL) {
     if (!$billingLocationID) {
@@ -873,103 +930,103 @@ abstract class CRM_Core_Payment {
       // So taking this as a param is possibly something to be removed in favour of the standard default.
       $billingLocationID = CRM_Core_BAO_LocationType::getBilling();
     }
-    $metadata = array();
-    $metadata['billing_first_name'] = array(
+    $metadata = [];
+    $metadata['billing_first_name'] = [
       'htmlType' => 'text',
       'name' => 'billing_first_name',
       'title' => ts('Billing First Name'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
-      ),
+      ],
       'is_required' => TRUE,
-    );
+    ];
 
-    $metadata['billing_middle_name'] = array(
+    $metadata['billing_middle_name'] = [
       'htmlType' => 'text',
       'name' => 'billing_middle_name',
       'title' => ts('Billing Middle Name'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
-      ),
+      ],
       'is_required' => FALSE,
-    );
+    ];
 
-    $metadata['billing_last_name'] = array(
+    $metadata['billing_last_name'] = [
       'htmlType' => 'text',
       'name' => 'billing_last_name',
       'title' => ts('Billing Last Name'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
-      ),
+      ],
       'is_required' => TRUE,
-    );
+    ];
 
-    $metadata["billing_street_address-{$billingLocationID}"] = array(
+    $metadata["billing_street_address-{$billingLocationID}"] = [
       'htmlType' => 'text',
       'name' => "billing_street_address-{$billingLocationID}",
       'title' => ts('Street Address'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
-      ),
+      ],
       'is_required' => TRUE,
-    );
+    ];
 
-    $metadata["billing_city-{$billingLocationID}"] = array(
+    $metadata["billing_city-{$billingLocationID}"] = [
       'htmlType' => 'text',
       'name' => "billing_city-{$billingLocationID}",
       'title' => ts('City'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
-      ),
+      ],
       'is_required' => TRUE,
-    );
+    ];
 
-    $metadata["billing_state_province_id-{$billingLocationID}"] = array(
+    $metadata["billing_state_province_id-{$billingLocationID}"] = [
       'htmlType' => 'chainSelect',
       'title' => ts('State/Province'),
       'name' => "billing_state_province_id-{$billingLocationID}",
       'cc_field' => TRUE,
       'is_required' => TRUE,
-    );
+    ];
 
-    $metadata["billing_postal_code-{$billingLocationID}"] = array(
+    $metadata["billing_postal_code-{$billingLocationID}"] = [
       'htmlType' => 'text',
       'name' => "billing_postal_code-{$billingLocationID}",
       'title' => ts('Postal Code'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         'size' => 30,
         'maxlength' => 60,
         'autocomplete' => 'off',
-      ),
+      ],
       'is_required' => TRUE,
-    );
+    ];
 
-    $metadata["billing_country_id-{$billingLocationID}"] = array(
+    $metadata["billing_country_id-{$billingLocationID}"] = [
       'htmlType' => 'select',
       'name' => "billing_country_id-{$billingLocationID}",
       'title' => ts('Country'),
       'cc_field' => TRUE,
-      'attributes' => array(
+      'attributes' => [
         '' => ts('- select -'),
-      ) + CRM_Core_PseudoConstant::country(),
+      ] + CRM_Core_PseudoConstant::country(),
       'is_required' => TRUE,
-    );
+    ];
     return $metadata;
   }
 
@@ -1033,20 +1090,20 @@ abstract class CRM_Core_Payment {
     }
 
     if ($this->_component == 'event') {
-      return CRM_Utils_System::url($this->getBaseReturnUrl(), array(
+      return CRM_Utils_System::url($this->getBaseReturnUrl(), [
         'reset' => 1,
         'cc' => 'fail',
         'participantId' => $participantID,
-      ),
+      ],
         TRUE, NULL, FALSE
       );
     }
 
-    return CRM_Utils_System::url($this->getBaseReturnUrl(), array(
+    return CRM_Utils_System::url($this->getBaseReturnUrl(), [
       '_qf_Main_display' => 1,
       'qfKey' => $qfKey,
       'cancel' => 1,
-    ),
+    ],
       TRUE, NULL, FALSE
     );
   }
@@ -1063,10 +1120,10 @@ abstract class CRM_Core_Payment {
       return $this->successUrl;
     }
 
-    return CRM_Utils_System::url($this->getBaseReturnUrl(), array(
+    return CRM_Utils_System::url($this->getBaseReturnUrl(), [
       '_qf_ThankYou_display' => 1,
       'qfKey' => $qfKey,
-    ),
+    ],
       TRUE, NULL, FALSE
     );
   }
@@ -1109,10 +1166,10 @@ abstract class CRM_Core_Payment {
    * @return string url
    */
   protected function getGoBackUrl($qfKey) {
-    return CRM_Utils_System::url($this->getBaseReturnUrl(), array(
+    return CRM_Utils_System::url($this->getBaseReturnUrl(), [
       '_qf_Confirm_display' => 'true',
       'qfKey' => $qfKey,
-    ),
+    ],
       TRUE, NULL, FALSE
     );
   }
@@ -1129,17 +1186,18 @@ abstract class CRM_Core_Payment {
   protected function getNotifyUrl() {
     $url = CRM_Utils_System::url(
       'civicrm/payment/ipn/' . $this->_paymentProcessor['id'],
-      array(),
+      [],
       TRUE,
       NULL,
-      FALSE
+      FALSE,
+      TRUE
     );
     return (stristr($url, '.')) ? $url : '';
   }
 
   /**
    * Calling this from outside the payment subsystem is deprecated - use doPayment.
-   *
+   * @deprecated
    * Does a server to server payment transaction.
    *
    * @param array $params
@@ -1153,27 +1211,26 @@ abstract class CRM_Core_Payment {
   }
 
   /**
-   * Process payment - this function wraps around both doTransferPayment and doDirectPayment.
-   *
-   * The function ensures an exception is thrown & moves some of this logic out of the form layer and makes the forms
-   * more agnostic.
+   * Process payment - this function wraps around both doTransferCheckout and doDirectPayment.
+   * Any processor that still implements the deprecated doTransferCheckout() or doDirectPayment() should be updated to use doPayment().
    *
-   * Payment processors should set payment_status_id. This function adds some historical defaults ie. the
-   * assumption that if a 'doDirectPayment' processors comes back it completed the transaction & in fact
-   * doTransferCheckout would not traditionally come back.
+   * This function adds some historical defaults ie. the assumption that if a 'doDirectPayment' processors comes back it completed
+   *   the transaction & in fact doTransferCheckout would not traditionally come back.
+   * Payment processors should throw exceptions and not return Error objects as they may have done with the old functions.
    *
-   * doDirectPayment does not do an immediate payment for Authorize.net or Paypal so the default is assumed
-   * to be Pending.
+   * Payment processors should set payment_status_id (which is really contribution_status_id) in the returned array. The default is assumed to be Pending.
+   *   In some cases the IPN will set the payment to "Completed" some time later.
    *
-   * Once this function is fully rolled out then it will be preferred for processors to throw exceptions than to
-   * return Error objects
+   * @fixme Creating a contribution record is inconsistent! We should always create a contribution BEFORE calling doPayment...
+   *  For the current status see: https://lab.civicrm.org/dev/financial/issues/53
+   * If we DO have a contribution ID, then the payment processor can (and should) update parameters on the contribution record as necessary.
    *
    * @param array $params
    *
    * @param string $component
    *
    * @return array
-   *   Result array
+   *   Result array (containing at least the key payment_status_id)
    *
    * @throws \Civi\Payment\Exception\PaymentProcessorException
    */
@@ -1213,6 +1270,17 @@ abstract class CRM_Core_Payment {
     return $result;
   }
 
+  /**
+   * Refunds payment
+   *
+   * Payment processors should set payment_status_id if it set the status to Refunded in case the transaction is successful
+   *
+   * @param array $params
+   *
+   * @throws \Civi\Payment\Exception\PaymentProcessorException
+   */
+  public function doRefund(&$params) {}
+
   /**
    * Query payment processor for details about a transaction.
    *
@@ -1228,7 +1296,7 @@ abstract class CRM_Core_Payment {
    *   - fee_amount Amount of fee paid
    */
   public function doQuery($params) {
-    return array();
+    return [];
   }
 
   /**
@@ -1273,14 +1341,23 @@ abstract class CRM_Core_Payment {
    * Page callback for civicrm/payment/ipn
    */
   public static function handleIPN() {
-    self::handlePaymentMethod(
-      'PaymentNotification',
-      array(
-        'processor_name' => @$_GET['processor_name'],
-        'processor_id' => @$_GET['processor_id'],
-        'mode' => @$_GET['mode'],
-      )
-    );
+    try {
+      self::handlePaymentMethod(
+        'PaymentNotification',
+        [
+          'processor_name' => CRM_Utils_Request::retrieveValue('processor_name', 'String'),
+          'processor_id' => CRM_Utils_Request::retrieveValue('processor_id', 'Integer'),
+          'mode' => CRM_Utils_Request::retrieveValue('mode', 'Alphanumeric'),
+        ]
+      );
+    }
+    catch (CRM_Core_Exception $e) {
+      Civi::log()->error('ipn_payment_callback_exception', [
+        'context' => [
+          'backtrace' => CRM_Core_Error::formatBacktrace(debug_backtrace()),
+        ],
+      ]);
+    }
     CRM_Utils_System::civiExit();
   }
 
@@ -1301,7 +1378,7 @@ abstract class CRM_Core_Payment {
    * @throws \CRM_Core_Exception
    * @throws \Exception
    */
-  public static function handlePaymentMethod($method, $params = array()) {
+  public static function handlePaymentMethod($method, $params = []) {
     if (!isset($params['processor_id']) && !isset($params['processor_name'])) {
       $q = explode('/', CRM_Utils_Array::value(CRM_Core_Config::singleton()->userFrameworkURLVar, $_GET, ''));
       $lastParam = array_pop($q);
@@ -1324,25 +1401,25 @@ abstract class CRM_Core_Payment {
 
     if (isset($params['processor_id'])) {
       $sql .= " WHERE pp.id = %2";
-      $args[2] = array($params['processor_id'], 'Integer');
-      $notFound = ts("No active instances of payment processor %1 were found.", array(1 => $params['processor_id']));
+      $args[2] = [$params['processor_id'], 'Integer'];
+      $notFound = ts("No active instances of payment processor %1 were found.", [1 => $params['processor_id']]);
     }
     else {
       // 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(
+      $args[1] = [
         (CRM_Utils_Array::value('mode', $params) == 'test') ? 1 : 0,
         'Integer',
-      );
-      $args[2] = array($params['processor_name'], 'String');
-      $notFound = ts("No active instances of payment processor '%1' were found.", array(1 => $params['processor_name']));
+      ];
+      $args[2] = [$params['processor_name'], 'String'];
+      $notFound = ts("No active instances of payment processor '%1' were found.", [1 => $params['processor_name']]);
     }
 
     $dao = CRM_Core_DAO::executeQuery($sql, $args);
 
     // Check whether we found anything at all.
     if (!$dao->N) {
-      CRM_Core_Error::fatal($notFound);
+      throw new CRM_Core_Exception($notFound);
     }
 
     $method = 'handle' . $method;
@@ -1368,7 +1445,7 @@ abstract class CRM_Core_Payment {
 
       // Does PP implement this method, and can we call it?
       if (!method_exists($processorInstance, $method) ||
-        !is_callable(array($processorInstance, $method))
+        !is_callable([$processorInstance, $method])
       ) {
         // on the off chance there is a double implementation of this processor we should keep looking for another
         // note that passing processor_id is more reliable & we should work to deprecate processor_name
@@ -1380,10 +1457,16 @@ abstract class CRM_Core_Payment {
       $extension_instance_found = TRUE;
     }
 
+    // Call IPN postIPNProcess hook to allow for custom processing of IPN data.
+    $IPNParams = array_merge($_GET, $_REQUEST);
+    CRM_Utils_Hook::postIPNProcess($IPNParams);
     if (!$extension_instance_found) {
       $message = "No extension instances of the '%1' payment processor were found.<br />" .
         "%2 method is unsupported in legacy payment processors.";
-      CRM_Core_Error::fatal(ts($message, array(1 => $params['processor_name'], 2 => $method)));
+      throw new CRM_Core_Exception(ts($message, [
+        1 => $params['processor_name'],
+        2 => $method,
+      ]));
     }
   }
 
@@ -1448,18 +1531,24 @@ abstract class CRM_Core_Payment {
     // Set URL
     switch ($action) {
       case 'cancel':
+        if (!$this->supports('cancelRecurring')) {
+          return NULL;
+        }
         $url = 'civicrm/contribute/unsubscribe';
         break;
 
       case 'billing':
         //in notify mode don't return the update billing url
-        if (!$this->isSupported('updateSubscriptionBillingInfo')) {
+        if (!$this->supports('updateSubscriptionBillingInfo')) {
           return NULL;
         }
         $url = 'civicrm/contribute/updatebilling';
         break;
 
       case 'update':
+        if (!$this->supports('changeSubscriptionAmount') && !$this->supports('editRecurringContribution')) {
+          return NULL;
+        }
         $url = 'civicrm/contribute/updaterecur';
         break;
     }
@@ -1488,7 +1577,12 @@ abstract class CRM_Core_Payment {
       FROM civicrm_contribution_recur rec
 INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
      WHERE rec.id = %1";
-          $contactID = CRM_Core_DAO::singleValueQuery($sql, array(1 => array($entityID, 'Integer')));
+          $contactID = CRM_Core_DAO::singleValueQuery($sql, [
+            1 => [
+              $entityID,
+              'Integer',
+            ],
+          ]);
           $entityArg = 'crid';
           break;
       }
@@ -1504,7 +1598,7 @@ INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
     }
 
     // Else login URL
-    if ($this->isSupported('accountLoginURL')) {
+    if ($this->supports('accountLoginURL')) {
       return $this->accountLoginURL();
     }
 
@@ -1527,10 +1621,19 @@ INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
    * @return string
    */
   protected function getPaymentDescription($params, $length = 24) {
-    $parts = array('contactID', 'contributionID', 'description', 'billing_first_name', 'billing_last_name');
-    $validParts = array();
+    $parts = [
+      'contactID',
+      'contributionID',
+      'description',
+      'billing_first_name',
+      'billing_last_name',
+    ];
+    $validParts = [];
     if (isset($params['description'])) {
-      $uninformativeStrings = array(ts('Online Event Registration: '), ts('Online Contribution: '));
+      $uninformativeStrings = [
+        ts('Online Event Registration: '),
+        ts('Online Contribution: '),
+      ];
       $params['description'] = str_replace($uninformativeStrings, '', $params['description']);
     }
     foreach ($parts as $part) {
@@ -1550,6 +1653,18 @@ INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
     return FALSE;
   }
 
+  /**
+   * Does this processor support changing the amount for recurring contributions through code.
+   *
+   * If the processor returns true then it must be possible to update the amount from within CiviCRM
+   * that will be updated at the payment processor.
+   *
+   * @return bool
+   */
+  protected function supportsChangeSubscriptionAmount() {
+    return method_exists(CRM_Utils_System::getClassName($this), 'changeSubscriptionAmount');
+  }
+
   /**
    * Checks if payment processor supports recurring contributions
    *
@@ -1562,6 +1677,17 @@ INNER JOIN civicrm_contribution con ON ( con.contribution_recur_id = rec.id )
     return FALSE;
   }
 
+  /**
+   * Checks if payment processor supports an account login URL
+   * TODO: This is checked by self::subscriptionURL but is only used if no entityID is found.
+   * TODO: It is implemented by AuthorizeNET, any others?
+   *
+   * @return bool
+   */
+  protected function supportsAccountLoginURL() {
+    return method_exists(CRM_Utils_System::getClassName($this), 'accountLoginURL');
+  }
+
   /**
    * Should a receipt be sent out for a pending payment.
    *