From d28038514c58ae9423d85b2bb114a70f82b6117a Mon Sep 17 00:00:00 2001 From: Matthew Wire Date: Sun, 1 Jul 2018 19:53:27 +0100 Subject: [PATCH] NFC code cleanup for AuthNet, Paypal, PaypalPro IPNs --- CRM/Core/Payment/AuthorizeNetIPN.php | 2 +- CRM/Core/Payment/PayPalIPN.php | 139 +++++++++++++-------------- CRM/Core/Payment/PayPalProIPN.php | 114 +++++++++++----------- 3 files changed, 122 insertions(+), 133 deletions(-) diff --git a/CRM/Core/Payment/AuthorizeNetIPN.php b/CRM/Core/Payment/AuthorizeNetIPN.php index 2696e2a1ab..e350195886 100644 --- a/CRM/Core/Payment/AuthorizeNetIPN.php +++ b/CRM/Core/Payment/AuthorizeNetIPN.php @@ -77,7 +77,7 @@ class CRM_Core_Payment_AuthorizeNetIPN extends CRM_Core_Payment_BaseIPN { // processor id & the handleNotification function (which should call the completetransaction api & by-pass this // entirely). The only thing the IPN class should really do is extract data from the request, validate it // & call completetransaction or call fail? (which may not exist yet). - Civi::log()->warning('Unreliable method used for AuthNet IPN - this will cause problems if you have more than one instance'); + Civi::log()->warning('Unreliable method used to get payment_processor_id for AuthNet IPN - this will cause problems if you have more than one instance'); $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'AuthNet', 'id', 'name' ); diff --git a/CRM/Core/Payment/PayPalIPN.php b/CRM/Core/Payment/PayPalIPN.php index 0100ca27d5..b058eb63f6 100644 --- a/CRM/Core/Payment/PayPalIPN.php +++ b/CRM/Core/Payment/PayPalIPN.php @@ -29,7 +29,6 @@ * * @package CRM * @copyright CiviCRM LLC (c) 2004-2018 - * $Id$ * */ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { @@ -52,7 +51,7 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { * @throws CRM_Core_Exception */ public function __construct($inputData) { - //CRM-19676 + // CRM-19676 $params = (!empty($inputData['custom'])) ? array_merge($inputData, json_decode($inputData['custom'], TRUE)) : $inputData; @@ -62,47 +61,46 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { /** * @param string $name - * @param $type + * @param string $type * @param bool $abort * * @return mixed + * @throws \CRM_Core_Exception */ public function retrieve($name, $type, $abort = TRUE) { - static $store = NULL; - $value = CRM_Utils_Type::validate( - CRM_Utils_Array::value($name, $this->_inputParameters), - $type, - FALSE - ); + $value = CRM_Utils_Type::validate(CRM_Utils_Array::value($name, $this->_inputParameters), $type, FALSE); if ($abort && $value === NULL) { - CRM_Core_Error::debug_log_message("Could not find an entry for $name"); + Civi::log()->debug("PayPalIPN: Could not find an entry for $name"); echo "Failure: Missing Parameter

" . CRM_Utils_Type::escape($name, 'String'); - exit(); + throw new CRM_Core_Exception("PayPalIPN: Could not find an entry for $name"); } return $value; } /** - * @param $input - * @param $ids - * @param $objects - * @param $first + * @param array $input + * @param array $ids + * @param array $objects + * @param bool $first + * + * @return void * - * @return bool + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception */ public function recur(&$input, &$ids, &$objects, $first) { if (!isset($input['txnType'])) { - CRM_Core_Error::debug_log_message("Could not find txn_type in input request"); + Civi::log()->debug('PayPalIPN: Could not find txn_type in input request'); echo "Failure: Invalid parameters

"; - return FALSE; + return; } if ($input['txnType'] == 'subscr_payment' && $input['paymentStatus'] != 'Completed' ) { - CRM_Core_Error::debug_log_message("Ignore all IPN payments that are not completed"); + Civi::log()->debug('PayPalIPN: Ignore all IPN payments that are not completed'); echo "Failure: Invalid parameters

"; - return FALSE; + return; } $recur = &$objects['contributionRecur']; @@ -110,9 +108,9 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { // make sure the invoice ids match // make sure the invoice is valid and matches what we have in the contribution record if ($recur->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request"); + Civi::log()->debug('PayPalIPN: Invoice values dont match between database and IPN request (RecurID: ' . $recur->id . ').'); echo "Failure: Invoice values dont match between database and IPN request

