Merge pull request #21564 from lcdservices/dev-core-2850
authorSeamus Lee <seamuslee001@gmail.com>
Wed, 22 Sep 2021 23:27:58 +0000 (09:27 +1000)
committerGitHub <noreply@github.com>
Wed, 22 Sep 2021 23:27:58 +0000 (09:27 +1000)
dev/core#2850 update_email_resetdate consider opt out on hold

31 files changed:
CRM/Contact/BAO/GroupContactCache.php
CRM/Contact/Form/Task/PDFTrait.php
CRM/Core/BAO/CustomField.php
CRM/Core/BAO/MessageTemplate.php
CRM/Core/BAO/UFMatch.php
CRM/Core/DAO.php
CRM/Core/DomainTokens.php
CRM/Core/Form.php
CRM/Logging/Schema.php
CRM/Utils/Date.php
Civi/Api4/Query/SqlEquation.php [new file with mode: 0644]
Civi/Api4/Query/SqlExpression.php
Civi/Api4/Query/SqlFunction.php
Civi/Api4/Query/SqlFunctionIF.php
Civi/Token/TokenProcessor.php
Civi/Token/TokenRow.php
ext/afform/core/Civi/Afform/Event/AfformSubmitEvent.php
release-notes.md
release-notes/5.42.0.md [new file with mode: 0644]
tests/phpunit/CRM/Core/BAO/UFMatchTest.php [new file with mode: 0644]
tests/phpunit/CRM/Core/TokenSmartyTest.php
tests/phpunit/CRM/Logging/LoggingTest.php
tests/phpunit/CRM/Utils/DateTest.php
tests/phpunit/CRM/Utils/TokenConsistencyTest.php
tests/phpunit/Civi/Token/TokenProcessorTest.php
tests/phpunit/CiviTest/CiviUnitTestCase.php
tests/phpunit/api/v3/ContactTest.php
tests/phpunit/api/v3/ParticipantPaymentTest.php
tests/phpunit/api/v3/ParticipantTest.php
tests/phpunit/api/v4/Action/ContactApiKeyTest.php
tests/phpunit/api/v4/Action/SqlExpressionTest.php

index 32dc446c4cefa67bd608dbae1c6c1d20685d4231..43088ff058a4e0ad7bbbeca0949c2b72721570e2 100644 (file)
@@ -246,19 +246,32 @@ WHERE  id IN ( $groupIDs )
       // Someone else is kindly doing the refresh for us right now.
       return;
     }
+
+    // Get the list of expired smart groups that may need flushing
     $params = [1 => [self::getCacheInvalidDateTime(), 'String']];
-    $groupsDAO = CRM_Core_DAO::executeQuery("SELECT id FROM civicrm_group WHERE cache_date <= %1", $params);
+    $groupsThatMayNeedToBeFlushedSQL = "SELECT id FROM civicrm_group WHERE (saved_search_id IS NOT NULL OR children <> '') AND (cache_date <= %1 OR cache_date IS NULL)";
+    $groupsDAO = CRM_Core_DAO::executeQuery($groupsThatMayNeedToBeFlushedSQL, $params);
     $expiredGroups = [];
     while ($groupsDAO->fetch()) {
       $expiredGroups[] = $groupsDAO->id;
     }
-    if (!empty($expiredGroups)) {
-      $expiredGroups = implode(',', $expiredGroups);
-      CRM_Core_DAO::executeQuery("DELETE FROM civicrm_group_contact_cache WHERE group_id IN ({$expiredGroups})");
+    if (empty($expiredGroups)) {
+      // There are no expired smart groups to flush
+      return;
+    }
+
+    $expiredGroupsCSV = implode(',', $expiredGroups);
+    $flushSQLParams = [1 => [$expiredGroupsCSV, 'CommaSeparatedIntegers']];
+    // Now check if we actually have any entries in the smart groups to flush
+    $groupsHaveEntriesToFlushSQL = 'SELECT group_id FROM civicrm_group_contact_cache gc WHERE group_id IN (%1) LIMIT 1';
+    $groupsHaveEntriesToFlush = (bool) CRM_Core_DAO::singleValueQuery($groupsHaveEntriesToFlushSQL, $flushSQLParams);
+
+    if ($groupsHaveEntriesToFlush) {
+      CRM_Core_DAO::executeQuery("DELETE FROM civicrm_group_contact_cache WHERE group_id IN (%1)", [1 => [$expiredGroupsCSV, 'CommaSeparatedIntegers']]);
 
       // Clear these out without resetting them because we are not building caches here, only clearing them,
       // so the state is 'as if they had never been built'.
-      CRM_Core_DAO::executeQuery("UPDATE civicrm_group SET cache_date = NULL WHERE id IN ({$expiredGroups})");
+      CRM_Core_DAO::executeQuery("UPDATE civicrm_group SET cache_date = NULL WHERE id IN (%1)", [1 => [$expiredGroupsCSV, 'CommaSeparatedIntegers']]);
     }
     $lock->release();
   }
