From 130a7173720608f9fcf28600b64309ebcac90a19 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Tue, 27 Dec 2022 17:18:16 -0400 Subject: [PATCH 01/27] Bug Fixes and add param to savedonor 1) Fix: Don't allow + symbol in params as it generates an error. 2) Fix: + symbol resolves to a numeric but is not numeric (ex. in phone numbers). Treat anything with a + as a string so the + can be removed. 3) Fix: Card expiration date resolves to a numeric field (in vault) but the data field is not numeric. 4) Fix: nomail is a required field and generates an error if set to NULL. If unspecified, set it to N. 5) Add: receipt_delivery param to savedonor --- src/DonorPerfect.php | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index bc9111b..717b758 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -124,7 +124,6 @@ protected function callInternal(array $params) if (strlen(static::$baseUrl . $relativeUrl) > 8000) { throw new Exception('The DonorPerfect API call exceeds the maximum length permitted (8000 characters)'); } - // Make the request $response = (string) $this->client->request('GET', $relativeUrl)->getBody(); @@ -208,7 +207,8 @@ public function call(string $action, array $parameters) foreach ($parameters as $param => $value) { $value = trim($value); - if (is_numeric($value) && strpos($value, 'e') === false) { + if (is_numeric($value) && strpos($value, 'e') === false && strpos($value, '+') === false && + $param != 'CardExpirationDate') { $value = $value; } elseif (is_bool($value)) { $value = $value ? '1' : '0'; @@ -218,8 +218,9 @@ public function call(string $action, array $parameters) $value = "N'" . implode($value, '|') . "'"; } else { // Ensure quotes are doubled for escaping purposes + // + in param will be interpreted as a space and will generate an error // @see https://api.warrenbti.com/2020/08/03/apostrophes-in-peoples-names/ - $value = str_replace(["'", '"', '%'], ["''", '', '%25'], $value); + $value = str_replace(["'", '"', '%', '+'], ["''", '', '%25', ''], $value); // Wrap the value in quotes $value = "'$value'"; @@ -628,6 +629,11 @@ public function dp_donorsearch($data) */ public function dp_savedonor($data) { + // The nomail parameter is a required field for dp_savedonor. Make sure it is + // set properly and default to N if missing or invalid to prevent error. + if (!isset($data['nomail']) || $data['nomail'] != 'Y') $data['nomail'] = 'N'; + // receipt delivery defaults to L if unspecified, make it explicit + if (!isset($data['receipt_delivery'])) $data['receipt_delivery'] = 'L'; return $this->call('dp_savedonor', static::prepareParams($data, [ 'donor_id' => ['numeric'], // Enter 0 (zero) to create a new donor/constituent record or an existing donor_id. Please note: If you are updating an existing donor, all existing values for the fields specified below will be overwritten by the values you send with this API call. 'first_name' => ['string', 50], // @@ -656,6 +662,7 @@ public function dp_savedonor($data) 'nomail_reason' => ['string', 30], // 'narrative' => ['string', 2147483647], // 'donor_rcpt_type' => ['string', 1], // 'I' for individual or 'C' for consolidated receipting preference + 'receipt_delivery'=> ['string', 1], // 'B' for letter and email, 'L' for letter, 'E' email, 'N' do not acknowledge 'user_id' => $this->appName, ])); } @@ -1243,7 +1250,7 @@ public function dp_PaymentMethod_Insert($data) return $this->call('dp_PaymentMethod_Insert', static::prepareParams($data, [ 'CustomerVaultID' => ['string', 55], // Enter -0 to create a new Customer Vault ID record 'donor_id' => ['numeric'], // - 'IsDefault' => ['bool'], // Enter 1 if this is will be the default EFT payment method + 'IsDefault' => ['bool'], // Enter 1 if this is will be the default EFT payment method. Note anything other than 1 (i.e. 0 or NULL fails and not sure why) 'AccountType' => ['string', 256], // e.g. ‘Visa’ 'dpPaymentMethodTypeID' => ['string', 20], // e.g.; ‘creditcard’ 'CardNumberLastFour' => ['string', 16], // e.g.; ‘4xxxxxxxxxxx1111 From dae202008870d4b375720b6245989f84943e22e6 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Wed, 4 Jan 2023 11:22:05 -0600 Subject: [PATCH 02/27] Update src/DonorPerfect.php --- src/DonorPerfect.php | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 717b758..82509c9 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -629,11 +629,15 @@ public function dp_donorsearch($data) */ public function dp_savedonor($data) { - // The nomail parameter is a required field for dp_savedonor. Make sure it is - // set properly and default to N if missing or invalid to prevent error. - if (!isset($data['nomail']) || $data['nomail'] != 'Y') $data['nomail'] = 'N'; - // receipt delivery defaults to L if unspecified, make it explicit - if (!isset($data['receipt_delivery'])) $data['receipt_delivery'] = 'L'; + // nomail is required for dp_savedonor, ensure it is present and valid + if (!isset($data['nomail']) || $data['nomail'] != 'Y') { + $data['nomail'] = 'N'; + } + // receipt_delivery defaults to L if unspecified, make it explicit + if (!isset($data['receipt_delivery'])) { + $data['receipt_delivery'] = 'L'; + } + return $this->call('dp_savedonor', static::prepareParams($data, [ 'donor_id' => ['numeric'], // Enter 0 (zero) to create a new donor/constituent record or an existing donor_id. Please note: If you are updating an existing donor, all existing values for the fields specified below will be overwritten by the values you send with this API call. 'first_name' => ['string', 50], // From c00c8d9de3e6abd5c59630a3f3f41fb507bb6c44 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Thu, 5 Jan 2023 17:31:32 -0400 Subject: [PATCH 03/27] Code cleanup and ... Add currency and acknowledge pref to savepledge --- src/DonorPerfect.php | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 717b758..1cb3ab6 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -121,6 +121,9 @@ protected function callInternal(array $params) // Validate the API call before making it $relativeUrl .= http_build_query($args, null, '&', PHP_QUERY_RFC3986); + // encode any + signs + $relativeUrl = str_replace('+', '%2B', $relativeUrl); + if (strlen(static::$baseUrl . $relativeUrl) > 8000) { throw new Exception('The DonorPerfect API call exceeds the maximum length permitted (8000 characters)'); } @@ -207,8 +210,10 @@ public function call(string $action, array $parameters) foreach ($parameters as $param => $value) { $value = trim($value); - if (is_numeric($value) && strpos($value, 'e') === false && strpos($value, '+') === false && - $param != 'CardExpirationDate') { + if (is_numeric($value) + && strpos($value, 'e') === false + && strpos($value, '+') === false + && $param != 'CardExpirationDate') { $value = $value; } elseif (is_bool($value)) { $value = $value ? '1' : '0'; @@ -218,9 +223,8 @@ public function call(string $action, array $parameters) $value = "N'" . implode($value, '|') . "'"; } else { // Ensure quotes are doubled for escaping purposes - // + in param will be interpreted as a space and will generate an error // @see https://api.warrenbti.com/2020/08/03/apostrophes-in-peoples-names/ - $value = str_replace(["'", '"', '%', '+'], ["''", '', '%25', ''], $value); + $value = str_replace(["'", '"', '%'], ["''", '', '%25'], $value); // Wrap the value in quotes $value = "'$value'"; @@ -251,8 +255,21 @@ public function call(string $action, array $parameters) */ public function callSql($sql) { + // Clean the sql of extra spaces and tabs + $cleansql = $sql; + $done = false; + do { + $cleansql_new = trim(str_ireplace(["\n", "\t", ' ', ' )'], [' ', '', ' ', ' )'], $cleansql)); + if ($cleansql_new === $cleansql) { + $done = true; + } + else { + $cleansql = $cleansql_new; + } + } while (!$done); + $params = [ - 'action' => trim(str_ireplace(["\n", "\t", ' ', ' )'], [' ', '', ' ', ' )'], $sql)), + 'action' => $cleansql_new, ]; return $this->callInternal($params); @@ -629,11 +646,15 @@ public function dp_donorsearch($data) */ public function dp_savedonor($data) { - // The nomail parameter is a required field for dp_savedonor. Make sure it is - // set properly and default to N if missing or invalid to prevent error. - if (!isset($data['nomail']) || $data['nomail'] != 'Y') $data['nomail'] = 'N'; + // nomail is required for dp_savedonor, ensure it is present and valid + if (!isset($data['nomail']) || $data['nomail'] != 'Y') { + $data['nomail'] = 'N'; + } // receipt delivery defaults to L if unspecified, make it explicit - if (!isset($data['receipt_delivery'])) $data['receipt_delivery'] = 'L'; + if (!isset($data['receipt_delivery'])) { + $data['receipt_delivery'] = 'L'; + } + return $this->call('dp_savedonor', static::prepareParams($data, [ 'donor_id' => ['numeric'], // Enter 0 (zero) to create a new donor/constituent record or an existing donor_id. Please note: If you are updating an existing donor, all existing values for the fields specified below will be overwritten by the values you send with this API call. 'first_name' => ['string', 50], // @@ -765,6 +786,8 @@ public function dp_savepledge($data) 'vault_id' => ['numeric'], // This field must be populated from the Vault ID number returned by SafeSave for the pledge to be listed as active in the user interface. 'receipt_delivery_g' => ['string', 1], // ‘E’ for email, ‘B’ for both email and letter, ‘L’ for letter, ‘N’ for do not acknowledge or NULL 'contact_id' => ['numeric'], // Or NULL + 'acknowledgepref' => ['string', 3], + 'currency' => ['string', 3] ])); } From e1326030ae28fd7467327ff7d9bf3eeef0146f33 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Thu, 5 Jan 2023 17:36:16 -0400 Subject: [PATCH 04/27] Update DonorPerfect.php --- src/DonorPerfect.php | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index e337641..1cb3ab6 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -648,19 +648,11 @@ public function dp_savedonor($data) { // nomail is required for dp_savedonor, ensure it is present and valid if (!isset($data['nomail']) || $data['nomail'] != 'Y') { -<<<<<<< HEAD $data['nomail'] = 'N'; } // receipt delivery defaults to L if unspecified, make it explicit if (!isset($data['receipt_delivery'])) { $data['receipt_delivery'] = 'L'; -======= - $data['nomail'] = 'N'; - } - // receipt_delivery defaults to L if unspecified, make it explicit - if (!isset($data['receipt_delivery'])) { - $data['receipt_delivery'] = 'L'; ->>>>>>> dae202008870d4b375720b6245989f84943e22e6 } return $this->call('dp_savedonor', static::prepareParams($data, [ From 2ccea6fdc16f8d3f3e0ac52a25ca0f9e9e317395 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Fri, 6 Jan 2023 17:38:25 -0400 Subject: [PATCH 05/27] Update src/DonorPerfect.php Co-authored-by: Luke Towers --- src/DonorPerfect.php | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 1cb3ab6..a1386cd 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -255,19 +255,24 @@ public function call(string $action, array $parameters) */ public function callSql($sql) { - // Clean the sql of extra spaces and tabs - $cleansql = $sql; - $done = false; - do { - $cleansql_new = trim(str_ireplace(["\n", "\t", ' ', ' )'], [' ', '', ' ', ' )'], $cleansql)); - if ($cleansql_new === $cleansql) { - $done = true; - } - else { - $cleansql = $cleansql_new; - } - } while (!$done); + // Remove all formatting whitespace while leaving whitespace that is part of value strings + $in_quote = false; + $output = ''; + for ($i = 0; $i < strlen($sql); $i++) { + if ($sql[$i] == "'" || $sql[$i] == '"') { + $in_quote = !$in_quote; + } + if (($sql[$i] == ' ' || $sql[$i] == "\n" || $sql[$i] == "\r") && !$in_quote) { + if (empty($output) || substr($output, -1) == ' ') { + continue; + } + $output .= ' '; + } else { + $output .= $sql[$i]; + } + } + $params = [ 'action' => $cleansql_new, ]; From 821c3e3634625a45523c14b575f177b91c1bba85 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Fri, 6 Jan 2023 17:50:35 -0400 Subject: [PATCH 06/27] set sql action to correct variable after new clean procedure --- src/DonorPerfect.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index a1386cd..9a02c4a 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -272,9 +272,9 @@ public function callSql($sql) $output .= $sql[$i]; } } - + $params = [ - 'action' => $cleansql_new, + 'action' => $output ]; return $this->callInternal($params); From 7edb37b532fcc1ce8a10a80d07e5f5b2700d1faa Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 8 Jan 2023 14:46:04 -0600 Subject: [PATCH 07/27] Update src/DonorPerfect.php --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 9a02c4a..cb925e1 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -792,7 +792,7 @@ public function dp_savepledge($data) 'receipt_delivery_g' => ['string', 1], // ‘E’ for email, ‘B’ for both email and letter, ‘L’ for letter, ‘N’ for do not acknowledge or NULL 'contact_id' => ['numeric'], // Or NULL 'acknowledgepref' => ['string', 3], - 'currency' => ['string', 3] + 'currency' => ['string', 3], ])); } From adcf78badd4af881aeca8cb30f60c80109ac1228 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 8 Jan 2023 15:46:33 -0600 Subject: [PATCH 08/27] Formatting & improvements to the trimSql() method See below for the list of statements this was tested against: ```php // Test cases $tests = [ // No quotes 'SELECT * FROM users' => [ // Unchanged 'SELECT * FROM users', // Trim start, end, and in between ' SELECT * FROM users ', // Trim start, end, and in between, and remove newlines "\t\tSELECT \n\t\t\t*\n\t\tFROM\n\t\t\tusers\n\t\t", // Trim SQL formatted for readability ' SELECT * FROM users ', ], // Double quotes 'SELECT * FROM USERS WHERE name = "BOB BUILDER"' => [ // Unchanged 'SELECT * FROM USERS WHERE name = "BOB BUILDER"', // Trim start, end, and in between ' SELECT * FROM USERS WHERE name = "BOB BUILDER" ', // Don't remove whitespace inside double quotes "SELECT * FROM USERS WHERE name = \"BOB BUILDER\"", ], // Single quotes 'SELECT * FROM USERS WHERE name = \'BOB BUILDER\'' => [ // Unchanged 'SELECT * FROM USERS WHERE name = \'BOB BUILDER\'', // Trim start, end, and in between ' SELECT * FROM USERS WHERE name = \'BOB BUILDER\' ', // Don't remove whitespace inside single quotes "SELECT * FROM USERS WHERE name = 'BOB BUILDER'", ], // Escaped quotes "SELECT * FROM USERS WHERE name = \"BOB \\\" BUILDER" => [ // Don't remove whitespace inside double quotes, even if escaped "SELECT * FROM USERS WHERE name = \"BOB \\\" BUILDER", ], ]; ``` --- src/DonorPerfect.php | 63 ++++++++++++++++++++++++++++++++------------ 1 file changed, 46 insertions(+), 17 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index cb925e1..def38b3 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -210,10 +210,12 @@ public function call(string $action, array $parameters) foreach ($parameters as $param => $value) { $value = trim($value); - if (is_numeric($value) + if ( + is_numeric($value) && strpos($value, 'e') === false && strpos($value, '+') === false - && $param != 'CardExpirationDate') { + && $param != 'CardExpirationDate' + ) { $value = $value; } elseif (is_bool($value)) { $value = $value ? '1' : '0'; @@ -246,35 +248,62 @@ public function call(string $action, array $parameters) } /** - * Make a SQL call to the DonorPerfect API. - * - * @param string $sql The raw SQL to send to the API. Any user provided values should be properly - * escaped and provided inline with the SQL. - * @throws Exception if the call fails - * @return mixed + * Trim the provided SQL statement to remove all extra whitespace from the logic while retaining + * any whitespace inside of quoted values. */ - public function callSql($sql) + protected static function trimSql(string $sql): string { - // Remove all formatting whitespace while leaving whitespace that is part of value strings - $in_quote = false; + $inQuote = null; $output = ''; + $quoteChars = ['"', "'"]; + $whitespaceChars = [' ', "\n", "\r", "\t"]; for ($i = 0; $i < strlen($sql); $i++) { - if ($sql[$i] == "'" || $sql[$i] == '"') { - $in_quote = !$in_quote; + $currentChar = $sql[$i]; + if ( + // Check if the character is a quote + in_array($currentChar, $quoteChars) + // Check if the character matches the current quote context + && (is_null($inQuote) || $currentChar === $inQuote) + // Check if the character was escaped + && ($i > 0 && $sql[$i - 1] !== "\\") + ) { + $inQuote = is_null($inQuote) ? $currentChar : null; } - if (($sql[$i] == ' ' || $sql[$i] == "\n" || $sql[$i] == "\r") && !$in_quote) { - if (empty($output) || substr($output, -1) == ' ') { + if ( + in_array($currentChar, $whitespaceChars) + && is_null($inQuote) + ) { + if ( + empty($output) + || substr($output, -1) == ' ' + ) { continue; } $output .= ' '; } else { - $output .= $sql[$i]; + $output .= $currentChar; } } + return trim($output); + } + + /** + * Make a SQL call to the DonorPerfect API. + * + * @param string $sql The raw SQL to send to the API. Any user provided values should be properly + * escaped and provided inline with the SQL. + * @throws Exception if the call fails + * @return mixed + */ + public function callSql($sql) + { + // Remove all formatting whitespace while leaving whitespace that is part of value strings + $sql = static::trimSql($sql); + $params = [ - 'action' => $output + 'action' => $sql, ]; return $this->callInternal($params); From 3920a8c1d4d9b1a803f7cdf24f2cab7eeaf92562 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Sun, 8 Jan 2023 16:04:08 -0600 Subject: [PATCH 09/27] Wrap string values in an anonymous class --- src/DonorPerfect.php | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index def38b3..c98194e 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -214,7 +214,6 @@ public function call(string $action, array $parameters) is_numeric($value) && strpos($value, 'e') === false && strpos($value, '+') === false - && $param != 'CardExpirationDate' ) { $value = $value; } elseif (is_bool($value)) { @@ -315,7 +314,7 @@ public function callSql($sql) * @param mixed $value The value to prepare * @param int $maxlen The maximum length of the string * @throws Exception if the provided value is longer than the max allowed length - * @return string + * @return object (Annonymous class that implements __toString and holds the string value) */ public static function prepareString($value, int $maxlen = null) { @@ -325,7 +324,20 @@ public static function prepareString($value, int $maxlen = null) throw new Exception("$value is longer than the max allowed length of $maxlen"); } - return $value; + // Returns as an anonymous class that implements __toString in order to ensure + // that numeric values that have been explicitly declared as strings are not + // converted to integers when processed in the call() method. + return new class($value) { + protected string $value; + public function __construct(string $value) + { + $this->value = $value; + } + public function __toString() + { + return $this->value; + } + }; } /** From a48424f30980c2b678cfd64dcc5241f6627c2656 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 9 Jan 2023 09:42:41 -0600 Subject: [PATCH 10/27] Ensure that class wrapped values aren't interpreted as strings --- src/DonorPerfect.php | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index c98194e..43d9074 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -208,10 +208,12 @@ public function call(string $action, array $parameters) $paramString = ''; $i = 0; foreach ($parameters as $param => $value) { + $original = $value; $value = trim($value); if ( - is_numeric($value) + !is_object($original) + && is_numeric($value) && strpos($value, 'e') === false && strpos($value, '+') === false ) { From 4578258f67a807983b4f2e6a7b5ed322829a3f05 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Mon, 9 Jan 2023 09:59:15 -0600 Subject: [PATCH 11/27] Only trim strings --- src/DonorPerfect.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 43d9074..3aceff7 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -208,11 +208,12 @@ public function call(string $action, array $parameters) $paramString = ''; $i = 0; foreach ($parameters as $param => $value) { - $original = $value; - $value = trim($value); + if (is_string($value)) { + $value = trim($value); + } if ( - !is_object($original) + !is_object($value) && is_numeric($value) && strpos($value, 'e') === false && strpos($value, '+') === false From 4c24c286a1c0eeb098284b24f0afbead2027dd28 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Thu, 12 Jan 2023 17:13:17 -0400 Subject: [PATCH 12/27] Remove extra encoding of the + (not needed) --- src/DonorPerfect.php | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 3aceff7..802918c 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -121,8 +121,6 @@ protected function callInternal(array $params) // Validate the API call before making it $relativeUrl .= http_build_query($args, null, '&', PHP_QUERY_RFC3986); - // encode any + signs - $relativeUrl = str_replace('+', '%2B', $relativeUrl); if (strlen(static::$baseUrl . $relativeUrl) > 8000) { throw new Exception('The DonorPerfect API call exceeds the maximum length permitted (8000 characters)'); From 78b5342b51d7a2776f56c3fe068e6a80e7a312a4 Mon Sep 17 00:00:00 2001 From: Luke Towers Date: Thu, 12 Jan 2023 16:51:54 -0600 Subject: [PATCH 13/27] Update DonorPerfect.php --- src/DonorPerfect.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 802918c..42904ae 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -224,6 +224,9 @@ public function call(string $action, array $parameters) } elseif (is_array($value)) { $value = "N'" . implode($value, '|') . "'"; } else { + // Ensure wrapped string values are still trimmed + $value = trim($value); + // Ensure quotes are doubled for escaping purposes // @see https://api.warrenbti.com/2020/08/03/apostrophes-in-peoples-names/ $value = str_replace(["'", '"', '%'], ["''", '', '%25'], $value); From 3070efc02c3aa58299c2bf48dd20a89b041f80f5 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Tue, 31 Jan 2023 16:20:47 -0400 Subject: [PATCH 14/27] add receipt type to save pladge parameters --- src/DonorPerfect.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 42904ae..c51f1a4 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -838,6 +838,7 @@ public function dp_savepledge($data) 'contact_id' => ['numeric'], // Or NULL 'acknowledgepref' => ['string', 3], 'currency' => ['string', 3], + 'rcpt_type' => ['string', 1] // C for consolidated or I for individual or NULL for unset ])); } From 49d15d00289ab8be64a199c01d4f29b405954a33 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Tue, 31 Jan 2023 16:26:51 -0400 Subject: [PATCH 15/27] Update DonorPerfect.php --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index c51f1a4..b1d8444 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -226,7 +226,7 @@ public function call(string $action, array $parameters) } else { // Ensure wrapped string values are still trimmed $value = trim($value); - + // Ensure quotes are doubled for escaping purposes // @see https://api.warrenbti.com/2020/08/03/apostrophes-in-peoples-names/ $value = str_replace(["'", '"', '%'], ["''", '', '%25'], $value); From 084ae74272e85538449ed612f8f5ed80f8b6f088 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Tue, 31 Jan 2023 16:35:05 -0400 Subject: [PATCH 16/27] Add Receipt Type to Pledge Parameters --- src/DonorPerfect.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index bf20f41..b1d8444 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -838,6 +838,7 @@ public function dp_savepledge($data) 'contact_id' => ['numeric'], // Or NULL 'acknowledgepref' => ['string', 3], 'currency' => ['string', 3], + 'rcpt_type' => ['string', 1] // C for consolidated or I for individual or NULL for unset ])); } From ae9311209d972a55a768ee0ddefb10a3b4d0b912 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Mon, 3 Apr 2023 15:37:55 -0300 Subject: [PATCH 17/27] Make http_build_query php 8 compliant --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index b1d8444..681f30c 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -120,7 +120,7 @@ protected function callInternal(array $params) $args = array_merge($args, $params); // Validate the API call before making it - $relativeUrl .= http_build_query($args, null, '&', PHP_QUERY_RFC3986); + $relativeUrl .= http_build_query($args, '', '&', PHP_QUERY_RFC3986); if (strlen(static::$baseUrl . $relativeUrl) > 8000) { throw new Exception('The DonorPerfect API call exceeds the maximum length permitted (8000 characters)'); From 28e799eea688b315a27cacfd7bfe5892277ccfd7 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Mon, 25 Sep 2023 10:02:25 -0300 Subject: [PATCH 18/27] Add RCPT_TYPE to savegift --- src/DonorPerfect.php | 1 + 1 file changed, 1 insertion(+) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 681f30c..2a07dd1 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -793,6 +793,7 @@ public function dp_savegift($data) 'currency' => ['string', 3], // If you use the multi-currency feature, enter appropriate code value per your currency field – e.g; 'USD', 'CAD', etc. 'receipt_delivery_g' => ['string', 1], // This field sets receipt delivery preference for the specified gift. Supply one of the following single letter code values: • N = do not acknowledge • E = email • B = email and letter • L = letter 'acknowledgepref' => ['string', 3], // Used in Canadian DonorPerfect systems to indicate official receipt acknowledgement preference code: • 1AR – Acknowledge/Receipt • 2AD – Acknowledge / Do Not Receipt • 3DD – Do Not Acknowledge / Do Not Receipt + 'rcpt_type' => ['string', 1], // C for consolidated or I for individual or NULL for unset ])); } From b94ddc45f04cef4603e90ddfe4c1746caced4bf1 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Tue, 10 Oct 2023 14:16:15 -0300 Subject: [PATCH 19/27] fix bug in saveaddress ukcounty typo --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 2a07dd1..e1baa3d 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -925,7 +925,7 @@ public function dp_saveaddress($data) 'mobile_phone' => ['string', 40], // 'address3' => ['string', 100], // 'address4' => ['string', 100], // - 'ukcountry' => ['string', 100], // + 'ukcounty' => ['string', 100], // 'org_rec' => ['string', 1], // Enter 'Y' to check the Org Rec field (indicating an organizational record) or 'N' to leave it unchecked to indicate an individual record. ])); } From 0f43c5de923de923b9de62bfda1ba0dcbd05be89 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Mon, 16 Oct 2023 13:03:58 -0300 Subject: [PATCH 20/27] add missing required params on dp_savecode --- src/DonorPerfect.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index e1baa3d..7ddae4c 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -967,12 +967,17 @@ public function dp_savecode($data) return $this->call('dp_savecode', static::prepareParams($data, [ 'field_name' => ['string', 20], // Enter the name of an existing field type from the DPCODES table 'code' => ['string', 30], // Enter the new CODE value - 'description' => ['string', 100], // Enter the description value that will appear in drop-down selection values + 'description' => ['string', 100], // Enter the description value that will appear in drop-down selection values 'original_code' => ['string', 20], // Enter NULL unless you are updating an existing code. In that case, set this field to the current (before update) value of the CODE 'code_date' => ['date'], // Enter NULL 'mcat_hi' => ['money'], // Enter NULL 'mcat_lo' => ['money'], // Enter NULL 'mcat_gl' => ['string', 1], // Enter NULL + 'reciprocal' => null, + 'mailed' => null, + 'printing' => null, + 'other' => null, + 'goal' => null, 'acct_num' => ['string', 30], // Enter NULL 'campaign' => ['string', 30], // Enter NULL 'solicit_code' => ['string', 30], // Enter NULL From 399a1f9a52bb1abe168de87b32c63d610aa73745 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Thu, 19 Oct 2023 16:02:11 -0300 Subject: [PATCH 21/27] Allow many fields on mergemultivalues function --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 7ddae4c..88a62bb 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -1069,7 +1069,7 @@ public function mergemultivalues($data) return $this->call('mergemultivalues', static::prepareParams($data, [ 'matchingid' => ['numeric'], // Specify the desired donor_id 'fieldname' => ['string', 20], // Enter the name of the checkbox field name. - 'valuestring' => ['string', 20], // Enter any CODE values to be set. Separate with commas. Any code values not specified will be unset (unchecked). + 'valuestring' => ['string', 7000], // Enter any CODE values to be set. Separate with commas (max 20 chars per code). Any code values not specified will be unset (unchecked). 'debug' => ['numeric'], // Specification of this field is optional but if you want to return the list of checkbox fields and the values in them after running this command then add debug=1 as a parameter to this API call. If a code was previously set but was not specified in your mergemultivalues API call then it will show as a DeletedCode value. If a value was not previously set but was specified in your API call, then it will show as an InsertedCode. ])); } From 8f2247a5608576834dab18328ff7fe25d6652bf1 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Tue, 31 Oct 2023 12:45:11 -0300 Subject: [PATCH 22/27] Mask API Key and Passwords from error generation --- src/DonorPerfect.php | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 88a62bb..6c66dfa 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -136,6 +136,8 @@ protected function callInternal(array $params) // Handle error messages if (array_key_exists('error', $response)) { + // conceal any credentials in the error to prevent them from being displayed in output + $response['error'] = str_replace([$this->apiKey,$this->pass],['**APIKEY**','**PASSWORD**'],$response['error']); throw new Exception($response['error']); } elseif (isset($response['field']['@attributes']['value']) && $response['field']['@attributes']['value'] === 'false') { throw new Exception($response['field']['@attributes']['reason']); From 08d85d659b85fd05b413a8a5a7a2f05e2f35490a Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Fri, 3 Nov 2023 13:04:55 -0300 Subject: [PATCH 23/27] conceal credentials in errors --- src/DonorPerfect.php | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 6c66dfa..8f982ee 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -140,7 +140,10 @@ protected function callInternal(array $params) $response['error'] = str_replace([$this->apiKey,$this->pass],['**APIKEY**','**PASSWORD**'],$response['error']); throw new Exception($response['error']); } elseif (isset($response['field']['@attributes']['value']) && $response['field']['@attributes']['value'] === 'false') { - throw new Exception($response['field']['@attributes']['reason']); + // conceal any credentials in the error to prevent them from being displayed in output + $error = $response['field']['@attributes']['reason']; + $error = str_replace([$this->apiKey,$this->pass],['**APIKEY**','**PASSWORD**'],$error); + throw new Exception($error); } // Handle empty responses From 9a875acbc64e3efcd2fe12397eaafeb647e2b39a Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Mon, 1 Jan 2024 14:21:50 -0400 Subject: [PATCH 24/27] Replace apikey and password on error --- src/DonorPerfect.php | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 8f982ee..0d2c859 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -133,16 +133,24 @@ protected function callInternal(array $params) // Turn the response into a usable PHP array $response = json_decode(json_encode(simplexml_load_string($response)), true); - + // Handle error messages + $pattern = [ + '/(apikey=)([^&]*)/', + '/(pass=)([^&]*)/' + ]; + $replacement = [ + '${1}**APIKEY**', + '${1}**PASSWORD**' + ]; if (array_key_exists('error', $response)) { // conceal any credentials in the error to prevent them from being displayed in output - $response['error'] = str_replace([$this->apiKey,$this->pass],['**APIKEY**','**PASSWORD**'],$response['error']); + $response['error'] = preg_replace($pattern, $replacement, $response['error']); throw new Exception($response['error']); } elseif (isset($response['field']['@attributes']['value']) && $response['field']['@attributes']['value'] === 'false') { // conceal any credentials in the error to prevent them from being displayed in output $error = $response['field']['@attributes']['reason']; - $error = str_replace([$this->apiKey,$this->pass],['**APIKEY**','**PASSWORD**'],$error); + $error = preg_replace($pattern, $replacement, $error); throw new Exception($error); } From 18282d71be8d061e38f2b9d7f94b5fbafd22a65e Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Mon, 1 Jan 2024 17:59:17 -0400 Subject: [PATCH 25/27] hide credentials on exceptions --- src/DonorPerfect.php | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 0d2c859..0710cf2 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -125,8 +125,25 @@ protected function callInternal(array $params) if (strlen(static::$baseUrl . $relativeUrl) > 8000) { throw new Exception('The DonorPerfect API call exceeds the maximum length permitted (8000 characters)'); } + // Create filters to remove any credentials from the response + $pattern = [ + '/(apikey=)([^&]*)/', + '/(pass=)([^&]*)/' + ]; + $replacement = [ + '${1}**APIKEY**', + '${1}**PASSWORD**' + ]; // Make the request - $response = (string) $this->client->request('GET', $relativeUrl)->getBody(); + try{ + $response = (string) $this->client->request('GET', $relativeUrl)->getBody(); + } + catch(Exception $e){ + // Conceal any credentials in the error to prevent them from being displayed in output + $error = $e->getMessage(); + $error = preg_replace($pattern, $replacement, $error); + throw new Exception($error); + } // Fix values with invalid unescaped XML values $response = preg_replace('|(?Umsi)(value=\'DATE:.*\\R*\')|', 'value=\'\'', $response); @@ -135,14 +152,6 @@ protected function callInternal(array $params) $response = json_decode(json_encode(simplexml_load_string($response)), true); // Handle error messages - $pattern = [ - '/(apikey=)([^&]*)/', - '/(pass=)([^&]*)/' - ]; - $replacement = [ - '${1}**APIKEY**', - '${1}**PASSWORD**' - ]; if (array_key_exists('error', $response)) { // conceal any credentials in the error to prevent them from being displayed in output $response['error'] = preg_replace($pattern, $replacement, $response['error']); From 232d8cda0f70958d9ffeaf5249fdab44fdc99d11 Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Thu, 8 Feb 2024 13:41:03 -0400 Subject: [PATCH 26/27] prevent blanks - force null --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 0710cf2..8d6f966 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -470,7 +470,7 @@ public static function prepareParams($data, $params) } // Handle a param not being included in the data - if (!isset($data[$param])) { + if (empty($data[$param])) { $return[$param] = null; continue; } From 50c0fd4e13ee21f3f3f335fb2eb8d442e713f6dd Mon Sep 17 00:00:00 2001 From: Ron Abarbanel Date: Sun, 11 Feb 2024 11:18:57 -0400 Subject: [PATCH 27/27] fix null from '' --- src/DonorPerfect.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DonorPerfect.php b/src/DonorPerfect.php index 8d6f966..ea59e60 100644 --- a/src/DonorPerfect.php +++ b/src/DonorPerfect.php @@ -470,7 +470,7 @@ public static function prepareParams($data, $params) } // Handle a param not being included in the data - if (empty($data[$param])) { + if (!isset($data[$param]) || $data[$param] === '') { $return[$param] = null; continue; }