"; - return FALSE; + return; } $now = date('YmdHis'); @@ -127,18 +125,19 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } $sendNotification = FALSE; $subscriptionPaymentStatus = NULL; - //set transaction type + // set transaction type $txnType = $this->retrieve('txn_type', 'String'); + $contributionStatuses = array_flip(CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'validate')); switch ($txnType) { case 'subscr_signup': $recur->create_date = $now; - //some times subscr_signup response come after the - //subscr_payment and set to pending mode. + // sometimes subscr_signup response come after the subscr_payment and set to pending mode. + $statusID = CRM_Core_DAO::getFieldValue('CRM_Contribute_DAO_ContributionRecur', $recur->id, 'contribution_status_id' ); - if ($statusID != 5) { - $recur->contribution_status_id = 2; + if ($statusID != $contributionStatuses['In Progress']) { + $recur->contribution_status_id = $contributionStatuses['Pending']; } $recur->processor_id = $this->retrieve('subscr_id', 'String'); $recur->trxn_id = $recur->processor_id; @@ -147,8 +146,8 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { break; case 'subscr_eot': - if ($recur->contribution_status_id != 3) { - $recur->contribution_status_id = 1; + if ($recur->contribution_status_id != $contributionStatuses['Cancelled']) { + $recur->contribution_status_id = $contributionStatuses['Completed']; } $recur->end_date = $now; $sendNotification = TRUE; @@ -156,19 +155,19 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { break; case 'subscr_cancel': - $recur->contribution_status_id = 3; + $recur->contribution_status_id = $contributionStatuses['Cancelled']; $recur->cancel_date = $now; break; case 'subscr_failed': - $recur->contribution_status_id = 4; + $recur->contribution_status_id = $contributionStatuses['Failed']; $recur->modified_date = $now; break; case 'subscr_modify': - CRM_Core_Error::debug_log_message("We do not handle modifications to subscriptions right now"); + Civi::log()->debug('PayPalIPN: We do not handle modifications to subscriptions right now (RecurID: ' . $recur->id . ').'); echo "Failure: We do not handle modifications to subscriptions right now

"; - return FALSE; + return; case 'subscr_payment': if ($first) { @@ -180,8 +179,8 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { // make sure the contribution status is not done // since order of ipn's is unknown - if ($recur->contribution_status_id != 1) { - $recur->contribution_status_id = 5; + if ($recur->contribution_status_id != $contributionStatuses['Completed']) { + $recur->contribution_status_id = $contributionStatuses['In Progress']; } break; } @@ -189,7 +188,6 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { $recur->save(); if ($sendNotification) { - $autoRenewMembership = FALSE; if ($recur->id && isset($ids['membership']) && $ids['membership'] @@ -211,14 +209,18 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } if (!$first) { - //check if this contribution transaction is already processed - //if not create a contribution and then get it processed + // check if this contribution transaction is already processed + // if not create a contribution and then get it processed $contribution = new CRM_Contribute_BAO_Contribution(); $contribution->trxn_id = $input['trxn_id']; if ($contribution->trxn_id && $contribution->find()) { - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalIPN: Returning since contribution has already been handled (trxn_id: ' . $contribution->trxn_id . ')'); echo "Success: Contribution has already been handled

"; - return TRUE; + return; + } + + if ($input['paymentStatus'] != 'Completed') { + throw new CRM_Core_Exception("Ignore all IPN payments that are not completed"); } $contribution->contact_id = $ids['contact']; @@ -240,27 +242,23 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } /** - * @param $input - * @param $ids - * @param $objects + * @param array $input + * @param array $ids + * @param array $objects * @param bool $recur * @param bool $first * - * @return bool + * @return void */ - public function single( - &$input, &$ids, &$objects, - $recur = FALSE, - $first = FALSE - ) { + public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE) { $contribution = &$objects['contribution']; // make sure the invoice is valid and matches what we have in the contribution record if ((!$recur) || ($recur && $first)) { if ($contribution->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request"); + Civi::log()->debug('PayPalIPN: Invoice values dont match between database and IPN request. (ID: ' . $contribution->id . ').'); echo "Failure: Invoice values dont match between database and IPN request