index fac73338907c77e1e0b38c3faf8a4f61eaf98d69..890246d84097c5b09cb16e555d07ccd7694a26eb 100644 (file)
@@ -449,7 +449,8 @@ trait CRM_Contact_Form_Task_PDFTrait {
       CRM_Core_DAO::executeQuery($query);
 
       $documentInfo = CRM_Core_BAO_File::getEntityFile('civicrm_msg_template', $formValues['template']);
-      foreach ((array) $documentInfo as $info) {
+      if ($documentInfo) {
+        $info = reset($documentInfo);
         [$html_message, $formValues['document_type']] = CRM_Utils_PDF_Document::docReader($info['fullPath'], $info['mime_type']);
         $formValues['document_file_path'] = $info['fullPath'];
       }
index 73944a1e7280881e155d8ace80112d268c39a14a..c5421a604ba5cd70ddb33b36d5c2a1662d76b920 100644 (file)
@@ -134,9 +134,8 @@ class CRM_Core_BAO_CustomField extends CRM_Core_DAO_CustomField {
    * @param array $records
    * @return CRM_Core_DAO_CustomField[]
    * @throws CRM_Core_Exception
-   * @throws CiviCRM_API3_Exception
    */
-  public static function writeRecords(array $records) {
+  public static function writeRecords(array $records): array {
     $addedColumns = $sql = $customFields = $pre = $post = [];
     foreach ($records as $index => $params) {
       CRM_Utils_Hook::pre(empty($params['id']) ? 'create' : 'edit', 'CustomField', $params['id'] ?? NULL, $params);
index 9c485e09480c17bfff7745fab1bf7bc4542e373b..802a3f822fdce8db4fd66a4a902b15117d4cdc35 100644 (file)
@@ -344,10 +344,6 @@ class CRM_Core_BAO_MessageTemplate extends CRM_Core_DAO_MessageTemplate {
     $params = array_merge($modelDefaults, $viewDefaults, $envelopeDefaults, $params);
 
     CRM_Utils_Hook::alterMailParams($params, 'messageTemplate');
-    if (!is_int($params['messageTemplateID']) && !is_null($params['messageTemplateID'])) {
-      CRM_Core_Error::deprecatedWarning('message template id should be an integer');
-      $params['messageTemplateID'] = (int) $params['messageTemplateID'];
-    }
     $mailContent = self::loadTemplate((string) $params['valueName'], $params['isTest'], $params['messageTemplateID'] ?? NULL, $params['groupName'] ?? '', $params['messageTemplate'], $params['subject'] ?? NULL);
 
     $params['tokenContext'] = array_merge([
index 9d0a58bbac35ac79ab05c4c95ee07f6a5bf721de..cc007958a1997564d0f6b534589e2f60a520113b 100644 (file)
@@ -624,13 +624,12 @@ AND    domain_id    = %4
       return [];
     }
 
-    static $ufValues;
-    if ($ufID && !isset($ufValues[$ufID])) {
+    if (!isset(Civi::$statics[__CLASS__][__FUNCTION__][$ufID])) {
       $ufmatch = new CRM_Core_DAO_UFMatch();
       $ufmatch->uf_id = $ufID;
       $ufmatch->domain_id = CRM_Core_Config::domainID();
       if ($ufmatch->find(TRUE)) {
-        $ufValues[$ufID] = [
+        Civi::$statics[__CLASS__][__FUNCTION__][$ufID] = [
           'uf_id' => $ufmatch->uf_id,
           'uf_name' => $ufmatch->uf_name,
           'contact_id' => $ufmatch->contact_id,
@@ -638,7 +637,7 @@ AND    domain_id    = %4
         ];
       }
     }
-    return $ufValues[$ufID];
+    return Civi::$statics[__CLASS__][__FUNCTION__][$ufID] ?? NULL;
   }
 
   /**
index 774b3548b60e7997fe85c759ebf4a46b728c03bd..2a375fbca14d621081e35ac2f5f895258d39d1c7 100644 (file)
@@ -932,7 +932,7 @@ class CRM_Core_DAO extends DB_DataObject {
    * @return static[]
    * @throws CRM_Core_Exception
    */
-  public static function writeRecords(array $records) {
+  public static function writeRecords(array $records): array {
     $results = [];
     foreach ($records as $record) {
       $results[] = static::writeRecord($record);
index 8aaafaff4ca5dc6b9fbbac597f57e19647ec08bb..fe7384270bef950898ba84e09d58867a5b837cc9 100644 (file)
@@ -47,6 +47,7 @@ class CRM_Core_DomainTokens extends AbstractTokenSubscriber {
       'email' => ts('Domain (organization) email'),
       'id' => ts('Domain ID'),
       'description' => ts('Domain Description'),
+      'now' => ts('Current time/date'),
     ];
   }
 
@@ -55,6 +56,11 @@ class CRM_Core_DomainTokens extends AbstractTokenSubscriber {
    * @throws \CRM_Core_Exception
    */
   public function evaluateToken(TokenRow $row, $entity, $field, $prefetch = NULL): void {
+    if ($field === 'now') {
+      $nowObj = (new \DateTime())->setTimestamp(\CRM_Utils_Time::time());
+      $row->format('text/html')->tokens($entity, $field, $nowObj);
+      return;
+    }
     $row->format('text/html')->tokens($entity, $field, self::getDomainTokenValues()[$field]);
     $row->format('text/plain')->tokens($entity, $field, self::getDomainTokenValues(NULL, FALSE)[$field]);
   }
index 69fc4ccb62bee3b83637c3651d8a1e1d771b8578..e139e04262dfafda8b067e5c4ae6608b959edf39 100644 (file)
@@ -70,6 +70,16 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
    */
   public $_action;
 
+  /**
+   * Monetary fields that may be submitted.
+   *
+   * Any fields in this list will be converted to non-localised format
+   * if retrieved by `getSubmittedValue`
+   *
+   * @var array
+   */
+  protected $submittableMoneyFields = [];
+
   /**
    * Available payment processors.
    *
@@ -2746,7 +2756,11 @@ class CRM_Core_Form extends HTML_QuickForm_Page {
     if (empty($this->exportedValues)) {
       $this->exportedValues = $this->controller->exportValues($this->_name);
     }
-    return $this->exportedValues[$fieldName] ?? NULL;
+    $value = $this->exportedValues[$fieldName] ?? NULL;
+    if (in_array($fieldName, $this->submittableMoneyFields, TRUE)) {
+      return CRM_Utils_Rule::cleanMoney($value);
+    }
+    return $value;
   }
 
   /**
index ad09e3a32c20431bbc7266fee8c543f441424377..9d09e62d20fd7c76b0ba65791a4e5163d4276002 100644 (file)
@@ -429,16 +429,18 @@ AND    (TABLE_NAME LIKE 'log_civicrm_%' $nonStandardTableNameString )
    *   name of the relevant table.
    * @param array $cols
    *   Mixed array of columns to add or null (to check for the missing columns).
-   *
-   * @return bool
    */
-  public function fixSchemaDifferencesFor($table, $cols = []) {
-    if (empty($table)) {
-      return FALSE;
+  public function fixSchemaDifferencesFor(string $table, array $cols = []): void {
+    if (!in_array($table, $this->tables, TRUE)) {
+      // Create the table if the log table does not exist and
+      // the table is in 'this->tables'. This latter array
+      // could have been altered by a hook if the site does not
+      // want to log a specific table.
+      return;
     }
     if (empty($this->logs[$table])) {
       $this->createLogTableFor($table);
-      return TRUE;
+      return;
     }
 
     if (empty($cols)) {
@@ -480,8 +482,6 @@ AND    (TABLE_NAME LIKE 'log_civicrm_%' $nonStandardTableNameString )
     }
 
     $this->resetSchemaCacheForTable("log_$table");
-
-    return TRUE;
   }
 
   /**
index 632eb4910bbafd25080a7d9159f8f849615df7e7..d2a7bab78daaa66d81e460af5e209bfb1a71d886 100644 (file)
@@ -160,8 +160,10 @@ class CRM_Utils_Date {
    *
    */
   public static function getAbbrWeekdayNames() {
-    static $days = [];
+    $key = 'abbrDays_' . \CRM_Core_I18n::getLocale();
+    $days = &\Civi::$statics[__CLASS__][$key];
     if (!$days) {
+      $days = [];
       // First day of the week
       $firstDay = Civi::settings()->get('weekBegins');
 
@@ -189,8 +191,10 @@ class CRM_Utils_Date {
    *
    */
   public static function getFullWeekdayNames() {
-    static $days = [];
+    $key = 'fullDays_' . \CRM_Core_I18n::getLocale();
+    $days = &\Civi::$statics[__CLASS__][$key];
     if (!$days) {
+      $days = [];
       // First day of the week
       $firstDay = Civi::settings()->get('weekBegins');
 
@@ -214,7 +218,8 @@ class CRM_Utils_Date {
    *
    */
   public static function &getAbbrMonthNames($month = FALSE) {
-    static $abbrMonthNames;
+    $key = 'abbrMonthNames_' . \CRM_Core_I18n::getLocale();
+    $abbrMonthNames = &\Civi::$statics[__CLASS__][$key];
     if (!isset($abbrMonthNames)) {
 
       // set LC_TIME and build the arrays from locale-provided names
@@ -237,11 +242,12 @@ class CRM_Utils_Date {
    *
    */
   public static function &getFullMonthNames() {
-    if (empty(\Civi::$statics[__CLASS__]['fullMonthNames'])) {
+    $key = 'fullMonthNames_' . \CRM_Core_I18n::getLocale();
+    if (empty(\Civi::$statics[__CLASS__][$key])) {
       // Not relying on strftime because it depends on the operating system
       // and most people will not have a non-US locale configured out of the box
       // Ignoring other date names for now, since less visible by default
-      \Civi::$statics[__CLASS__]['fullMonthNames'] = [
+      \Civi::$statics[__CLASS__][$key] = [
         1 => ts('January'),
         2 => ts('February'),
         3 => ts('March'),
@@ -257,7 +263,7 @@ class CRM_Utils_Date {
       ];
     }
 
-    return \Civi::$statics[__CLASS__]['fullMonthNames'];
+    return \Civi::$statics[__CLASS__][$key];
   }
 
   /**
diff --git a/Civi/Api4/Query/SqlEquation.php b/Civi/Api4/Query/SqlEquation.php
new file mode 100644 (file)
index 0000000..7585ab0
--- /dev/null
@@ -0,0 +1,109 @@
+<?php
+/*
+ +--------------------------------------------------------------------+
+ | Copyright CiviCRM LLC. All rights reserved.                        |
+ |                                                                    |
+ | This work is published under the GNU AGPLv3 license with some      |
+ | permitted exceptions and without any warranty. For full license    |
+ | and copyright information, see https://civicrm.org/licensing       |
+ +--------------------------------------------------------------------+
+ */
+
+namespace Civi\Api4\Query;
+
+/**
+ * Numeric sql expression
+ */
+class SqlEquation extends SqlExpression {
+
+  /**
+   * @var array
+   */
+  protected $args = [];
+
+  /**
+   * @var string[]
+   */
+  public static $arithmeticOperators = [
+    '+',
+    '-',
+    '*',
+    '/',
+  ];
+
+  /**
+   * @var string[]
+   */
+  public static $comparisonOperators = [
+    '<=',
+    '>=',
+    '<',
+    '>',
+    '=',
+    '!=',
+    '<=>',
+    'IS NOT',
+    'IS',
+    'BETWEEN',
+    'AND',
+  ];
+
+  protected function initialize() {
+    $arg = trim(substr($this->expr, strpos($this->expr, '(') + 1, -1));
+    $permitted = ['SqlField', 'SqlString', 'SqlNumber', 'SqlNull'];
+    $operators = array_merge(self::$arithmeticOperators, self::$comparisonOperators);
+    while (strlen($arg)) {
+      $this->args = array_merge($this->args, $this->captureExpressions($arg, $permitted, FALSE));
+      $op = $this->captureKeyword($operators, $arg);
+      if ($op) {
+        $this->args[] = $op;
+      }
+    }
+  }
+
+  /**
+   * Render the expression for insertion into the sql query
+   *
+   * @param array $fieldList
+   * @return string
+   */
+  public function render(array $fieldList): string {
+    $output = [];
+    foreach ($this->args as $arg) {
+      $output[] = is_string($arg) ? $arg : $arg->render($fieldList);
+    }
+    return '(' . implode(' ', $output) . ')';
+  }
+
+  /**
+   * Returns the alias to use for SELECT AS.
+   *
+   * @return string
+   */
+  public function getAlias(): string {
+    return $this->alias ?? \CRM_Utils_String::munge(trim($this->expr, ' ()'), '_', 256);
+  }
+
+  /**
+   * Change $dataType according to operator used in equation
+   *
+   * @see \Civi\Api4\Utils\FormattingUtil::formatOutputValues
+   * @param string $value
+   * @param string $dataType
+   * @return string
+   */
+  public function formatOutputValue($value, &$dataType) {
+    foreach (self::$comparisonOperators as $op) {
+      if (strpos($this->expr, " $op ")) {
+        $dataType = 'Boolean';
+      }
+    }
+    foreach (self::$arithmeticOperators as $op) {
+      if (strpos($this->expr, " $op ")) {
+        $dataType = 'Float';
+      }
+    }
+    return $value;
+  }
+
+}
index 338ae5b0575e2ad12530599d38b910b27637711d..c05228131582700c390d23be47931fafe9ddcbdc 100644 (file)
@@ -83,8 +83,12 @@ abstract class SqlExpression {
     $bracketPos = strpos($expr, '(');
     $firstChar = substr($expr, 0, 1);
     $lastChar = substr($expr, -1);
+    // Statement surrounded by brackets is an equation
+    if ($firstChar === '(' && $lastChar === ')') {
+      $className = 'SqlEquation';
+    }
     // If there are brackets but not the first character, we have a function
-    if ($bracketPos && $lastChar === ')') {
+    elseif ($bracketPos && $lastChar === ')') {
       $fnName = substr($expr, 0, $bracketPos);
       if ($fnName !== strtoupper($fnName)) {
         throw new \API_Exception('Sql function must be uppercase.');
@@ -174,4 +178,97 @@ abstract class SqlExpression {
     return static::$dataType;
   }
 
+  /**
+   * Shift a keyword off the beginning of the argument string and return it.
+   *
+   * @param array $keywords
+   *   Whitelist of keywords
+   * @param string $arg
+   * @return mixed|null
+   */
+  protected function captureKeyword($keywords, &$arg) {
+    foreach ($keywords as $key) {
+      if (strpos($arg, $key . ' ') === 0) {
+        $arg = ltrim(substr($arg, strlen($key)));
+        return $key;
+      }
+    }
+    return NULL;
+  }
+
+  /**
+   * Shifts 0 or more expressions off the argument string and returns them
+   *
+   * @param string $arg
+   * @param array $mustBe
+   * @param bool $multi
+   * @return SqlExpression[]
+   * @throws \API_Exception
+   */
+  protected function captureExpressions(string &$arg, array $mustBe, bool $multi) {
+    $captured = [];
+    $arg = ltrim($arg);
+    while ($arg) {
+      $item = $this->captureExpression($arg);
+      $arg = ltrim(substr($arg, strlen($item)));
+      $expr = self::convert($item, FALSE, $mustBe);
+      $this->fields = array_merge($this->fields, $expr->getFields());
+      $captured[] = $expr;
+      // Keep going if we have a comma indicating another expression follows
+      if ($multi && substr($arg, 0, 1) === ',') {
+        $arg = ltrim(substr($arg, 1));
+      }
+      else {
+        break;
+      }
+    }
+    return $captured;
+  }
+
+  /**
+   * Scans the beginning of a string for an expression; stops when it hits delimiter
+   *
+   * @param $arg
+   * @return string
+   */
+  protected function captureExpression($arg) {
+    $isEscaped = $quote = NULL;
+    $item = '';
+    $quotes = ['"', "'"];
+    $brackets = [
+      ')' => '(',
+    ];
+    $enclosures = array_fill_keys($brackets, 0);
+    foreach (str_split($arg) as $char) {
+      if (!$isEscaped && in_array($char, $quotes, TRUE)) {
+        // Open quotes - we'll ignore everything inside
+        if (!$quote) {
+          $quote = $char;
+        }
+        // Close quotes
+        elseif ($char === $quote) {
+          $quote = NULL;
+        }
+      }
+      if (!$quote) {
+        // Delineates end of expression
+        if (($char == ',' || $char == ' ') && !array_filter($enclosures)) {
+          return $item;
+        }
+        // Open brackets - we'll ignore delineators inside
+        if (isset($enclosures[$char])) {
+          $enclosures[$char]++;
+        }
+        // Close brackets
+        if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
+          $enclosures[$brackets[$char]]--;
+        }
+      }
+      $item .= $char;
+      // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
+      $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2);
+    }
+    return $item;
+  }
+
 }
index 94bf31b85dba6029d1def51c98af290c6369f223..fd10f80ccabaff338cf374f723ed79d1eafe458f 100644 (file)
@@ -67,7 +67,7 @@ abstract class SqlFunction extends SqlExpression {
         'suffix' => [],
       ];
       if ($param['max_expr'] && (!$param['name'] || $param['name'] === $prefix)) {
-        $exprs = $this->captureExpressions($arg, $param['must_be']);
+        $exprs = $this->captureExpressions($arg, $param['must_be'], TRUE);
         if (count($exprs) < $param['min_expr'] || count($exprs) > $param['max_expr']) {
           throw new \API_Exception('Incorrect number of arguments for SQL function ' . static::getName());
         }
@@ -93,98 +93,6 @@ abstract class SqlFunction extends SqlExpression {
     return $value;
   }
 
-  /**
-   * Shift a keyword off the beginning of the argument string and return it.
-   *
-   * @param array $keywords
-   *   Whitelist of keywords
-   * @param string $arg
-   * @return mixed|null
-   */
-  private function captureKeyword($keywords, &$arg) {
-    foreach ($keywords as $key) {
-      if (strpos($arg, $key . ' ') === 0) {
-        $arg = ltrim(substr($arg, strlen($key)));
-        return $key;
-      }
-    }
-    return NULL;
-  }
-
-  /**
-   * Shifts 0 or more expressions off the argument string and returns them
-   *
-   * @param string $arg
-   * @param array $mustBe
-   * @return array
-   * @throws \API_Exception
-   */
-  private function captureExpressions(&$arg, $mustBe) {
-    $captured = [];
-    $arg = ltrim($arg);
-    while ($arg) {
-      $item = $this->captureExpression($arg);
-      $arg = ltrim(substr($arg, strlen($item)));
-      $expr = SqlExpression::convert($item, FALSE, $mustBe);
-      $this->fields = array_merge($this->fields, $expr->getFields());
-      $captured[] = $expr;
-      // Keep going if we have a comma indicating another expression follows
-      if (substr($arg, 0, 1) === ',') {
-        $arg = ltrim(substr($arg, 1));
-      }
-      else {
-        break;
-      }
-    }
-    return $captured;
-  }
-
-  /**
-   * Scans the beginning of a string for an expression; stops when it hits delimiter
-   *
-   * @param $arg
-   * @return string
-   */
-  private function captureExpression($arg) {
-    $isEscaped = $quote = NULL;
-    $item = '';
-    $quotes = ['"', "'"];
-    $brackets = [
-      ')' => '(',
-    ];
-    $enclosures = array_fill_keys($brackets, 0);
-    foreach (str_split($arg) as $char) {
-      if (!$isEscaped && in_array($char, $quotes, TRUE)) {
-        // Open quotes - we'll ignore everything inside
-        if (!$quote) {
-          $quote = $char;
-        }
-        // Close quotes
-        elseif ($char === $quote) {
-          $quote = NULL;
-        }
-      }
-      if (!$quote) {
-        // Delineates end of expression
-        if (($char == ',' || $char == ' ') && !array_filter($enclosures)) {
-          return $item;
-        }
-        // Open brackets - we'll ignore delineators inside
-        if (isset($enclosures[$char])) {
-          $enclosures[$char]++;
-        }
-        // Close brackets
-        if (isset($brackets[$char]) && $enclosures[$brackets[$char]]) {
-          $enclosures[$brackets[$char]]--;
-        }
-      }
-      $item .= $char;
-      // We are escaping the next char if this is a backslash not preceded by an odd number of backslashes
-      $isEscaped = $char === '\\' && ((strlen($item) - strlen(rtrim($item, '\\'))) % 2);
-    }
-    return $item;
-  }
-
   /**
    * Render the expression for insertion into the sql query
    *
index 24e2faa65a8b7d1be7e637dd90325b96fe4433a4..e9bda2705a0aad9d39ac75c0cf7f6e62545d6735 100644 (file)
@@ -26,6 +26,7 @@ class SqlFunctionIF extends SqlFunction {
         'min_expr' => 3,
         'max_expr' => 3,
         'optional' => FALSE,
+        'must_be' => ['SqlEquation', 'SqlField', 'SqlFunction', 'SqlString', 'SqlNumber', 'SqlNull'],
         'ui_defaults' => [
           ['type' => 'SqlField', 'placeholder' => ts('If')],
           ['type' => 'SqlField', 'placeholder' => ts('Then')],
index 3bad2d891878c5bf08c94870906c4d27f8f7d2ba..bb066183a3b3be466f792b03bcaac1205c0d0ffc 100644 (file)
@@ -138,10 +138,14 @@ class TokenProcessor {
    * @return TokenProcessor
    */
   public function addMessage($name, $value, $format) {
+    $tokens = [];
+    $this->visitTokens($value ?: '', function (?string $fullToken, ?string $entity, ?string $field, ?array $modifier) use (&$tokens) {
+      $tokens[$entity][] = $field;
+    });
     $this->messages[$name] = [
       'string' => $value,
       'format' => $format,
-      'tokens' => \CRM_Utils_Token::getTokens($value),
+      'tokens' => $tokens,
     ];
     return $this;
   }
@@ -361,49 +365,74 @@ class TokenProcessor {
     $useSmarty = !empty($row->context['smarty']);
 
     $tokens = $this->rowValues[$row->tokenRow][$message['format']];
-    $getToken = function($m) use ($tokens, $useSmarty, $row) {
-      [$full, $entity, $field] = $m;
+    $getToken = function(?string $fullToken, ?string $entity, ?string $field, ?array $modifier) use ($tokens, $useSmarty, $row) {
       if (isset($tokens[$entity][$field])) {
         $v = $tokens[$entity][$field];
-        if (isset($m[3])) {
-          $v = $this->filterTokenValue($v, $m[3], $row);
-        }
+        $v = $this->filterTokenValue($v, $modifier, $row);
         if ($useSmarty) {
           $v = \CRM_Utils_Token::tokenEscapeSmarty($v);
         }
         return $v;
       }
-      return $full;
+      return $fullToken;
     };
 
     $event = new TokenRenderEvent($this);
     $event->message = $message;
     $event->context = $row->context;
     $event->row = $row;
-    // Regex examples: '{foo.bar}', '{foo.bar|whiz}'
-    // Regex counter-examples: '{foobar}', '{foo bar}', '{$foo.bar}', '{$foo.bar|whiz}', '{foo.bar|whiz{bang}}'
-    // Key observations: Civi tokens MUST have a `.` and MUST NOT have a `$`. Civi filters MUST NOT have `{}`s or `$`s.
-    $tokRegex = '([\w]+)\.([\w:\.]+)';
-    $filterRegex = '(\w+)';
-    $event->string = preg_replace_callback(";\{$tokRegex(?:\|$filterRegex)?\};", $getToken, $message['string']);
+    $event->string = $this->visitTokens($message['string'] ?? '', $getToken);
     $this->dispatcher->dispatch('civi.token.render', $event);
     return $event->string;
   }
 
+  private function visitTokens(string $expression, callable $callback): string {
+    // Regex examples: '{foo.bar}', '{foo.bar|whiz}', '{foo.bar|whiz:"bang"}', '{foo.bar|whiz:"bang":"bang"}'
+    // Regex counter-examples: '{foobar}', '{foo bar}', '{$foo.bar}', '{$foo.bar|whiz}', '{foo.bar|whiz{bang}}'
+    // Key observations: Civi tokens MUST have a `.` and MUST NOT have a `$`. Civi filters MUST NOT have `{}`s or `$`s.
+    $tokRegex = '([\w]+)\.([\w:\.]+)'; /* EX: 'foo.bar' in '{foo.bar|whiz:"bang":"bang"}' */
+    $argRegex = ':[\w": %\-_()\[\]\+/#@!,\.\?]*'; /* EX: ':"bang":"bang"' in '{foo.bar|whiz:"bang":"bang"}' */
+    // Debatable: Maybe relax to this: $argRegex = ':[^{}\n]*'; /* EX: ':"bang":"bang"' in '{foo.bar|whiz:"bang":"bang"}' */
+    $filterRegex = "(\w+(?:$argRegex)?)"; /* EX: 'whiz:"bang"' in '{foo.bar|whiz:"bang"' */
+    return preg_replace_callback(";\{$tokRegex(?:\|$filterRegex)?\};", function($m) use ($callback) {
+      $filterParts = NULL;
+      if (isset($m[3])) {
+        $filterParts = [];
+        $enqueue = function($m) use (&$filterParts) {
+          $filterParts[] = $m[1];
+          return '';
+        };
+        $unmatched = preg_replace_callback_array([
+          '/^(\w+)/' => $enqueue,
+          '/:"([^"]+)"/' => $enqueue,
+        ], $m[3]);
+        if ($unmatched) {
+          throw new \CRM_Core_Exception("Malformed token parameters (" . $m[0] . ")");
+        }
+      }
+      return $callback($m[0] ?? NULL, $m[1] ?? NULL, $m[2] ?? NULL, $filterParts);
+    }, $expression);
+  }
+
   /**
    * Given a token value, run it through any filters.
    *
    * @param mixed $value
    *   Raw token value (e.g. from `$row->tokens['foo']['bar']`).
-   * @param string $filter
+   * @param array|null $filter
    * @param TokenRow $row
    *   The current target/row.
    * @return string
    * @throws \CRM_Core_Exception
    */
-  private function filterTokenValue($value, $filter, TokenRow $row) {
+  private function filterTokenValue($value, ?array $filter, TokenRow $row) {
     // KISS demonstration. This should change... e.g. provide a filter-registry or reuse Smarty's registry...
-    switch ($filter) {
+
+    if ($value instanceof \DateTime && $filter === NULL) {
+      $filter = ['crmDate'];
+    }
+
+    switch ($filter[0]) {
       case NULL:
         return $value;
 
@@ -413,6 +442,13 @@ class TokenProcessor {
       case 'lower':
         return mb_strtolower($value);
 
+      case 'crmDate':
+        if ($value instanceof \DateTime) {
+          // @todo cludgey.
+          require_once 'CRM/Core/Smarty/plugins/modifier.crmDate.php';
+          return \smarty_modifier_crmDate($value->format('Y-m-d H:i:s'), $filter[1] ?? NULL);
+        }
+
       default:
         throw new \CRM_Core_Exception("Invalid token filter: $filter");
     }
index fb1fda5c218276f353b03e45501fd29ac6d691ea..46aa8a889de8a1ee7fcc928287ecb3800849d1e5 100644 (file)
@@ -283,15 +283,18 @@ class TokenRow {
         // HTML => Plain.
         foreach ($htmlTokens as $entity => $values) {
           foreach ($values as $field => $value) {
+            if (!$value instanceof \DateTime) {
+              $value = html_entity_decode(strip_tags($value));
+            }
             if (!isset($textTokens[$entity][$field])) {
-              $textTokens[$entity][$field] = html_entity_decode(strip_tags($value));
+              $textTokens[$entity][$field] = $value;
             }
           }
         }
         break;
 
       default:
-        throw new \RuntimeException("Invalid format");
+        throw new \RuntimeException('Invalid format');
     }
 
     return $this;
index 1277cf66db21bb4edfc4413e959b3a925b2b83af..1788763d3c2cbdad594e76ce4ed6da25fd9d0ad4 100644 (file)
@@ -107,6 +107,33 @@ class AfformSubmitEvent extends AfformBaseEvent {
     return $this;
   }
 
+  /**
+   * Get the id of a saved record
+   * @param int $index
+   * @return mixed
+   */
+  public function getEntityId(int $index = 0) {
+    $idField = CoreUtil::getIdFieldName($this->entityName);
+    return $this->entityIds[$this->entityName][$index][$idField] ?? NULL;
+  }
+
+  /**
+   * Get records to be saved
+   * @return array
+   */
+  public function getRecords(): array {
+    return $this->records;
+  }
+
+  /**
+   * @param array $records
+   * @return $this
+   */
+  public function setRecords(array $records) {
+    $this->records = $records;
+    return $this;
+  }
+
   /**
    * @param int $index
    * @param string $joinEntity
index 48c553074939651b7a316d2f57eff279e27e9857..3d3a57a2d4784597943b3f2e20c8f1831f45712d 100644 (file)
@@ -15,6 +15,17 @@ Other resources for identifying changes are:
     * https://github.com/civicrm/civicrm-joomla
     * https://github.com/civicrm/civicrm-wordpress
 
+## CiviCRM 5.42.0
+
+Released October 6, 2021
+
+- **[Synopsis](release-notes/5.42.0.md#synopsis)**
+- **[Features](release-notes/5.42.0.md#features)**
+- **[Bugs resolved](release-notes/5.42.0.md#bugs)**
+- **[Miscellany](release-notes/5.42.0.md#misc)**
+- **[Credits](release-notes/5.42.0.md#credits)**
+- **[Feedback](release-notes/5.42.0.md#feedback)**
+
 ## CiviCRM 5.41.0
 
 Released September 1, 2021
diff --git a/release-notes/5.42.0.md b/release-notes/5.42.0.md
new file mode 100644 (file)
index 0000000..8d7f5d5
--- /dev/null
@@ -0,0 +1,442 @@
+# CiviCRM 5.42.0
+
+Released October 6, 2021
+
+- **[Synopsis](#synopsis)**
+- **[Features](#features)**
+- **[Bugs resolved](#bugs)**
+- **[Miscellany](#misc)**
+- **[Credits](#credits)**
+- **[Feedback](#feedback)**
+
+## <a name="synopsis"></a>Synopsis
+
+| *Does this version...?*                                         |         |
+|:--------------------------------------------------------------- |:-------:|
+| Fix security vulnerabilities?                                   |         |
+| Change the database schema?                                     |         |
+| Alter the API?                                                  |         |
+| Require attention to configuration options?                     |         |
+| Fix problems installing or upgrading to a previous version?     |         |
+| Introduce features?                                             |         |
+| Fix bugs?                                                       |         |
+
+## <a name="features"></a>Features
+
+### Core CiviCRM
+
+- **CRM- Missing Summary ([58](https://github.com/civicrm/civicrm-joomla/pull/58))**
+
+## <a name="bugs"></a>Bugs resolved
+
+### Core CiviCRM
+
+- **SearchKit - Fix aggregated joins ([21411](https://github.com/civicrm/civicrm-core/pull/21411))**
+
+- **SearchKit - Add download CSV action ([21328](https://github.com/civicrm/civicrm-core/pull/21328))**
+
+- **TokenProcessor - Allow defining Smarty variables which are populated via token ([21336](https://github.com/civicrm/civicrm-core/pull/21336))**
+
+- **[Ref] Deprecate Core_Error handling ([21279](https://github.com/civicrm/civicrm-core/pull/21279))**
+
+- **Afform - Fix button appearance and block form during submission ([21287](https://github.com/civicrm/civicrm-core/pull/21287))**
+
+- **[NFC] Fix E-notice in Afform unit tests ([21345](https://github.com/civicrm/civicrm-core/pull/21345))**
+
+- **[REF] Fix Page Hook test on php8 by putting in guard into customDataB… ([21344](https://github.com/civicrm/civicrm-core/pull/21344))**
+
+- **SearchKit - Add links to admin table and refresh after popups ([21343](https://github.com/civicrm/civicrm-core/pull/21343))**
+
+- **Tidies Joomla 4 integration (menu, padding) after final release ([21342](https://github.com/civicrm/civicrm-core/pull/21342))**
+
+- **Simplify ContributionView form. Always display "lineitems" ([21285](https://github.com/civicrm/civicrm-core/pull/21285))**
+
+- **[NFC] Cleanup boilerplate code in extension upgrader classes ([21340](https://github.com/civicrm/civicrm-core/pull/21340))**
+
+- **dev/core#2806 Fix accidental exposure of v4 tokens ([21337](https://github.com/civicrm/civicrm-core/pull/21337))**
+
+- **[NFC/Unit test] Update flaky test CRM_Utils_TokenConsistencyTest::testCaseTokenConsistency ([21341](https://github.com/civicrm/civicrm-core/pull/21341))**
+
+- **[REF] dev/core#2790 deprecate preProcessSingle ([21334](https://github.com/civicrm/civicrm-core/pull/21334))**
+
+- **Afform - Optimize Get by checking type ([21316](https://github.com/civicrm/civicrm-core/pull/21316))**
+
+- **Fixes unusable modals in Joomla 4 ([21286](https://github.com/civicrm/civicrm-core/pull/21286))**
+
+- **SearchKit - Use a search display to display searches ([21270](https://github.com/civicrm/civicrm-core/pull/21270))**
+
+- **SearchKit - Fix pager count and add 'None Found' text in empty tables ([21333](https://github.com/civicrm/civicrm-core/pull/21333))**
+
+- **[REF] dev/core#2790 move preProcess static to the trait ([21331](https://github.com/civicrm/civicrm-core/pull/21331))**
+
+- **[REF] dev/core#2790 Deprecate CRM/Member/Form/Task/PDFLetterCommon ([21305](https://github.com/civicrm/civicrm-core/pull/21305))**
+
+- **5.41 to master ([21332](https://github.com/civicrm/civicrm-core/pull/21332))**
+
+- **[REF] dev/core#2790 Pre process cleanup on pdf tasks ([21310](https://github.com/civicrm/civicrm-core/pull/21310))**
+
+- **[REF] dev/core#2790 towards pdf task trait ([21276](https://github.com/civicrm/civicrm-core/pull/21276))**
+
+- **Afform - selectable location type for address, email, etc ([21254](https://github.com/civicrm/civicrm-core/pull/21254))**
+
+- ** dev/core#2732 SearchKit - Move field formatting from client-side to server-side  ([21320](https://github.com/civicrm/civicrm-core/pull/21320))**
+
+- **Fix support link just added in oauth-client extension info.xml ([21256](https://github.com/civicrm/civicrm-core/pull/21256))**
+
+- **Remove extraneous buildQuickForm ([21325](https://github.com/civicrm/civicrm-core/pull/21325))**
+
+- **[NFC] Fix UpdateSubscriptionTest on php8 by creating a Payment Processor ([21324](https://github.com/civicrm/civicrm-core/pull/21324))**
+
+- **[REF] dev/core#2790 move buildForm to pdfTrait ([21297](https://github.com/civicrm/civicrm-core/pull/21297))**
+
+- **5.41 to master ([21323](https://github.com/civicrm/civicrm-core/pull/21323))**
+
+- **SearchKit - Allow random sorting (Fixes dev/report#75) ([21177](https://github.com/civicrm/civicrm-core/pull/21177))**
+
+- **[REF] Fix undefined smarty vars in Advanced Search ([21321](https://github.com/civicrm/civicrm-core/pull/21321))**
+
+- **better target multivalue checkbox and multiselect import validation ([21317](https://github.com/civicrm/civicrm-core/pull/21317))**
+
+- **Move financial acl setting to the extension ([21120](https://github.com/civicrm/civicrm-core/pull/21120))**
+
+- **Fix Searchkit "Add" columns button UI ([21315](https://github.com/civicrm/civicrm-core/pull/21315))**
+
+- **[NFC] Fix undefined array key when running CRM unit test suite in php8 ([21314](https://github.com/civicrm/civicrm-core/pull/21314))**
+
+- **[REF] Improve Custom data insert performance when using the copyCusto… ([21313](https://github.com/civicrm/civicrm-core/pull/21313))**
+
+- **SavedSearch - Add pseudoconstant for api_entity field ([21312](https://github.com/civicrm/civicrm-core/pull/21312))**
+
+- **dev/core#2682 Entity batch declare option group for pseudoconstant ([21241](https://github.com/civicrm/civicrm-core/pull/21241))**
+
+- **Use getter to get subscription id ([21309](https://github.com/civicrm/civicrm-core/pull/21309))**
+
+- **[REF] Copy preProcessFromAddress back into the pdf function ([21306](https://github.com/civicrm/civicrm-core/pull/21306))**
+
+- **Add test to UpdateSubscription form ([21282](https://github.com/civicrm/civicrm-core/pull/21282))**
+
+- **Fix deprecated API4 Join on Email in dynamic profile ([21308](https://github.com/civicrm/civicrm-core/pull/21308))**
+
+- **Do not add tracking to internal anchor URLs ([20115](https://github.com/civicrm/civicrm-core/pull/20115))**
+
+- **dev/core#2546 Add settings button to group page ([20135](https://github.com/civicrm/civicrm-core/pull/20135))**
+
+- **5.41 ([21307](https://github.com/civicrm/civicrm-core/pull/21307))**
+
+- **5.41 ([21303](https://github.com/civicrm/civicrm-core/pull/21303))**
+
+- **[NFC] Fix APIv4 Conformance tests on php8 ([21302](https://github.com/civicrm/civicrm-core/pull/21302))**
+
+- **[Ref] intial testing on case tokens, make knownTokens optional ([21289](https://github.com/civicrm/civicrm-core/pull/21289))**
+
+- **SearchKit - Image field handler implementation ([21300](https://github.com/civicrm/civicrm-core/pull/21300))**
+
+- **[REF] Remove duplicate IF ([21298](https://github.com/civicrm/civicrm-core/pull/21298))**
+
+- **Fix for new prefetch key ([21292](https://github.com/civicrm/civicrm-core/pull/21292))**
+
+- **[REF] Minor extraction ([21296](https://github.com/civicrm/civicrm-core/pull/21296))**
+
+- **[REF] Remove unreachable code ([21294](https://github.com/civicrm/civicrm-core/pull/21294))**
+
+- **[Ref] Minor extraction ([21293](https://github.com/civicrm/civicrm-core/pull/21293))**
+
+- **[REF] dev/core#2790 Initial creation of pdf trait ([21290](https://github.com/civicrm/civicrm-core/pull/21290))**
+
+- **(dev/translation#70) Multilingual - Fix loading multiple translations within same page-view (OptionValues, ContactTypes) ([21268](https://github.com/civicrm/civicrm-core/pull/21268))**
+
+- **Change the default PDF file name from "CiviLetter.pdf" to use the Activity Subject, if available ([21220](https://github.com/civicrm/civicrm-core/pull/21220))**
+
+- **(dev/mail#83) Workflow Messages - Introduce class contracts ([21139](https://github.com/civicrm/civicrm-core/pull/21139))**
+
+- **5.41 ([21281](https://github.com/civicrm/civicrm-core/pull/21281))**
+
+- **5.41 ([21280](https://github.com/civicrm/civicrm-core/pull/21280))**
+
+- **Afform - support file uploads ([21150](https://github.com/civicrm/civicrm-core/pull/21150))**
+
+- **dev/core#2789 - Filename is not non-english-friendly in print/merge document ([21259](https://github.com/civicrm/civicrm-core/pull/21259))**
+
+- **REF Don't check if id is set in ContributionView form - it's required ([21274](https://github.com/civicrm/civicrm-core/pull/21274))**
+
+- **[REF] Remove meaningless if ([21273](https://github.com/civicrm/civicrm-core/pull/21273))**
+
+- **dev/core#2717  - further cleanup ([21126](https://github.com/civicrm/civicrm-core/pull/21126))**
+
+- **Remove deprecated isDevelopment() function ([21269](https://github.com/civicrm/civicrm-core/pull/21269))**
+
+- **Change PDF file name from "civicrmContributionReceipt.pdf" to use the standard "receipt.pdf" file name ([21221](https://github.com/civicrm/civicrm-core/pull/21221))**
+
+- **5.41 ([21266](https://github.com/civicrm/civicrm-core/pull/21266))**
+
+- **Do not enable custom activity search on new installs ([21260](https://github.com/civicrm/civicrm-core/pull/21260))**
+
+- **5.41 to master ([21264](https://github.com/civicrm/civicrm-core/pull/21264))**
+
+- **Logging improvements for "Failed to update contribution in database" ([21243](https://github.com/civicrm/civicrm-core/pull/21243))**
+
+- **5.41 ([21258](https://github.com/civicrm/civicrm-core/pull/21258))**
+
+- **dev/core#2717 Simplify batch membership renewal ([20935](https://github.com/civicrm/civicrm-core/pull/20935))**
+
+- **dev/core#2634 Add v4 Membership api, access it via order ([21106](https://github.com/civicrm/civicrm-core/pull/21106))**
+
+- **Template fixes - notices, syntax ([21257](https://github.com/civicrm/civicrm-core/pull/21257))**
+
+- **Fix invalid parameter giving E_WARNING ([21255](https://github.com/civicrm/civicrm-core/pull/21255))**
+
+- **Provided standard links in ext/oauth-client/info.xml, fixed typo ([21252](https://github.com/civicrm/civicrm-core/pull/21252))**
+
+- **[Ref] Copy emailcommon function back to email trait ([21251](https://github.com/civicrm/civicrm-core/pull/21251))**
+
+- **[REF] Update a few references to invoicing ([21101](https://github.com/civicrm/civicrm-core/pull/21101))**
+
+- **SearchKit - Allow tokens in menu button text ([21217](https://github.com/civicrm/civicrm-core/pull/21217))**
+
+- ** dev/core#2745 - Contribution Tokens - Support 'contributionId' ([21134](https://github.com/civicrm/civicrm-core/pull/21134))**
+
+- **dev/core#2121 Ability to change pdf filename before downloading ([21006](https://github.com/civicrm/civicrm-core/pull/21006))**
+
+- **5.41 to master ([21246](https://github.com/civicrm/civicrm-core/pull/21246))**
+
+- **[NFC] - Replace deprecated function in AngularLoaderTest ([21244](https://github.com/civicrm/civicrm-core/pull/21244))**
+
+- **CiviCRM Mailing, function unsub_from_mailing has spelling error, "experiement" impacts A/B Mailing unsubscribes ([21245](https://github.com/civicrm/civicrm-core/pull/21245))**
+
+- **dev/core#2769 use php email validation not hacked & bad quickform function ([21169](https://github.com/civicrm/civicrm-core/pull/21169))**
+
+- **(Smart Group) is being constantly added while editing the smart group title from 'Manage Group' page ([20898](https://github.com/civicrm/civicrm-core/pull/20898))**
+
+- **5.41 to master ([21240](https://github.com/civicrm/civicrm-core/pull/21240))**
+
+- **Afform - Store submissions in a new database table ([21105](https://github.com/civicrm/civicrm-core/pull/21105))**
+
+- **5.41 ([21231](https://github.com/civicrm/civicrm-core/pull/21231))**
+
+- **Add date metadata for email.on_hold, reset_date ([21233](https://github.com/civicrm/civicrm-core/pull/21233))**
+
+- **Afform - fix contact source field & field defaults ([21228](https://github.com/civicrm/civicrm-core/pull/21228))**
+
+- **Afform - Rename blocks and joins for clarity ([21218](https://github.com/civicrm/civicrm-core/pull/21218))**
+
+- **[REF] APIv4 Notes - Ensure child notes are deleted with parent, and hooks are called ([21208](https://github.com/civicrm/civicrm-core/pull/21208))**
+
+- **[REF] Remove unused/unneeded variables from Note View page ([21226](https://github.com/civicrm/civicrm-core/pull/21226))**
+
+- **EntityBatch - Deprecate del() function (dev/core#2757) ([21213](https://github.com/civicrm/civicrm-core/pull/21213))**
+
+- **Fixes dev/core#2778 - Fatal error on dedupe screen ([21223](https://github.com/civicrm/civicrm-core/pull/21223))**
+
+- **Improve test for CRM_Utils_Recent ([21222](https://github.com/civicrm/civicrm-core/pull/21222))**
+
+- **Alternate to 20131 - Avoid crash during import for blank lines in a one-column csv file ([21216](https://github.com/civicrm/civicrm-core/pull/21216))**
+
+- **[REF] CRM_Utils_Recent - Use hook listener to delete items ([21204](https://github.com/civicrm/civicrm-core/pull/21204))**
+
+- **Extract ACL contact cache clearing part out ([21219](https://github.com/civicrm/civicrm-core/pull/21219))**
+
+- **dev/core#2774 : Sort by date column on multirecord field listing section on profile edit mode doesn't work ([21191](https://github.com/civicrm/civicrm-core/pull/21191))**
+
+- **[REF] dev/core#2757 Move acl delete logic to an event listener ([21201](https://github.com/civicrm/civicrm-core/pull/21201))**
+
+- **[NFC] CRM_Utils_SystemTest - Call to Uri->withPath() using deprecated format ([21215](https://github.com/civicrm/civicrm-core/pull/21215))**
+
+- **[NFC] CRM_Extension_Manager_ModuleUpgTest - use ?? instead of error-suppression operator ([21214](https://github.com/civicrm/civicrm-core/pull/21214))**
+
+- **[REF] Deprecate unnecessary del() functions ([21200](https://github.com/civicrm/civicrm-core/pull/21200))**
+
+- **Remove unused, duplicate functions getEntitiesByTag ([21209](https://github.com/civicrm/civicrm-core/pull/21209))**
+
+- **[NFC] CRM_Extension_Manager_ModuleTest - use ?? instead of error-suppression operator ([21206](https://github.com/civicrm/civicrm-core/pull/21206))**
+
+- **CRM_Queue_Service - Use ?? instead of error-supression operator ([21207](https://github.com/civicrm/civicrm-core/pull/21207))**
+
+- **5.41 to master (conflicts resolved) ([21203](https://github.com/civicrm/civicrm-core/pull/21203))**
+
+- **APIv4 pseudoconstant improvements ([21184](https://github.com/civicrm/civicrm-core/pull/21184))**
+
+- **REF Switch to CRM_Core_Form::setTitle() instead of CRM_Utils_System::setTitle() part 1 ([21193](https://github.com/civicrm/civicrm-core/pull/21193))**
+
+- **add grid layout support for searchkit ([21194](https://github.com/civicrm/civicrm-core/pull/21194))**
+
+- **5.41 ([21190](https://github.com/civicrm/civicrm-core/pull/21190))**
+
+- **Add no-prefetch campaign pseudoconstants ([21185](https://github.com/civicrm/civicrm-core/pull/21185))**
+
+- **SearchKit - Misc bulk action bug fixes ([21159](https://github.com/civicrm/civicrm-core/pull/21159))**
+
+- **Remove deprecated function ([21179](https://github.com/civicrm/civicrm-core/pull/21179))**
+
+- **Log details of mailing error and don't display details to end user ([21173](https://github.com/civicrm/civicrm-core/pull/21173))**
+
+- **closes core#2770: Dedupe by website ([21168](https://github.com/civicrm/civicrm-core/pull/21168))**
+
+- **dev/core#2762 Fix custom field edit form to set serialization correctly ([21160](https://github.com/civicrm/civicrm-core/pull/21160))**
+
+- **dev/core#2758 - Fix contribution activity campaign propagation ...more ([21171](https://github.com/civicrm/civicrm-core/pull/21171))**
+
+- **SearchKit - Add placeholder to token select ([21172](https://github.com/civicrm/civicrm-core/pull/21172))**
+
+- **Update MembershipType.duration and MembershipStatus.name to be required ([21119](https://github.com/civicrm/civicrm-core/pull/21119))**
+
+- **Enotice fixes in tpl ([21170](https://github.com/civicrm/civicrm-core/pull/21170))**
+
+- **APIv4 - Support multiple implicit joins to the same table ([21071](https://github.com/civicrm/civicrm-core/pull/21071))**
+
+- **dev/core#2763 cache clearing fix ([21166](https://github.com/civicrm/civicrm-core/pull/21166))**
+
+- **Fix search display access for non-admin users ([21082](https://github.com/civicrm/civicrm-core/pull/21082))**
+
+- **dev/core#2758 - Fix contribution activity campaign propagation ([21167](https://github.com/civicrm/civicrm-core/pull/21167))**
+
+- **[Ref] remove unused variable ([21161](https://github.com/civicrm/civicrm-core/pull/21161))**
+
+- **[REF] SearchKit - Refactor search task code to share a trait ([21156](https://github.com/civicrm/civicrm-core/pull/21156))**
+
+- **5.41 ([21164](https://github.com/civicrm/civicrm-core/pull/21164))**
+
+- **APIv4 - Add File entity ([21158](https://github.com/civicrm/civicrm-core/pull/21158))**
+
+- **[NFC] Update CRM_Core_RegionTest so it doesn't need the error-suppression operator ([21155](https://github.com/civicrm/civicrm-core/pull/21155))**
+
+- **5.41 ([21154](https://github.com/civicrm/civicrm-core/pull/21154))**
+
+- **5.41 ([21148](https://github.com/civicrm/civicrm-core/pull/21148))**
+
+- **[NFC] Update testCaseActivityCopyTemplate to provide variable that would usually be present ([21146](https://github.com/civicrm/civicrm-core/pull/21146))**
+
+- **Use convenience function for one-off token evaluations to avoid too-long filenames and possible privacy issues ([21140](https://github.com/civicrm/civicrm-core/pull/21140))**
+
+- **[Ref] Move id fetching to the classes ([21075](https://github.com/civicrm/civicrm-core/pull/21075))**
+
+- **dev/search#63 Add recurring contributions to contribution reports ([20168](https://github.com/civicrm/civicrm-core/pull/20168))**
+
+- **5.41 ([21138](https://github.com/civicrm/civicrm-core/pull/21138))**
+
+- **(REF) ReflectionUtils - Add findStandardProperties() and findMethodHelpers() ([21114](https://github.com/civicrm/civicrm-core/pull/21114))**
+
+- **5.41 ([21129](https://github.com/civicrm/civicrm-core/pull/21129))**
+
+- **dev/core#2691 - On logging detail civireport show words instead of numbers (part 2) ([20907](https://github.com/civicrm/civicrm-core/pull/20907))**
+
+- **NFC - Fix docblock in CRM_Core_Transaction ([21125](https://github.com/civicrm/civicrm-core/pull/21125))**
+
+- **5.41 to master  ([21127](https://github.com/civicrm/civicrm-core/pull/21127))**
+
+- **[NFC] {Test} Minor cleanup ([21116](https://github.com/civicrm/civicrm-core/pull/21116))**
+
+- **dev/user-interface#38 Contact Edit: Only display signatures if contact has a CMS account ([21103](https://github.com/civicrm/civicrm-core/pull/21103))**
+
+- **Replace deprecated calls to `renderMessageTemplate()` ([21121](https://github.com/civicrm/civicrm-core/pull/21121))**
+
+- **MessageTemplate - Add renderTemplate(). Deprecate renderMessageTemplate(). ([21115](https://github.com/civicrm/civicrm-core/pull/21115))**
+
+- **5.41 to master ([21117](https://github.com/civicrm/civicrm-core/pull/21117))**
+
+- **5.41 ([21113](https://github.com/civicrm/civicrm-core/pull/21113))**
+
+- **5.41 ([21111](https://github.com/civicrm/civicrm-core/pull/21111))**
+
+- **dev/core#2747 Reconcile remaining fields between scheduled reminders and legacy tokens ([21046](https://github.com/civicrm/civicrm-core/pull/21046))**
+
+- **Replace extension key with label during install/upgrade/disable/uninstall ([21094](https://github.com/civicrm/civicrm-core/pull/21094))**
+
+- **Respect http_timeout core setting for Guzzle HTTP requests ([21096](https://github.com/civicrm/civicrm-core/pull/21096))**
+
+- **dev/core#2717 Use Same order ->payment flow for non recurring back of… ([20936](https://github.com/civicrm/civicrm-core/pull/20936))**
+
+- **Token Parser - Allow tokens with multiple dots (eg {contribution.contribution_recur_id.amount}) ([21076](https://github.com/civicrm/civicrm-core/pull/21076))**
+
+- **dev/core#2719 [REF] Remove a couple more (tested) references to legacy contribution_invoice_settings ([20991](https://github.com/civicrm/civicrm-core/pull/20991))**
+
+- **(dev/core#2673) Email Tokens - Custom tokens in `subject` block similar tokens in `body` ([21080](https://github.com/civicrm/civicrm-core/pull/21080))**
+
+- **[REF] SearchKit - Use non-deprecated join syntax when loading standalone displays ([21095](https://github.com/civicrm/civicrm-core/pull/21095))**
+
+- **(NFC) MailingQueryEvent - Add more docblocks about query-writing and `tokenContext_*` ([21098](https://github.com/civicrm/civicrm-core/pull/21098))**
+
+- **ActionSchedule - Pass real batches into TokenProcessor. Simplify CRM_Activity_Tokens. ([21088](https://github.com/civicrm/civicrm-core/pull/21088))**
+
+- **Scheduled Reminders UI - Show more activity tokens in admin GUI ([21091](https://github.com/civicrm/civicrm-core/pull/21091))**
+
+- **[REF] Afform - Code cleanup in LoadAdminData API action ([21089](https://github.com/civicrm/civicrm-core/pull/21089))**
+
+- **Upgrade angular-file-uploader to v2.6.1 ([21081](https://github.com/civicrm/civicrm-core/pull/21081))**
+
+- **(NFC) Expand test coverage for scheduled-reminders with `{activity.*}` tokens ([21092](https://github.com/civicrm/civicrm-core/pull/21092))**
+
+- **(NFC) TokenProcessorTest - Add scenario inspired by dev/core#2673 ([21090](https://github.com/civicrm/civicrm-core/pull/21090))**
+
+- **CRM_Core_Component - Remove unused code ([21086](https://github.com/civicrm/civicrm-core/pull/21086))**
+
+- **Upgrade Pear/DB package to be version 1.11.0 ([21087](https://github.com/civicrm/civicrm-core/pull/21087))**
+
+- **Fix caching on campaign pseudoconstant ([21083](https://github.com/civicrm/civicrm-core/pull/21083))**
+
+- **Scheduled Reminders - Pass locale through to TokenProcessor ([21085](https://github.com/civicrm/civicrm-core/pull/21085))**
+
+- **[Ref] Simplify IF clause ([21078](https://github.com/civicrm/civicrm-core/pull/21078))**
+
+- **APIv4 - Silently ignore errors in CoreUtil::getInfoItem() ([21084](https://github.com/civicrm/civicrm-core/pull/21084))**
+
+- **Fix the check to see if the financialAclExtension is installed ([21077](https://github.com/civicrm/civicrm-core/pull/21077))**
+
+- **Remove no longer used variable in Email.tpl / smarty warning ([21074](https://github.com/civicrm/civicrm-core/pull/21074))**
+
+- **[Ref] extract function to getEmailDefaults ([21067](https://github.com/civicrm/civicrm-core/pull/21067))**
+
+- **MessageTemplate::sendTemplate() - Accept `array $messageTemplate` and `array $tokenContext` ([21073](https://github.com/civicrm/civicrm-core/pull/21073))**
+
+- **APIv4 - Throw exception instead of munging illegal join aliases ([21072](https://github.com/civicrm/civicrm-core/pull/21072))**
+
+- **SearchKit - Merge admin results table with searchDisplay code ([21069](https://github.com/civicrm/civicrm-core/pull/21069))**
+
+- ** dev/core#2747 REF] Move all the generic functions to the parent ([21057](https://github.com/civicrm/civicrm-core/pull/21057))**
+
+- **[Ref] Clarify what parameters are passed in ([21063](https://github.com/civicrm/civicrm-core/pull/21063))**
+
+- **Smarty notice - Explicitly set hideRelativeLabel var on Find Cases form ([21070](https://github.com/civicrm/civicrm-core/pull/21070))**
+
+- **Move make-sure-single-set out of shared function ([21062](https://github.com/civicrm/civicrm-core/pull/21062))**
+
+- **[REF] SearchKit - display code refactor + pager options ([21049](https://github.com/civicrm/civicrm-core/pull/21049))**
+
+- **Fix Membership.create in BAO to respect passed in status_id ([20976](https://github.com/civicrm/civicrm-core/pull/20976))**
+
+- **dev/core#2730 - Replace fopen call in CRM_Utils_File::isIncludable with one that doesn't need error-supression to avoid problems in php8 ([21060](https://github.com/civicrm/civicrm-core/pull/21060))**
+
+- **[Ref] Move rule to email trait ([21066](https://github.com/civicrm/civicrm-core/pull/21066))**
+
+- **Remove unused assignment ([21061](https://github.com/civicrm/civicrm-core/pull/21061))**
+
+- **5.41 ([21056](https://github.com/civicrm/civicrm-core/pull/21056))**
+
+- **[Ref] cleanup alterActionSchedule ([21047](https://github.com/civicrm/civicrm-core/pull/21047))**
+
+- **dev/drupal#161 - Remove drush sample data install option that doesn't work ([648](https://github.com/civicrm/civicrm-drupal/pull/648))**
+
+- **Update quickform original ([330](https://github.com/civicrm/civicrm-packages/pull/330))**
+
+- **Fixes dev/core#2769 remove quickform hack - we are no longer calling this rule ([329](https://github.com/civicrm/civicrm-packages/pull/329))**
+
+- **Replace Drupal 9 user function, function getUsername is no more valid ([328](https://github.com/civicrm/civicrm-packages/pull/328))**
+
+## <a name="misc"></a>Miscellany
+
+## <a name="credits"></a>Credits
+
+This release was developed by the following code authors:
+
+AGH Strategies - Alice Frumin, Andie Hunt; Agileware - Justin Freeman; Australian Greens - John Twyman; Benjamin W; CiviCRM - Coleman Watts, Tim Otten; CompuCorp - Debarshi Bhaumik; Coop SymbioTIC - Mathieu Lutfy; Dave D; Fuzion - Jitendra Purohit; Greenpeace Central and Eastern Europe - Patrick Figel; JMA Consulting - Joe Murray, Monish Deb, Seamus Lee; Joinery - Allen Shaw; lisandro-compucorp; Megaphone Technology Consulting - Jon Goldberg; MJW Consulting - Matthew Wire; Nicol Wistreich; Skvare - Sunil Pawar; Tadpole Collective - Kevin Cristiano; Third Sector Design - Kurund Jalmi, Michael McAndrew; Wikimedia Foundation - Eileen McNaughton; Wildsight - Lars Sanders-Green
+
+Most authors also reviewed code for this release; in addition, the following
+reviewers contributed their comments:
+
+Agileware - Justin Freeman; Black Brick Software - David Hayes; civibot[bot]; CiviCoop - Jaap Jansma; CiviCRM - Coleman Watts, Tim Otten; CompuCorp - Debarshi Bhaumik; Coop SymbioTIC - Mathieu Lutfy; Dave D; JMA Consulting - Monish Deb, Seamus Lee; Joinery - Allen Shaw; Lighthouse Consulting and Design - Brian Shaughnessy; lisandro-compucorp; Megaphone Technology Consulting - Jon Goldberg; MJW Consulting - Matthew Wire; Nicol Wistreich; redcuillin; Tadpole Collective - Kevin Cristiano; Third Sector Design - Kurund Jalmi; Wikimedia Foundation - Eileen McNaughton; Wildsight - Lars Sanders-Green
+
+## <a name="feedback"></a>Feedback
+
+These release notes are edited by Alice Frumin and Andie Hunt.  If you'd like
+to provide feedback on them, please log in to https://chat.civicrm.org/civicrm
+and contact `@agh1`.
+
diff --git a/tests/phpunit/CRM/Core/BAO/UFMatchTest.php b/tests/phpunit/CRM/Core/BAO/UFMatchTest.php
new file mode 100644 (file)
index 0000000..e9916b8
--- /dev/null
@@ -0,0 +1,18 @@
+<?php
+
+/**
+ * Class CRM_Core_BAO_UFMatchTest
+ * @group headless
+ */
+class CRM_Core_BAO_UFMatchTest extends CiviUnitTestCase {
+
+  /**
+   * Don't crash if the uf_id doesn't exist
+   */
+  public function testGetUFValuesWithNonexistentUFId() {
+    $max_id = (int) CRM_Core_DAO::singleValueQuery('SELECT MAX(uf_id) FROM civicrm_uf_match');
+    $dontcrash = CRM_Core_BAO_UFMatch::getUFValues($max_id + 1);
+    $this->assertNull($dontcrash);
+  }
+
+}
index 328e312bd4dc6c7eee11c7e1d7adb7b0a0424223..4f5e705b29c1fab32a578000cb0a8cbde913ee38 100644 (file)
@@ -32,12 +32,35 @@ class CRM_Core_TokenSmartyTest extends CiviUnitTestCase {
       ['extra' => ['foo' => 'foobar']]
     );
     $this->assertEquals('First name is Bob. ExtraFoo is foobar.', $rendered['msg_subject']);
+
+    try {
+      $modifiers = [
+        '|crmDate:"shortdate"' => '02/01/2020',
+        '|crmDate:"%B %Y"' => 'February 2020',
+        '|crmDate' => 'February 1st, 2020  3:04 AM',
+      ];
+      foreach ($modifiers as $modifier => $expected) {
+        CRM_Utils_Time::setTime('2020-02-01 03:04:05');
+        $rendered = CRM_Core_TokenSmarty::render(
+          ['msg_subject' => "Now is the token, {domain.now$modifier}! No, now is the smarty-pants, {\$extra.now$modifier}!"],
+          ['contactId' => $this->contactId],
+          ['extra' => ['now' => '2020-02-01 03:04:05']]
+        );
+        $this->assertEquals("Now is the token, $expected! No, now is the smarty-pants, $expected!", $rendered['msg_subject']);
+      }
+    }
+    finally {
+      \CRM_Utils_Time::resetTime();
+    }
   }
 
   /**
    * A template which uses token-data as part of a Smarty expression.
    */
   public function testTokenInSmarty() {
+    \CRM_Utils_Time::setTime('2022-04-08 16:32:04');
+    $resetTime = \CRM_Utils_AutoClean::with(['CRM_Utils_Time', 'resetTime']);
+
     $rendered = CRM_Core_TokenSmarty::render(
       ['msg_html' => '<p>{assign var="greeting" value="{contact.email_greeting}"}Greeting: {$greeting}!</p>'],
       ['contactId' => $this->contactId],
@@ -51,6 +74,20 @@ class CRM_Core_TokenSmartyTest extends CiviUnitTestCase {
       []
     );
     $this->assertEquals('<p>Yes CID</p>', $rendered['msg_html']);
+
+    $rendered = CRM_Core_TokenSmarty::render(
+      ['msg_html' => '<p>{assign var="greeting" value="hey yo {contact.first_name|upper} {contact.last_name|upper} circa {domain.now|crmDate:"%m/%Y"}"}My Greeting: {$greeting}!</p>'],
+      ['contactId' => $this->contactId],
+      []
+    );
+    $this->assertEquals('<p>My Greeting: hey yo BOB ROBERTS circa 04/2022!</p>', $rendered['msg_html']);
+
+    $rendered = CRM_Core_TokenSmarty::render(
+      ['msg_html' => '<p>{assign var="greeting" value="hey yo {contact.first_name} {contact.last_name|upper} circa {domain.now|crmDate:"shortdate"}"}My Greeting: {$greeting|capitalize}!</p>'],
+      ['contactId' => $this->contactId],
+      []
+    );
+    $this->assertEquals('<p>My Greeting: Hey Yo Bob ROBERTS Circa 04/08/2022!</p>', $rendered['msg_html']);
   }
 
   /**
index 050fce542a3d83edddb02a9c563186f94488c4b6..58ba85a0021b8a605c9f11f15f2175b2565a992d 100644 (file)
@@ -38,6 +38,28 @@ class CRM_Logging_LoggingTest extends CiviUnitTestCase {
     $this->assertNotEmpty(CRM_Core_DAO::singleValueQuery("SHOW tables LIKE 'log_abcd'"));
   }
 
+  /**
+   * Test that hooks removing tables from logging are respected during custom field add.
+   *
+   * During custom field save logging is only handled for the affected table.
+   * We need to make sure this respects hooks to remove from the logging set.
+   */
+  public function testLoggingHookIgnore(): void {
+    $this->hookClass->setHook('civicrm_alterLogTables', [$this, 'ignoreSillyName']);
+    Civi::settings()->set('logging', TRUE);
+    $this->createCustomGroupWithFieldOfType(['table_name' => 'silly_name']);
+    $this->assertEmpty(CRM_Core_DAO::singleValueQuery("SHOW tables LIKE 'log_silly_name'"));
+  }
+
+  /**
+   * Implement hook to cause our log table to be ignored.
+   *
+   * @param array $logTableSpec
+   */
+  public function ignoreSillyName(array &$logTableSpec): void {
+    unset($logTableSpec['silly_name']);
+  }
+
   /**
    * Test creating logging schema when database is in multilingual mode.
    */
index ac73ae3bb6eca0e51f79137b0a0c6a6bf8805781..ca8305192aa2789d70eb36c29ffe0e90056f3d28 100644 (file)
@@ -306,4 +306,22 @@ class CRM_Utils_DateTest extends CiviUnitTestCase {
     ], $date);
   }
 
+  public function testLocalizeConsts() {
+    $expect['en_US'] = ['Jan', 'Tue', 'March', 'Thursday'];
+    $expect['fr_FR'] = ['janv.', 'mar.', 'Mars', 'jeudi'];
+    $expect['es_MX'] = ['ene', 'mar', 'Marzo', 'jueves'];
+
+    foreach ($expect as $lang => $expectNames) {
+      $useLocale = CRM_Utils_AutoClean::swapLocale($lang);
+      $actualNames = [
+        CRM_Utils_Date::getAbbrMonthNames()[1],
+        CRM_Utils_Date::getAbbrWeekdayNames()[2],
+        CRM_Utils_Date::getFullMonthNames()[3],
+        CRM_Utils_Date::getFullWeekdayNames()[4],
+      ];
+      $this->assertEquals($expectNames, $actualNames, "Check temporal names in $lang");
+      unset($useLocale);
+    }
+  }
+
 }
index 0a0ab45571112070dcb4499b322d9d4dc7ef8009..96055889902d33b0f924fce613dcd329a67ad585 100644 (file)
  */
 
 use Civi\Token\TokenProcessor;
+use Civi\Api4\LocBlock;
+use Civi\Api4\Email;
+use Civi\Api4\Phone;
+use Civi\Api4\Address;
 
 /**
  * CRM_Utils_TokenConsistencyTest
@@ -446,6 +450,32 @@ Check';
     return $this->ids['Membership'][0];
   }
 
+  /**
+   * Get expected output from token parsing.
+   *
+   * @return string
+   */
+  protected function getExpectedEventTokenOutput(): string {
+    return '
+1
+Annual CiviCRM meet
+October 21st, 2008 12:00 AM
+October 23rd, 2008 12:00 AM
+Conference
+If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now
+event@example.com
+456 789
+event description
+15 Walton St
+Emerald City, Maine 90210
+
+$ 50.00
+' . CRM_Utils_System::url('civicrm/event/info', NULL, TRUE) . '&amp;reset=1&amp;id=1
+' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&amp;reset=1&amp;id=1
+
+my field';
+  }
+
   /**
    * Get expected output from token parsing.
    *
@@ -509,9 +539,50 @@ December 21st, 2007
     ]);
     $tokens['{domain.id}'] = 'Domain ID';
     $tokens['{domain.description}'] = 'Domain Description';
+    $tokens['{domain.now}'] = 'Current time/date';
     $this->assertEquals($tokens, $tokenProcessor->listTokens());
   }
 
+  /**
+   * @throws \API_Exception
+   * @throws \CRM_Core_Exception
+   */
+  public function testDomainNow(): void {
+    putenv('TIME_FUNC=frozen');
+    CRM_Utils_Time::setTime('2021-09-18 23:58:00');
+    $modifiers = [
+      'shortdate' => '09/18/2021',
+      '%B %Y' => 'September 2021',
+    ];
+    foreach ($modifiers as $filter => $expected) {
+      $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
+        'messageTemplate' => [
+          'msg_text' => '{domain.now|crmDate:"' . $filter . '"}',
+        ],
+      ])['text'];
+      $this->assertEquals($expected, $resolved);
+    }
+    $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
+      'messageTemplate' => [
+        'msg_text' => '{domain.now}',
+      ],
+    ])['text'];
+    $this->assertEquals('September 18th, 2021 11:58 PM', $resolved);
+
+    // This example is malformed - no quotes
+    try {
+      $resolved = CRM_Core_BAO_MessageTemplate::renderTemplate([
+        'messageTemplate' => [
+          'msg_text' => '{domain.now|crmDate:shortdate}',
+        ],
+      ])['text'];
+      $this->fail("Expected unquoted parameter to fail");
+    }
+    catch (\CRM_Core_Exception $e) {
+      $this->assertRegExp(';Malformed token param;', $e->getMessage());
+    }
+  }
+
   /**
    * Get declared participant tokens.
    *
@@ -525,6 +596,7 @@ December 21st, 2007
       '{domain.email}' => 'Domain (organization) email',
       '{domain.id}' => ts('Domain ID'),
       '{domain.description}' => ts('Domain Description'),
+      '{domain.now}' => 'Current time/date',
     ];
   }
 
@@ -532,6 +604,9 @@ December 21st, 2007
    * Test that domain tokens are consistently rendered.
    */
   public function testEventTokenConsistency(): void {
+    $mut = new CiviMailUtils($this);
+    $this->setupParticipantScheduledReminder();
+
     $tokens = CRM_Core_SelectValues::eventTokens();
     $this->assertEquals($this->getEventTokens(), $tokens);
     $tokenProcessor = new TokenProcessor(\Civi::dispatcher(), [
@@ -540,6 +615,57 @@ December 21st, 2007
       'schema' => ['eventId'],
     ]);
     $this->assertEquals(array_merge($tokens, $this->getDomainTokens()), $tokenProcessor->listTokens());
+
+    $this->callAPISuccess('job', 'send_reminder', []);
+    $expected = $this->getExpectedEventTokenOutput();
+    $mut->checkMailLog([$expected]);
+  }
+
+  /**
+   * Set up scheduled reminder for participants.
+   *
+   * @throws \API_Exception
+   */
+  public function setupParticipantScheduledReminder(): void {
+    $this->createCustomGroupWithFieldOfType(['extends' => 'Event']);
+    $emailID = Email::create()->setValues(['email' => 'event@example.com'])->execute()->first()['id'];
+    $addressID = Address::create()->setValues([
+      'street_address' => '15 Walton St',
+      'supplemental_address_1' => 'up the road',
+      'city' => 'Emerald City',
+      'state_province_id:label' => 'Maine',
+      'postal_code' => 90210,
+    ])->execute()->first()['id'];
+    $phoneID = Phone::create()->setValues(['phone' => '456 789'])->execute()->first()['id'];
+
+    $locationBlockID = LocBlock::save(FALSE)->setRecords([
+      [
+        'email_id' => $emailID,
+        'address_id' => $addressID,
+        'phone_id' => $phoneID,
+      ],
+    ])->execute()->first()['id'];
+    $event = $this->eventCreate([
+      'description' => 'event description',
+      $this->getCustomFieldName('text') => 'my field',
+      'loc_block_id' => $locationBlockID,
+    ]);
+    // Create an unrelated participant record so that the ids don't match.
+    // this prevents things working just because the id 'happens to be valid'
+    $this->participantCreate(['register_date' => '2020-01-01', 'event_id' => $event['id']]);
+    $this->participantCreate(['event_id' => $event['id'], 'fee_amount' => 50]);
+    CRM_Utils_Time::setTime('2007-02-20 15:00:00');
+    $this->callAPISuccess('action_schedule', 'create', [
+      'title' => 'job',
+      'subject' => 'job',
+      'entity_value' => 1,
+      'mapping_id' => 2,
+      'start_action_date' => 'register_date',
+      'start_action_offset' => 1,
+      'start_action_condition' => 'after',
+      'start_action_unit' => 'day',
+      'body_html' => implode("\n", array_keys($this->getEventTokens())),
+    ]);
   }
 
   /**
@@ -563,6 +689,7 @@ December 21st, 2007
       '{event.info_url}' => 'Event Info URL',
       '{event.registration_url}' => 'Event Registration URL',
       '{event.balance}' => 'Event Balance',
+      '{event.' . $this->getCustomFieldName('text') . '}' => 'Enter text here :: Group with field text',
     ];
   }
 
index 61a4f486de43030a4ae3e1e9026bf1d195654120..4b2b4ebb514a84f47a707d9a2c4d67cb3dfbc53a 100644 (file)
@@ -1,6 +1,7 @@
 <?php
 namespace Civi\Token;
 
+use Civi\Test\Invasive;
 use Civi\Token\Event\TokenRegisterEvent;
 use Civi\Token\Event\TokenValueEvent;
 use Symfony\Component\EventDispatcher\EventDispatcher;
@@ -30,6 +31,36 @@ class TokenProcessorTest extends \CiviUnitTestCase {
     ];
   }
 
+  /**
+   * The visitTokens() method is internal - but it is important basis for other methods.
+   * Specifically, it parses all token expressions and invokes a callback for each.
+   *
+   * Ensure these callbacks get the expected data (with various quirky notations).
+   */
+  public function testVisitTokens() {
+    $p = new TokenProcessor($this->dispatcher, [
+      'controller' => __CLASS__,
+    ]);
+    $examples = [
+      '{foo.bar}' => ['foo', 'bar', NULL],
+      '{foo.bar|whiz}' => ['foo', 'bar', ['whiz']],
+      '{foo.bar|whiz:"bang"}' => ['foo', 'bar', ['whiz', 'bang']],
+      '{love.shack|place:"bang":"b@ng, on +he/([do0r])?!"}' => ['love', 'shack', ['place', 'bang', 'b@ng, on +he/([do0r])?!']],
+    ];
+    foreach ($examples as $input => $expected) {
+      array_unshift($expected, $input);
+      $log = [];
+      Invasive::call([$p, 'visitTokens'], [
+        $input,
+        function (?string $fullToken, ?string $entity, ?string $field, ?array $modifier) use (&$log) {
+          $log[] = [$fullToken, $entity, $field, $modifier];
+        },
+      ]);
+      $this->assertEquals(1, count($log), "Should receive one callback on expression: $input");
+      $this->assertEquals($expected, $log[0]);
+    }
+  }
+
   /**
    * Test that a row can be added via "addRow(array $context)".
    */
@@ -140,20 +171,23 @@ class TokenProcessorTest extends \CiviUnitTestCase {
   }
 
   public function testRenderLocalizedSmarty() {
+    \CRM_Utils_Time::setTime('2022-04-08 16:32:04');
+    $resetTime = \CRM_Utils_AutoClean::with(['CRM_Utils_Time', 'resetTime']);
+    $this->dispatcher->addSubscriber(new \CRM_Core_DomainTokens());
     $this->dispatcher->addSubscriber(new TokenCompatSubscriber());
     $p = new TokenProcessor($this->dispatcher, [
       'controller' => __CLASS__,
       'smarty' => TRUE,
     ]);
-    $p->addMessage('text', '{ts}Yes{/ts} {ts}No{/ts}', 'text/plain');
+    $p->addMessage('text', '{ts}Yes{/ts} {ts}No{/ts} {domain.now|crmDate:"%B"}', 'text/plain');
     $p->addRow([]);
     $p->addRow(['locale' => 'fr_FR']);
     $p->addRow(['locale' => 'es_MX']);
 
     $expectText = [
-      'Yes No',
-      'Oui Non',
-      'Sí No',
+      'Yes No April',
+      'Oui Non Avril',
+      'Sí No Abril',
     ];
 
     $rowCount = 0;
index 5a0bbb29a61c494696a0ce9e6250466fa84b513a..8c3dbc2982e159ee354793fb36558ec32508d0d0 100644 (file)
@@ -855,7 +855,7 @@ class CiviUnitTestCase extends PHPUnit\Framework\TestCase {
    * @return int
    *   $id of participant created
    */
-  public function participantCreate($params = []) {
+  public function participantCreate(array $params = []) {
     if (empty($params['contact_id'])) {
       $params['contact_id'] = $this->individualCreate();
     }
@@ -1097,9 +1097,8 @@ class CiviUnitTestCase extends PHPUnit\Framework\TestCase {
    *   Name-value pair for an event.
    *
    * @return array
-   * @throws \CRM_Core_Exception
    */
-  public function eventCreate($params = []) {
+  public function eventCreate(array $params = []): array {
     // if no contact was passed, make up a dummy event creator
     if (!isset($params['contact_id'])) {
       $params['contact_id'] = $this->_contactCreate([
@@ -1113,7 +1112,7 @@ class CiviUnitTestCase extends PHPUnit\Framework\TestCase {
     $params = array_merge([
       'title' => 'Annual CiviCRM meet',
       'summary' => 'If you have any CiviCRM related issues or want to track where CiviCRM is heading, Sign up now',
-      'description' => 'This event is intended to give brief idea about progess of CiviCRM and giving solutions to common user issues',
+      'description' => 'This event is intended to give brief idea about progress of CiviCRM and giving solutions to common user issues',
       'event_type_id' => 1,
       'is_public' => 1,
       'start_date' => 20081021,
@@ -1129,7 +1128,9 @@ class CiviUnitTestCase extends PHPUnit\Framework\TestCase {
       'is_email_confirm' => 1,
     ], $params);
 
-    return $this->callAPISuccess('Event', 'create', $params);
+    $event = $this->callAPISuccess('Event', 'create', $params);
+    $this->ids['event'][] = $event['id'];
+    return $event;
   }
 
   /**
index 616616c09ac877a5fe0e7d0fbd0bb0687d08a87c..b161260412fe4871080c11ba26a4258cbecdc412 100644 (file)
@@ -148,9 +148,11 @@ class api_v3_ContactTest extends CiviUnitTestCase {
    * @throws \CRM_Core_Exception
    */
   public function testCreateIndividualNoCacheClear(): void {
-
     $contact = $this->callAPISuccess('contact', 'create', $this->_params);
-    $groupID = $this->groupCreate();
+
+    $smartGroupParams = ['form_values' => ['contact_type' => ['IN' => ['Household']]]];
+    $savedSearch = CRM_Contact_BAO_SavedSearch::create($smartGroupParams);
+    $groupID = $this->groupCreate(['saved_search_id' => $savedSearch->id]);
 
     $this->putGroupContactCacheInClearableState($groupID, $contact);
 
index 2841eeb44a95623a0fb50477ff51fa653976a756..56c09e4f55741cec33081af1b3ecaa7255e1148c 100644 (file)
@@ -32,7 +32,7 @@ class api_v3_ParticipantPaymentTest extends CiviUnitTestCase {
   public function setUp(): void {
     parent::setUp();
     $this->useTransaction(TRUE);
-    $event = $this->eventCreate(NULL);
+    $event = $this->eventCreate();
     $this->_eventID = $event['id'];
     $this->_contactID = $this->individualCreate();
     $this->_createdParticipants = [];
@@ -60,29 +60,10 @@ class api_v3_ParticipantPaymentTest extends CiviUnitTestCase {
     ]);
   }
 
-  /**
-   * Test civicrm_participant_payment_create with empty params.
-   */
-  public function testPaymentCreateEmptyParams() {
-    $params = [];
-    $this->callAPIFailure('participant_payment', 'create', $params);
-  }
-
-  /**
-   * Check without contribution_id.
-   */
-  public function testPaymentCreateMissingContributionId() {
-    //Without Payment EntityID
-    $params = [
-      'participant_id' => $this->_participantID,
-    ];
-    $this->callAPIFailure('participant_payment', 'create', $params);
-  }
-
   /**
    * Check with valid array.
    */
-  public function testPaymentCreate() {
+  public function testPaymentCreate(): void {
     //Create Contribution & get contribution ID
     $contributionID = $this->contributionCreate(['contact_id' => $this->_contactID]);
 
@@ -92,18 +73,14 @@ class api_v3_ParticipantPaymentTest extends CiviUnitTestCase {
       'contribution_id' => $contributionID,
     ];
 
-    $result = $this->callAPIAndDocument('participant_payment', 'create', $params, __FUNCTION__, __FILE__);
-    $this->assertTrue(array_key_exists('id', $result));
-
-    //delete created contribution
-    $this->contributionDelete($contributionID);
+    $this->callAPIAndDocument('participant_payment', 'create', $params, __FUNCTION__, __FILE__);
   }
 
   /**
    * Test getPaymentInfo() returns correct
    * information of the participant payment
    */
-  public function testPaymentInfoForEvent() {
+  public function testPaymentInfoForEvent(): void {
     //Create Contribution & get contribution ID
     $contributionID = $this->contributionCreate(['contact_id' => $this->_contactID]);
 
@@ -120,35 +97,6 @@ class api_v3_ParticipantPaymentTest extends CiviUnitTestCase {
     $this->assertEquals('100.00', $paymentInfo['total']);
   }
 
-  ///////////////// civicrm_participant_payment_create methods
-
-  /**
-   * Check with empty array.
-   */
-  public function testPaymentUpdateEmpty() {
-    $this->callAPIFailure('participant_payment', 'create', []);
-  }
-
-  /**
-   * Check with missing participant_id.
-   */
-  public function testPaymentUpdateMissingParticipantId() {
-    $params = [
-      'contribution_id' => '3',
-    ];
-    $this->callAPIFailure('participant_payment', 'create', $params);
-  }
-
-  /**
-   * Check with missing contribution_id.
-   */
-  public function testPaymentUpdateMissingContributionId() {
-    $params = [
-      'participant_id' => $this->_participantID,
-    ];
-    $participantPayment = $this->callAPIFailure('participant_payment', 'create', $params);
-  }
-
   /**
    * Check financial records for offline Participants.
    */
@@ -251,15 +199,6 @@ class api_v3_ParticipantPaymentTest extends CiviUnitTestCase {
     $this->callAPISuccess('participant_payment', 'delete', $params);
   }
 
-  /**
-   * Check with empty array.
-   */
-  public function testPaymentDeleteWithEmptyParams() {
-    $params = [];
-    $deletePayment = $this->callAPIFailure('participant_payment', 'delete', $params);
-    $this->assertEquals('Mandatory key(s) missing from params array: id', $deletePayment['error_message']);
-  }
-
   /**
    * Check with wrong id.
    */
index f9d20b6c0f00d9e2305d5edcfcf807f484ed62d1..a868a32fa2f32841b8991df4e006e9e77e5bc00b 100644 (file)
@@ -34,10 +34,9 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
   protected $_params;
 
   public function setUp(): void {
-    $this->_apiversion = 3;
     parent::setUp();
     $this->_entity = 'participant';
-    $event = $this->eventCreate(NULL);
+    $event = $this->eventCreate();
     $this->_eventID = $event['id'];
 
     $this->_contactID = $this->individualCreate();
@@ -80,12 +79,13 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
     ];
     // true tells quickCleanup to drop any tables that might have been created in the test
     $this->quickCleanup($tablesToTruncate, TRUE);
+    parent::tearDown();
   }
 
   /**
    * Check that getCount can count past 25.
    */
-  public function testGetCountLimit() {
+  public function testGetCountLimit(): void {
     $contactIDs = [];
 
     for ($count = $this->callAPISuccessGetCount('Participant', []); $count < 27; $count++) {
@@ -102,7 +102,7 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
   /**
    * Test get participants with role_id.
    */
-  public function testGetParticipantWithRole() {
+  public function testGetParticipantWithRole(): void {
     $roleId = [1, 2, 3];
     foreach ($roleId as $role) {
       $this->participantCreate([
@@ -118,7 +118,7 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
     $result = $this->callAPISuccess('participant', 'get', $params);
     //Assert all the returned participants has a role_id of 2
     foreach ($result['values'] as $pid => $values) {
-      $this->assertEquals($values['participant_role_id'], 2);
+      $this->assertEquals(2, $values['participant_role_id']);
     }
 
     $this->participantCreate([
@@ -131,8 +131,8 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
       'IS NULL' => 1,
     ];
     $result = $this->callAPISuccess('participant', 'get', $params);
-    foreach ($result['values'] as $pid => $values) {
-      $this->assertEquals($values['participant_role_id'], NULL);
+    foreach ($result['values'] as $values) {
+      $this->assertEquals(NULL, $values['participant_role_id']);
     }
 
   }
@@ -143,18 +143,18 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
    * variables specific to participant so it can be replicated into other entities
    * and / or moved to the automated test suite
    */
-  public function testCreateWithCustom() {
+  public function testCreateWithCustom(): void {
     $ids = $this->entityCustomGroupWithSingleFieldCreate(__FUNCTION__, __FILE__);
 
     $params = $this->_params;
-    $params['custom_' . $ids['custom_field_id']] = "custom string";
+    $params['custom_' . $ids['custom_field_id']] = 'custom string';
 
     $result = $this->callAPIAndDocument($this->_entity, 'create', $params, __FUNCTION__, __FILE__);
 
     $this->assertEquals($result['id'], $result['values'][$result['id']]['id']);
 
     $check = $this->callAPISuccess($this->_entity, 'get', ['id' => $result['id']]);
-    $this->assertEquals("custom string", $check['values'][$check['id']]['custom_' . $ids['custom_field_id']], ' in line ' . __LINE__);
+    $this->assertEquals('custom string', $check['values'][$check['id']]['custom_' . $ids['custom_field_id']], ' in line ' . __LINE__);
 
     $this->customFieldDelete($ids['custom_field_id']);
     $this->customGroupDelete($ids['custom_group_id']);
@@ -176,8 +176,8 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
     $result = $this->callAPISuccess('participant', 'get', $params);
     $this->assertAPISuccess($result, " in line " . __LINE__);
     $this->assertEquals($result['values'][$this->_participantID]['event_id'], $this->_eventID);
-    $this->assertEquals($result['values'][$this->_participantID]['participant_register_date'], '2007-02-19 00:00:00');
-    $this->assertEquals($result['values'][$this->_participantID]['participant_source'], 'Wimbeldon');
+    $this->assertEquals('2007-02-19 00:00:00', $result['values'][$this->_participantID]['participant_register_date']);
+    $this->assertEquals('Wimbeldon', $result['values'][$this->_participantID]['participant_source']);
     $params = [
       'id' => $this->_participantID,
       'return' => 'id,participant_register_date,event_id',
@@ -229,38 +229,38 @@ class api_v3_ParticipantTest extends CiviUnitTestCase {
   /**
    * Check with params id.
    */
-  public function testGetNestedEventGet() {
+  public function testGetNestedEventGet(): void {
     //create a second event & add participant to it.
-    $event = $this->eventCreate(NULL);
-    $this->callAPISuccess('participant', 'create', [
+    $event = $this->eventCreate();
+    $this->callAPISuccess('Participant', 'create', [
       'event_id' => $event['id'],
       'contact_id' => $this->_contactID,
     ]);
 
-    $description = "Demonstrates use of nested get to fetch event data with participant records.";
-    $subfile = "NestedEventGet";
+    $description = 'Demonstrates use of nested get to fetch event data with participant records.';
+    $subfile = 'NestedEventGet';
     $params = [
       'id' => $this->_participantID,
       'api.event.get' => 1,
     ];
-    $result = $this->callAPIAndDocument('participant', 'get', $params, __FUNCTION__, __FILE__, $description, $subfile);
-    $this->assertEquals($result['values'][$this->_participantID]['event_id'], $this->_eventID);
-    $this->assertEquals($result['values'][$this->_participantID]['participant_register_date'], '2007-02-19 00:00:00');
-    $this->assertEquals($result['values'][$this->_participantID]['participant_source'], 'Wimbeldon');
-    $this->assertEquals($this->_eventID, $result['values'][$this->_participantID]['api.event.get']['id']);
+    $result = $this->callAPIAndDocument('participant', 'get', $params, __FUNCTION__, __FILE__, $description, $subfile)['values'];
+    $this->assertEquals($this->_eventID, $result[$this->_participantID]['event_id']);
+    $this->assertEquals('2007-02-19 00:00:00', $result[$this->_participantID]['participant_register_date']);
+    $this->assertEquals('Wimbeldon', $result[$this->_participantID]['participant_source']);
+    $this->assertEquals($this->_eventID, $result[$this->_participantID]['api.event.get']['id']);
   }
 
   /**
    * Check Participant Get respects return properties.
    */
-  public function testGetWithReturnProperties() {
+  public function testGetWithReturnProperties(): void {
     $params = [
       'contact_id' => $this->_contactID,
       'return.status_id' => 1,
       'return.participant_status_id' => 1,
       'options' => ['limit' => 1],
     ];
-    $result = $this->callAPISuccess('participant', 'get', $params);
+    $result = $this->callAPISuccess('Participant', 'get', $params);
     $this->assertArrayHasKey('participant_status_id', $result['values'][$result['id']]);
   }
 
index 57d6b0ab851381a8d11622c7208c8216ba58e29b..de323c8c544ff730bd90c15384d10a17ede62beb 100644 (file)
@@ -50,17 +50,21 @@ class ContactApiKeyTest extends \api\v4\UnitTestCase {
     $result = Contact::get()
       ->addWhere('id', '=', $contact['id'])
       ->addSelect('api_key')
+      ->addSelect('IF((api_key IS NULL), "yes", "no") AS is_api_key_null')
       ->execute()
       ->first();
     $this->assertEquals($key, $result['api_key']);
+    $this->assertEquals('no', $result['is_api_key_null']);
     $this->assertFalse($isSafe($result), "Should reveal secret details ($key): " . var_export($result, 1));
 
     // Can also be fetched via join
     $email = Email::get()
       ->addSelect('contact_id.api_key')
+      ->addSelect('IF((contact_id.api_key IS NULL), "yes", "no") AS is_api_key_null')
       ->addWhere('id', '=', $contact['email']['id'])
       ->execute()->first();
     $this->assertEquals($key, $email['contact_id.api_key']);
+    $this->assertEquals('no', $result['is_api_key_null']);
     $this->assertFalse($isSafe($email), "Should reveal secret details ($key): " . var_export($email, 1));
 
     // Remove permission and we should not see the key
@@ -68,20 +72,24 @@ class ContactApiKeyTest extends \api\v4\UnitTestCase {
     $result = Contact::get()
       ->addWhere('id', '=', $contact['id'])
       ->addSelect('api_key')
+      ->addSelect('IF((api_key IS NULL), "yes", "no") AS is_api_key_null')
       ->setDebug(TRUE)
       ->execute();
     $this->assertContains('api_key', $result->debug['unauthorized_fields']);
     $this->assertArrayNotHasKey('api_key', $result[0]);
+    $this->assertArrayNotHasKey('is_api_key_null', $result[0]);
     $this->assertTrue($isSafe($result[0]), "Should NOT reveal secret details ($key): " . var_export($result[0], 1));
 
     // Also not available via join
     $email = Email::get()
       ->addSelect('contact_id.api_key')
+      ->addSelect('IF((contact_id.api_key IS NULL), "yes", "no") AS is_api_key_null')
       ->addWhere('id', '=', $contact['email']['id'])
       ->setDebug(TRUE)
       ->execute();
     $this->assertContains('contact_id.api_key', $email->debug['unauthorized_fields']);
     $this->assertArrayNotHasKey('contact_id.api_key', $email[0]);
+    $this->assertArrayNotHasKey('is_api_key_null', $result[0]);
     $this->assertTrue($isSafe($email[0]), "Should NOT reveal secret details ($key): " . var_export($email[0], 1));
 
     $result = Contact::get()
index 1de05f18e2043e059a9aaa0a9916b3b668df4ec7..b5fb0a15f68eaf37a6260ccb564b677598adaab1 100644 (file)
@@ -21,6 +21,7 @@ namespace api\v4\Action;
 
 use api\v4\UnitTestCase;
 use Civi\Api4\Contact;
+use Civi\Api4\Email;
 
 /**
  * @group headless
@@ -95,4 +96,31 @@ class SqlExpressionTest extends UnitTestCase {
       ->execute();
   }
 
+  public function testSelectEquations() {
+    $contact = Contact::create(FALSE)->addValue('first_name', 'bob')
+      ->addChain('email', Email::create()->setValues(['email' => 'hello@example.com', 'contact_id' => '$id']))
+      ->execute()->first();
+    $result = Email::get(FALSE)
+      ->setSelect([
+        'IF((contact_id.first_name = "bob"), "Yes", "No") AS is_bob',
+        'IF((contact_id.first_name != "fred"), "No", "Yes") AS is_fred',
+        '(5 * 11)',
+        '(5 > 11) AS five_greater_eleven',
+        '(5 <= 11) AS five_less_eleven',
+        '(1 BETWEEN 0 AND contact_id) AS is_between',
+        '(illegal * stuff) AS illegal_stuff',
+      ])
+      ->addWhere('contact_id', '=', $contact['id'])
+      ->setLimit(1)
+      ->execute()
+      ->first();
+    $this->assertEquals('Yes', $result['is_bob']);
+    $this->assertEquals('No', $result['is_fred']);
+    $this->assertEquals('55', $result['5_11']);
+    $this->assertFalse($result['five_greater_eleven']);
+    $this->assertTrue($result['five_less_eleven']);
+    $this->assertTrue($result['is_between']);
+    $this->assertArrayNotHasKey('illegal_stuff', $result);
+  }
+
 }