// 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();
}
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'];
}
* @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);
$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([
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,
];
}
}
- return $ufValues[$ufID];
+ return Civi::$statics[__CLASS__][__FUNCTION__][$ufID] ?? NULL;
}
/**
* @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);
'email' => ts('Domain (organization) email'),
'id' => ts('Domain ID'),
'description' => ts('Domain Description'),
+ 'now' => ts('Current time/date'),
];
}
* @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]);
}
*/
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.
*
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;
}
/**
* 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)) {
}
$this->resetSchemaCacheForTable("log_$table");
-
- return TRUE;
}
/**
*
*/
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');
*
*/
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');
*
*/
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
*
*/
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'),
];
}
- return \Civi::$statics[__CLASS__]['fullMonthNames'];
+ return \Civi::$statics[__CLASS__][$key];
}
/**
--- /dev/null
+<?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;
+ }
+
+}
$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.');
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;
+ }
+
}
'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());
}
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
*
'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')],
* @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;
}
$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;
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");
}
// 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;
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
* 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
--- /dev/null
+# 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`.
+
--- /dev/null
+<?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);
+ }
+
+}
['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],
[]
);
$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']);
}
/**
$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.
*/
], $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);
+ }
+ }
+
}
*/
use Civi\Token\TokenProcessor;
+use Civi\Api4\LocBlock;
+use Civi\Api4\Email;
+use Civi\Api4\Phone;
+use Civi\Api4\Address;
/**
* CRM_Utils_TokenConsistencyTest
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) . '&reset=1&id=1
+' . CRM_Utils_System::url('civicrm/event/register', NULL, TRUE) . '&reset=1&id=1
+
+my field';
+ }
+
/**
* Get expected output from token parsing.
*
]);
$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.
*
'{domain.email}' => 'Domain (organization) email',
'{domain.id}' => ts('Domain ID'),
'{domain.description}' => ts('Domain Description'),
+ '{domain.now}' => 'Current time/date',
];
}
* 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(), [
'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())),
+ ]);
}
/**
'{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',
];
}
<?php
namespace Civi\Token;
+use Civi\Test\Invasive;
use Civi\Token\Event\TokenRegisterEvent;
use Civi\Token\Event\TokenValueEvent;
use Symfony\Component\EventDispatcher\EventDispatcher;
];
}
+ /**
+ * 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)".
*/
}
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;
* @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();
}
* 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([
$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,
'is_email_confirm' => 1,
], $params);
- return $this->callAPISuccess('Event', 'create', $params);
+ $event = $this->callAPISuccess('Event', 'create', $params);
+ $this->ids['event'][] = $event['id'];
+ return $event;
}
/**
* @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);
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 = [];
]);
}
- /**
- * 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]);
'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]);
$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.
*/
$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.
*/
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();
];
// 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++) {
/**
* Test get participants with role_id.
*/
- public function testGetParticipantWithRole() {
+ public function testGetParticipantWithRole(): void {
$roleId = [1, 2, 3];
foreach ($roleId as $role) {
$this->participantCreate([
$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([
'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']);
}
}
* 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']);
$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',
/**
* 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']]);
}
$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
$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()
use api\v4\UnitTestCase;
use Civi\Api4\Contact;
+use Civi\Api4\Email;
/**
* @group headless
->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);
+ }
+
}