4 +--------------------------------------------------------------------+
5 | Copyright CiviCRM LLC. All rights reserved. |
7 | This work is published under the GNU AGPLv3 license with some |
8 | permitted exceptions and without any warranty. For full license |
9 | and copyright information, see https://civicrm.org/licensing |
10 +--------------------------------------------------------------------+
13 use Civi\Token\AbstractTokenSubscriber
;
14 use Civi\Token\Event\TokenRegisterEvent
;
15 use Civi\Token\Event\TokenValueEvent
;
16 use Civi\Token\TokenRow
;
17 use Civi\ActionSchedule\Event\MailingQueryEvent
;
18 use Civi\Token\TokenProcessor
;
21 * Class CRM_Core_EntityTokens
23 * Parent class for generic entity token functionality.
25 * WARNING - this class is highly likely to be temporary and
26 * to be consolidated with the TokenTrait and / or the
27 * AbstractTokenSubscriber in future. It is being used to clarify
28 * functionality but should NOT be used from outside of core tested code.
30 class CRM_Core_EntityTokens
extends AbstractTokenSubscriber
{
33 * Metadata about all tokens.
37 protected $tokensMetadata = [];
41 protected $prefetch = [];
44 * Should permissions be checked when loading tokens.
48 protected $checkPermissions = FALSE;
51 * Register the declared tokens.
53 * @param \Civi\Token\Event\TokenRegisterEvent $e
54 * The registration event. Add new tokens using register().
56 public function registerTokens(TokenRegisterEvent
$e) {
57 if (!$this->checkActive($e->getTokenProcessor())) {
60 foreach ($this->getTokenMetadata() as $tokenName => $field) {
61 if ($field['audience'] === 'user') {
63 'entity' => $this->entity
,
64 'field' => $tokenName,
65 'label' => $field['title'],
72 * Get the metadata about the available tokens
76 protected function getTokenMetadata(): array {
77 if (empty($this->tokensMetadata
)) {
78 $cacheKey = $this->getCacheKey();
79 if (Civi
::cache('metadata')->has($cacheKey)) {
80 $this->tokensMetadata
= Civi
::cache('metadata')->get($cacheKey);
83 $this->tokensMetadata
= $this->getBespokeTokens();
84 foreach ($this->getFieldMetadata() as $field) {
85 $this->addFieldToTokenMetadata($field, $this->getExposedFields());
87 foreach ($this->getHiddenTokens() as $name) {
88 $this->tokensMetadata
[$name]['audience'] = 'hidden';
90 Civi
::cache('metadata')->set($cacheKey, $this->tokensMetadata
);
93 return $this->tokensMetadata
;
98 * @throws \CRM_Core_Exception
100 public function evaluateToken(TokenRow
$row, $entity, $field, $prefetch = NULL) {
101 $this->prefetch
= (array) $prefetch;
102 $fieldValue = $this->getFieldValue($row, $field);
103 if (is_array($fieldValue)) {
104 // eg. role_id for participant would be an array here.
105 $fieldValue = implode(',', $fieldValue);
108 if ($this->isPseudoField($field)) {
109 if (!empty($fieldValue)) {
110 // If it's set here it has already been loaded in pre-fetch.
111 return $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue);
113 // Once prefetch is fully standardised we can remove this - as long
114 // as tests pass we should be fine as tests cover this.
115 $split = explode(':', $field);
116 return $row->tokens($entity, $field, $this->getPseudoValue($split[0], $split[1], $this->getFieldValue($row, $split[0])));
118 if ($this->isCustomField($field)) {
119 $prefetchedValue = $this->getCustomFieldValue($this->getFieldValue($row, 'id'), $field);
120 if ($prefetchedValue) {
121 return $row->format('text/html')->tokens($entity, $field, $prefetchedValue);
123 return $row->customToken($entity, \CRM_Core_BAO_CustomField
::getKeyID($field), $this->getFieldValue($row, 'id'));
125 if ($this->isMoneyField($field)) {
126 return $row->format('text/plain')->tokens($entity, $field,
127 \CRM_Utils_Money
::format($fieldValue, $this->getCurrency($row)));
129 if ($this->isDateField($field)) {
131 return $row->format('text/plain')
132 ->tokens($entity, $field, ($fieldValue ?
new DateTime($fieldValue) : $fieldValue));
134 catch (Exception
$e) {
135 Civi
::log()->info('invalid date token');
138 $row->format('text/plain')->tokens($entity, $field, (string) $fieldValue);
142 * Metadata about the entity fields.
146 protected $fieldMetadata = [];
149 * Get the entity name for api v4 calls.
153 protected function getApiEntityName(): string {
158 * Get the entity alias to use within queries.
160 * The default has a double underscore which should prevent any
161 * ambiguity with an existing table name.
165 protected function getEntityAlias(): string {
166 return $this->getApiEntityName() . '__';
170 * Get the name of the table this token class can extend.
172 * The default is based on the entity but some token classes,
173 * specifically the event class, latch on to other tables - ie
174 * the participant table.
176 public function getExtendableTableName(): string {
177 return CRM_Core_DAO_AllCoreTables
::getTableForEntityName($this->getApiEntityName());
181 * Get an array of fields to be requested.
183 * @todo this function should look up tokenMetadata that
188 protected function getReturnFields(): array {
189 return array_keys($this->getBasicTokens());
193 * Is the given field a boolean field.
195 * @param string $fieldName
199 protected function isBooleanField(string $fieldName): bool {
200 return $this->getMetadataForField($fieldName)['data_type'] === 'Boolean';
204 * Is the given field a date field.
206 * @param string $fieldName
210 protected function isDateField(string $fieldName): bool {
211 return in_array($this->getMetadataForField($fieldName)['data_type'], ['Timestamp', 'Date'], TRUE);
215 * Is the given field a pseudo field.
217 * @param string $fieldName
221 protected function isPseudoField(string $fieldName): bool {
222 return strpos($fieldName, ':') !== FALSE;
226 * Is the given field a custom field.
228 * @param string $fieldName
232 protected function isCustomField(string $fieldName) : bool {
233 return (bool) \CRM_Core_BAO_CustomField
::getKeyID($fieldName);
237 * Is the given field a date field.
239 * @param string $fieldName
243 protected function isMoneyField(string $fieldName): bool {
244 return $this->getMetadataForField($fieldName)['data_type'] === 'Money';
248 * Get the metadata for the available fields.
252 protected function getFieldMetadata(): array {
253 if (empty($this->fieldMetadata
)) {
255 // Tests fail without checkPermissions = FALSE
256 $this->fieldMetadata
= (array) civicrm_api4($this->getApiEntityName(), 'getfields', ['checkPermissions' => FALSE], 'name');
258 catch (API_Exception
$e) {
259 $this->fieldMetadata
= [];
262 return $this->fieldMetadata
;
266 * Get any tokens with custom calculation.
268 protected function getBespokeTokens(): array {
273 * Get the value for the relevant pseudo field.
275 * @param string $realField e.g contribution_status_id
276 * @param string $pseudoKey e.g name
277 * @param int|string $fieldValue e.g 1
280 * Eg. 'Completed' in the example above.
282 * @internal function will likely be protected soon.
284 protected function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string {
285 $bao = CRM_Core_DAO_AllCoreTables
::getFullName($this->getMetadataForField($realField)['entity']);
286 if ($pseudoKey === 'name') {
287 $fieldValue = (string) CRM_Core_PseudoConstant
::getName($bao, $realField, $fieldValue);
289 if ($pseudoKey === 'label') {
290 $fieldValue = (string) CRM_Core_PseudoConstant
::getLabel($bao, $realField, $fieldValue);
292 if ($pseudoKey === 'abbr' && $realField === 'state_province_id') {
293 // hack alert - currently only supported for state.
294 $fieldValue = (string) CRM_Core_PseudoConstant
::stateProvinceAbbreviation($fieldValue);
296 return (string) $fieldValue;
300 * @param \Civi\Token\TokenRow $row
301 * @param string $field
304 protected function getFieldValue(TokenRow
$row, string $field) {
305 $entityName = $this->getEntityName();
306 if (isset($row->context
[$entityName][$field])) {
307 return $row->context
[$entityName][$field];
310 $actionSearchResult = $row->context
['actionSearchResult'];
311 $aliasedField = $this->getEntityAlias() . $field;
312 if (isset($actionSearchResult->{$aliasedField})) {
313 return $actionSearchResult->{$aliasedField};
315 $entityID = $row->context
[$this->getEntityIDField()];
316 if ($field === 'id') {
319 return $this->prefetch
[$entityID][$field] ??
'';
325 public function __construct() {
326 parent
::__construct($this->getEntityName(), []);
330 * Check if the token processor is active.
332 * @param \Civi\Token\TokenProcessor $processor
336 public function checkActive(TokenProcessor
$processor) {
337 return (!empty($processor->context
['actionMapping'])
338 // This makes the 'schema context compulsory - which feels accidental
339 // since recent discu
340 && $processor->context
['actionMapping']->getEntity()) ||
in_array($this->getEntityIDField(), $processor->context
['schema']);
344 * Alter action schedule query.
346 * @param \Civi\ActionSchedule\Event\MailingQueryEvent $e
348 public function alterActionScheduleQuery(MailingQueryEvent
$e): void
{
349 if ($e->mapping
->getEntity() !== $this->getExtendableTableName()) {
352 foreach ($this->getReturnFields() as $token) {
353 $e->query
->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token);
358 * Get tokens to be suppressed from the widget.
360 * Note this is expected to be an interim function. Now we are no
361 * longer working around the parent function we can just define them once...
362 * with metadata, in a future refactor.
364 protected function getHiddenTokens(): array {
369 * @todo remove this function & use the metadata that is loaded.
372 * @throws \API_Exception
374 protected function getBasicTokens(): array {
376 foreach ($this->getExposedFields() as $fieldName) {
377 // Custom fields are still added v3 style - we want to keep v4 naming 'unpoluted'
378 // for now to allow us to consider how to handle names vs labels vs values
379 // and other raw vs not raw options.
380 if ($this->getFieldMetadata()[$fieldName]['type'] !== 'Custom') {
381 $return[$fieldName] = $this->getFieldMetadata()[$fieldName]['title'];
388 * Get entity fields that should be exposed as tokens.
393 protected function getExposedFields(): array {
395 foreach ($this->getFieldMetadata() as $field) {
396 if (!in_array($field['name'], $this->getSkippedFields(), TRUE)) {
397 $return[] = $field['name'];
404 * Get entity fields that should not be exposed as tokens.
408 protected function getSkippedFields(): array {
409 // tags is offered in 'case' & is one of the only fields that is
410 // 'not a real field' offered up by case - seems like an oddity
411 // we should skip at the top level for now.
413 if (!CRM_Campaign_BAO_Campaign
::isCampaignEnable()) {
414 $fields[] = 'campaign_id';
422 protected function getEntityName(): string {
423 return CRM_Core_DAO_AllCoreTables
::convertEntityNameToLower($this->getApiEntityName());
426 protected function getEntityIDField(): string {
427 return $this->getEntityName() . 'Id';
430 public function prefetch(TokenValueEvent
$e): ?
array {
431 $entityIDs = $e->getTokenProcessor()->getContextValues($this->getEntityIDField());
432 if (empty($entityIDs)) {
435 $select = $this->getPrefetchFields($e);
436 $result = (array) civicrm_api4($this->getApiEntityName(), 'get', [
437 'checkPermissions' => FALSE,
438 // Note custom fields are not yet added - I need to
439 // re-do the unit tests to support custom fields first.
441 'where' => [['id', 'IN', $entityIDs]],
446 protected function getCurrencyFieldName() {
451 * Get the currency to use for formatting money.
456 protected function getCurrency($row): string {
457 if (!empty($this->getCurrencyFieldName())) {
458 return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]);
460 return CRM_Core_Config
::singleton()->defaultCurrency
;
464 * Get the fields required to prefetch the entity.
466 * @param \Civi\Token\Event\TokenValueEvent $e
469 * @throws \API_Exception
471 public function getPrefetchFields(TokenValueEvent
$e): array {
472 $allTokens = array_keys($this->getTokenMetadata());
473 $requiredFields = array_intersect($this->getActiveTokens($e), $allTokens);
474 if (empty($requiredFields)) {
477 $requiredFields = array_merge($requiredFields, array_intersect($allTokens, array_merge(['id'], $this->getCurrencyFieldName())));
478 foreach ($this->getDependencies() as $field => $required) {
479 if (in_array($field, $this->getActiveTokens($e), TRUE)) {
480 foreach ((array) $required as $key) {
481 $requiredFields[] = $key;
485 return $requiredFields;
489 * Get fields which need to be returned to render another token.
493 protected function getDependencies(): array {
498 * Get the apiv4 style custom field name.
504 protected function getCustomFieldName(int $id): string {
505 foreach ($this->getTokenMetadata() as $key => $field) {
506 if (($field['custom_field_id'] ??
NULL) === $id) {
514 * @param string $field eg. 'custom_1'
516 * @return array|string|void|null $mixed
518 * @throws \CRM_Core_Exception
520 protected function getCustomFieldValue($entityID, string $field) {
521 $id = str_replace('custom_', '', $field);
522 $value = $this->prefetch
[$entityID][$this->getCustomFieldName($id)] ??
'';
523 if ($value !== NULL) {
524 return CRM_Core_BAO_CustomField
::displayValue($value, $id);
529 * Get the metadata for the field.
531 * @param string $fieldName
535 protected function getMetadataForField($fieldName): array {
536 if (isset($this->getTokenMetadata()[$fieldName])) {
537 return $this->getTokenMetadata()[$fieldName];
539 if (isset($this->getTokenMappingsForRelatedEntities()[$fieldName])) {
540 return $this->getTokenMetadata()[$this->getTokenMappingsForRelatedEntities()[$fieldName]];
542 return $this->getTokenMetadata()[$this->getDeprecatedTokens()[$fieldName]];
546 * Get token mappings for related entities - specifically the contact entity.
548 * This function exists to help manage the way contact tokens is structured
549 * of an query-object style result set that needs to be mapped to apiv4.
551 * The end goal is likely to be to advertised tokens that better map to api
552 * v4 and deprecate the existing ones but that is a long-term migration.
556 protected function getTokenMappingsForRelatedEntities(): array {
561 * Get array of deprecated tokens and the new token they map to.
565 protected function getDeprecatedTokens(): array {
570 * Get any overrides for token metadata.
572 * This is most obviously used for setting the audience, which
573 * will affect widget-presence.
575 * @return \string[][]
577 protected function getTokenMetadataOverrides(): array {
582 * To handle variable tokens, override this function and return the active tokens.
584 * @param \Civi\Token\Event\TokenValueEvent $e
588 public function getActiveTokens(TokenValueEvent
$e) {
589 $messageTokens = $e->getTokenProcessor()->getMessageTokens();
590 if (!isset($messageTokens[$this->entity
])) {
593 return array_intersect($messageTokens[$this->entity
], array_keys($this->getTokenMetadata()));
597 * Add the token to the metadata based on the field spec.
599 * @param array $field
600 * @param array $exposedFields
601 * @param string $prefix
603 protected function addFieldToTokenMetadata(array $field, array $exposedFields, string $prefix = ''): void
{
604 if ($field['type'] !== 'Custom' && !in_array($field['name'], $exposedFields, TRUE)) {
607 $field['audience'] = 'user';
608 if ($field['name'] === 'contact_id') {
609 // Since {contact.id} is almost always present don't confuse users
610 // by also adding (e.g {participant.contact_id)
611 $field['audience'] = 'sysadmin';
613 if (!empty($this->getTokenMetadataOverrides()[$field['name']])) {
614 $field = array_merge($field, $this->getTokenMetadataOverrides()[$field['name']]);
616 if ($field['type'] === 'Custom') {
617 // Convert to apiv3 style for now. Later we can add v4 with
618 // portable naming & support for labels/ dates etc so let's leave
619 // the space open for that.
620 // Not the existing quickform widget has handling for the custom field
621 // format based on the title using this syntax.
622 $parts = explode(': ', $field['label']);
623 $field['title'] = "{$parts[1]} :: {$parts[0]}";
624 $tokenName = 'custom_' . $field['custom_field_id'];
625 $this->tokensMetadata
[$tokenName] = $field;
628 $tokenName = $prefix ?
($prefix . '.' . $field['name']) : $field['name'];
629 if (in_array($field['name'], $exposedFields, TRUE)) {
631 ($field['options'] ||
!empty($field['suffixes']))
632 // At the time of writing currency didn't have a label option - this may have changed.
633 && !in_array($field['name'], $this->getCurrencyFieldName(), TRUE)
635 $this->tokensMetadata
[$tokenName . ':label'] = $this->tokensMetadata
[$tokenName . ':name'] = $field;
636 $fieldLabel = $field['input_attrs']['label'] ??
$field['label'];
637 $this->tokensMetadata
[$tokenName . ':label']['name'] = $field['name'] . ':label';
638 $this->tokensMetadata
[$tokenName . ':name']['name'] = $field['name'] . ':name';
639 $this->tokensMetadata
[$tokenName . ':name']['audience'] = 'sysadmin';
640 $this->tokensMetadata
[$tokenName . ':label']['title'] = $fieldLabel;
641 $this->tokensMetadata
[$tokenName . ':name']['title'] = ts('Machine name') . ': ' . $fieldLabel;
642 $field['audience'] = 'sysadmin';
644 if ($field['data_type'] === 'Boolean') {
645 $this->tokensMetadata
[$tokenName . ':label'] = $field;
646 $this->tokensMetadata
[$tokenName . ':label']['name'] = $field['name'] . ':label';
647 $field['audience'] = 'sysadmin';
649 $this->tokensMetadata
[$tokenName] = $field;
654 * Get a cache key appropriate to the current usage.
658 protected function getCacheKey(): string {
659 $cacheKey = __CLASS__
. 'token_metadata' . $this->getApiEntityName() . CRM_Core_Config
::domainID() . '_' . CRM_Core_I18n
::getLocale();
660 if ($this->checkPermissions
) {
661 $cacheKey .= '__' . CRM_Core_Session
::getLoggedInContactID();