"; - return FALSE; + return; } } else { @@ -269,9 +267,9 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { if (!$recur) { if ($contribution->total_amount != $input['amount']) { - CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request"); + Civi::log()->debug('PayPalIPN: Amount values dont match between database and IPN request. (ID: ' . $contribution->id . ').'); echo "Failure: Amount values dont match between database and IPN request

"; - return FALSE; + return; } } else { @@ -280,9 +278,6 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { $transaction = new CRM_Core_Transaction(); - $participant = &$objects['participant']; - $membership = &$objects['membership']; - $status = $input['paymentStatus']; if ($status == 'Denied' || $status == 'Failed' || $status == 'Voided') { return $this->failed($objects, $transaction); @@ -298,11 +293,12 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { } // check if contribution is already completed, if so we ignore this ipn - if ($contribution->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($contribution->contribution_status_id == $completedStatusId) { $transaction->commit(); - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalIPN: Returning since contribution has already been handled. (ID: ' . $contribution->id . ').'); echo "Success: Contribution has already been handled

"; - return TRUE; + return; } $this->completeTransaction($input, $ids, $objects, $transaction, $recur); @@ -311,10 +307,10 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { /** * Main function. * - * @return bool + * @throws \CRM_Core_Exception + * @throws \CiviCRM_API3_Exception */ public function main() { - $objects = $ids = $input = array(); $component = $this->retrieve('module', 'String'); $input['component'] = $component; @@ -361,29 +357,26 @@ class CRM_Core_Payment_PayPalIPN extends CRM_Core_Payment_BaseIPN { if ($ids['contributionRecur']) { // check if first contribution is completed, else complete first contribution $first = TRUE; - if ($objects['contribution']->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($objects['contribution']->contribution_status_id == $completedStatusId) { $first = FALSE; } - return $this->recur($input, $ids, $objects, $first); - } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); + $this->recur($input, $ids, $objects, $first); + return; } } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); - } + $this->single($input, $ids, $objects, FALSE, FALSE); } /** - * @param $input - * @param $ids + * @param array $input + * @param array $ids * - * @return bool + * @throws \CRM_Core_Exception */ public function getInput(&$input, &$ids) { if (!$this->getBillingID($ids)) { - return FALSE; + return; } $input['txnType'] = $this->retrieve('txn_type', 'String', FALSE); diff --git a/CRM/Core/Payment/PayPalProIPN.php b/CRM/Core/Payment/PayPalProIPN.php index 6a4fe29e29..f16c7ceae8 100644 --- a/CRM/Core/Payment/PayPalProIPN.php +++ b/CRM/Core/Payment/PayPalProIPN.php @@ -161,13 +161,13 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { * @param array $ids * @param array $objects * @param bool $first - * @return bool + * @return void */ public function recur(&$input, &$ids, &$objects, $first) { if (!isset($input['txnType'])) { - CRM_Core_Error::debug_log_message("Could not find txn_type in input request"); + Civi::log()->debug('PayPalProIPN: Could not find txn_type in input request.'); echo "Failure: Invalid parameters

"; - return FALSE; + return; } $recur = &$objects['contributionRecur']; @@ -176,9 +176,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // make sure the invoice is valid and matches what we have in // the contribution record if ($recur->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request recur is " . $recur->invoice_id . " input is " . $input['invoice']); + Civi::log()->debug('PayPalProIPN: Invoice values dont match between database and IPN request recur is ' . $recur->invoice_id . ' input is ' . $input['invoice']); echo "Failure: Invoice values dont match between database and IPN request recur is " . $recur->invoice_id . " input is " . $input['invoice']; - return FALSE; + return; } $now = date('YmdHis'); @@ -211,21 +211,20 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { //set transaction type $txnType = $this->retrieve('txn_type', 'String'); //Changes for paypal pro recurring payment - $contributionStatuses = civicrm_api3('contribution', 'getoptions', array('field' => 'contribution_status_id')); - $contributionStatuses = $contributionStatuses['values']; + $contributionStatuses = array_flip(CRM_Contribute_BAO_Contribution::buildOptions('contribution_status_id', 'validate')); switch ($txnType) { case 'recurring_payment_profile_created': if (in_array($recur->contribution_status_id, array( - array_search('Pending', $contributionStatuses), - array_search('In Progress', $contributionStatuses), + $contributionStatuses['Pending'], + $contributionStatuses['In Progress'], )) && !empty($recur->processor_id) ) { echo "already handled"; - return FALSE; + return; } $recur->create_date = $now; - $recur->contribution_status_id = 2; + $recur->contribution_status_id = $contributionStatuses['Pending']; $recur->processor_id = $this->retrieve('recurring_payment_id', 'String'); $recur->trxn_id = $recur->processor_id; $subscriptionPaymentStatus = CRM_Core_Payment::RECURRING_PAYMENT_START; @@ -256,9 +255,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { if ($this->retrieve('profile_status', 'String') == 'Expired') { if (!empty($recur->end_date)) { echo "already handled"; - return FALSE; + return; } - $recur->contribution_status_id = 1; + $recur->contribution_status_id = $contributionStatuses['Completed']; $recur->end_date = $now; $sendNotification = TRUE; $subscriptionPaymentStatus = CRM_Core_Payment::RECURRING_PAYMENT_END; @@ -266,8 +265,8 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // make sure the contribution status is not done // since order of ipn's is unknown - if ($recur->contribution_status_id != 1) { - $recur->contribution_status_id = 5; + if ($recur->contribution_status_id != $contributionStatuses['Completed']) { + $recur->contribution_status_id = $contributionStatuses['In Progress']; } break; } @@ -291,7 +290,7 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { } if ($txnType != 'recurring_payment') { - return TRUE; + return; } if (!$first) { @@ -300,9 +299,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { $contribution = new CRM_Contribute_BAO_Contribution(); $contribution->trxn_id = $input['trxn_id']; if ($contribution->trxn_id && $contribution->find()) { - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalProIPN: Returning since contribution has already been handled.'); echo "Success: Contribution has already been handled

"; - return TRUE; + return; } $contribution->contact_id = $recur->contact_id; @@ -319,19 +318,17 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // CRM-13737 - am not aware of any reason why payment_date would not be set - this if is a belt & braces $objects['contribution']->receive_date = !empty($input['payment_date']) ? date('YmdHis', strtotime($input['payment_date'])) : $now; - $this->single($input, $ids, $objects, - TRUE, $first - ); + $this->single($input, $ids, $objects, TRUE, $first); } /** - * @param $input - * @param $ids - * @param $objects + * @param array $input + * @param array $ids + * @param array $objects * @param bool $recur * @param bool $first * - * @return bool + * @return void */ public function single(&$input, &$ids, &$objects, $recur = FALSE, $first = FALSE) { $contribution = &$objects['contribution']; @@ -339,9 +336,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // make sure the invoice is valid and matches what we have in the contribution record if ((!$recur) || ($recur && $first)) { if ($contribution->invoice_id != $input['invoice']) { - CRM_Core_Error::debug_log_message("Invoice values dont match between database and IPN request"); + Civi::log()->debug('PayPalProIPN: Invoice values dont match between database and IPN request.'); echo "Failure: Invoice values dont match between database and IPN request

contribution is" . $contribution->invoice_id . " and input is " . $input['invoice']; - return FALSE; + return; } } else { @@ -350,9 +347,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { if (!$recur) { if ($contribution->total_amount != $input['amount']) { - CRM_Core_Error::debug_log_message("Amount values dont match between database and IPN request"); + Civi::log()->debug('PayPalProIPN: Amount values dont match between database and IPN request.'); echo "Failure: Amount values dont match between database and IPN request

"; - return FALSE; + return; } } else { @@ -363,24 +360,29 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { $status = $input['paymentStatus']; if ($status == 'Denied' || $status == 'Failed' || $status == 'Voided') { - return $this->failed($objects, $transaction); + $this->failed($objects, $transaction); + return; } elseif ($status == 'Pending') { - return $this->pending($objects, $transaction); + $this->pending($objects, $transaction); + return; } elseif ($status == 'Refunded' || $status == 'Reversed') { - return $this->cancelled($objects, $transaction); + $this->cancelled($objects, $transaction); + return; } elseif ($status != 'Completed') { - return $this->unhandled($objects, $transaction); + $this->unhandled($objects, $transaction); + return; } // check if contribution is already completed, if so we ignore this ipn - if ($contribution->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($contribution->contribution_status_id == $completedStatusId) { $transaction->commit(); - CRM_Core_Error::debug_log_message("returning since contribution has already been handled"); + Civi::log()->debug('PayPalProIPN: Returning since contribution has already been handled.'); echo "Success: Contribution has already been handled

"; - return TRUE; + return; } $this->completeTransaction($input, $ids, $objects, $transaction, $recur); @@ -397,6 +399,9 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { // processor id & the handleNotification function (which should call the completetransaction api & by-pass this // entirely). The only thing the IPN class should really do is extract data from the request, validate it // & call completetransaction or call fail? (which may not exist yet). + + Civi::log()->warning('Unreliable method used to get payment_processor_id for PayPal Pro IPN - this will cause problems if you have more than one instance'); + $paymentProcessorTypeID = CRM_Core_DAO::getFieldValue('CRM_Financial_DAO_PaymentProcessorType', 'PayPal', 'id', 'name' ); @@ -414,7 +419,7 @@ class CRM_Core_Payment_PayPalProIPN extends CRM_Core_Payment_BaseIPN { * (with the input parameters) & call this & all will be done * * @todo the references to POST throughout this class need to be removed - * @return bool + * @return void */ public function main() { CRM_Core_Error::debug_var('GET', $_GET, TRUE, TRUE); @@ -464,16 +469,10 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr } } - // This is an unreliable method as there could be more than one instance. - // Recommended approach is to use the civicrm/payment/ipn/xx url where xx is the payment - // processor id & the handleNotification function (which should call the completetransaction api & by-pass this - // entirely). The only thing the IPN class should really do is extract data from the request, validate it - // & call completetransaction or call fail? (which may not exist yet). - $paymentProcessorID = self::getPayPalPaymentProcessorID(); if (!$this->validateData($input, $ids, $objects, TRUE, $paymentProcessorID)) { - return FALSE; + return; } self::$_paymentProcessor = &$objects['paymentProcessor']; @@ -484,31 +483,27 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr if ($ids['contributionRecur']) { // check if first contribution is completed, else complete first contribution $first = TRUE; - if ($objects['contribution']->contribution_status_id == 1) { + $completedStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Completed'); + if ($objects['contribution']->contribution_status_id == $completedStatusId) { $first = FALSE; } - return $this->recur($input, $ids, $objects, $first); + $this->recur($input, $ids, $objects, $first); + return; } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); - } - } - else { - return $this->single($input, $ids, $objects, FALSE, FALSE); } + $this->single($input, $ids, $objects, FALSE, FALSE); } /** - * @param $input - * @param $ids + * @param array $input + * @param array $ids * - * @return bool + * @return void * @throws CRM_Core_Exception */ public function getInput(&$input, &$ids) { - if (!$this->getBillingID($ids)) { - return FALSE; + return; } $input['txnType'] = self::retrieve('txn_type', 'String', 'POST', FALSE); @@ -582,8 +577,9 @@ INNER JOIN civicrm_membership_payment mp ON m.id = mp.membership_id AND mp.contr $result = civicrm_api3('contribution', 'getsingle', ['invoice_id' => $input['invoice'], 'contribution_test' => '']); $ids['contribution'] = $result['id']; - //@todo hard - coding 'pending' for now - if ($result['contribution_status_id'] == 2) { + //@todo hardcoding 'pending' for now + $pendingStatusId = CRM_Core_Pseudoconstant::getKey('CRM_Contribute_BAO_Contribution', 'contribution_status_id', 'Pending'); + if ($result['contribution_status_id'] == $pendingStatusId) { $isFirst = TRUE; } // arg api won't get this - fix it -- 2.25.1