X-Git-Url: https://vcs.fsf.org/?a=blobdiff_plain;f=CRM%2FCore%2FEntityTokens.php;h=8f7866971b5a5230c43acc062d5b08bea1896aa5;hb=96d9c94a6d9c49a84cd38ab3b3d68167b3274d47;hp=56c1428deb2bd5fae857e70c6c9ff912d25b6dd8;hpb=a1553dfccd17bc889bb7cfb81b480959122b8dfc;p=civicrm-core.git diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index 56c1428deb..8f7866971b 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -16,6 +16,7 @@ use Civi\Token\Event\TokenValueEvent; use Civi\Token\TokenRow; use Civi\ActionSchedule\Event\MailingQueryEvent; use Civi\Token\TokenProcessor; +use Brick\Money\Money; /** * Class CRM_Core_EntityTokens @@ -29,11 +30,24 @@ use Civi\Token\TokenProcessor; */ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { + /** + * Metadata about all tokens. + * + * @var array + */ + protected $tokensMetadata = []; /** * @var array */ protected $prefetch = []; + /** + * Should permissions be checked when loading tokens. + * + * @var bool + */ + protected $checkPermissions = FALSE; + /** * Register the declared tokens. * @@ -44,17 +58,37 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { if (!$this->checkActive($e->getTokenProcessor())) { return; } - foreach ($this->getAllTokens() as $name => $label) { - if (!in_array($name, $this->getHiddenTokens(), TRUE)) { + foreach ($this->getTokenMetadata() as $tokenName => $field) { + if ($field['audience'] === 'user') { $e->register([ 'entity' => $this->entity, - 'field' => $name, - 'label' => $label, + 'field' => $tokenName, + 'label' => $field['title'], ]); } } } + /** + * Get the metadata about the available tokens + * + * @return array + */ + protected function getTokenMetadata(): array { + $cacheKey = $this->getCacheKey(); + if (!Civi::cache('metadata')->has($cacheKey)) { + $tokensMetadata = $this->getBespokeTokens(); + foreach ($this->getFieldMetadata() as $field) { + $this->addFieldToTokenMetadata($tokensMetadata, $field, $this->getExposedFields()); + } + foreach ($this->getHiddenTokens() as $name) { + $tokensMetadata[$name]['audience'] = 'hidden'; + } + Civi::cache('metadata')->set($cacheKey, $tokensMetadata); + } + return Civi::cache('metadata')->get($cacheKey); + } + /** * @inheritDoc * @throws \CRM_Core_Exception @@ -85,13 +119,23 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return $row->customToken($entity, \CRM_Core_BAO_CustomField::getKeyID($field), $this->getFieldValue($row, 'id')); } if ($this->isMoneyField($field)) { + $currency = $this->getCurrency($row); + if (empty($fieldValue) && !is_numeric($fieldValue)) { + $fieldValue = 0; + } + if (!$currency) { + // too hard basket for now - just do what we always did. + return $row->format('text/plain')->tokens($entity, $field, + \CRM_Utils_Money::format($fieldValue, $currency)); + } return $row->format('text/plain')->tokens($entity, $field, - \CRM_Utils_Money::format($fieldValue, $this->getCurrency($row))); + Money::of($fieldValue, $currency)); + } if ($this->isDateField($field)) { try { return $row->format('text/plain') - ->tokens($entity, $field, new DateTime($fieldValue)); + ->tokens($entity, $field, ($fieldValue ? new DateTime($fieldValue) : $fieldValue)); } catch (Exception $e) { Civi::log()->info('invalid date token'); @@ -139,47 +183,18 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return CRM_Core_DAO_AllCoreTables::getTableForEntityName($this->getApiEntityName()); } - /** - * Get the relevant bao name. - */ - public function getBAOName(): string { - return CRM_Core_DAO_AllCoreTables::getFullName($this->getApiEntityName()); - } - /** * Get an array of fields to be requested. * + * @todo this function should look up tokenMetadata that + * is already loaded. + * * @return string[] */ - public function getReturnFields(): array { + protected function getReturnFields(): array { return array_keys($this->getBasicTokens()); } - /** - * Get all the tokens supported by this processor. - * - * @return array|string[] - * @throws \API_Exception - */ - protected function getAllTokens(): array { - $basicTokens = $this->getBasicTokens(); - foreach (array_keys($basicTokens) as $fieldName) { - // The goal is to be able to render more complete tokens - // (eg. actual booleans, field names, raw ids) for a more - // advanced audiences - ie those using conditionals - // and to specify that audience in the api that retrieves. - // But, for now, let's not advertise, given that most of these fields - // aren't really needed even once... - if ($this->isBooleanField($fieldName)) { - unset($basicTokens[$fieldName]); - } - } - foreach ($this->getBespokeTokens() as $token) { - $basicTokens[$token['name']] = $token['title']; - } - return array_merge($basicTokens, $this->getPseudoTokens(), CRM_Utils_Token::getCustomFieldTokens($this->getApiEntityName())); - } - /** * Is the given field a boolean field. * @@ -187,8 +202,8 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return bool */ - public function isBooleanField(string $fieldName): bool { - return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Boolean'; + protected function isBooleanField(string $fieldName): bool { + return $this->getMetadataForField($fieldName)['data_type'] === 'Boolean'; } /** @@ -198,8 +213,8 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return bool */ - public function isDateField(string $fieldName): bool { - return in_array($this->getFieldMetadata()[$fieldName]['data_type'], ['Timestamp', 'Date'], TRUE); + protected function isDateField(string $fieldName): bool { + return in_array($this->getMetadataForField($fieldName)['data_type'], ['Timestamp', 'Date'], TRUE); } /** @@ -209,7 +224,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return bool */ - public function isPseudoField(string $fieldName): bool { + protected function isPseudoField(string $fieldName): bool { return strpos($fieldName, ':') !== FALSE; } @@ -220,7 +235,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return bool */ - public function isCustomField(string $fieldName) : bool { + protected function isCustomField(string $fieldName) : bool { return (bool) \CRM_Core_BAO_CustomField::getKeyID($fieldName); } @@ -231,8 +246,8 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return bool */ - public function isMoneyField(string $fieldName): bool { - return $this->getFieldMetadata()[$fieldName]['data_type'] === 'Money'; + protected function isMoneyField(string $fieldName): bool { + return $this->getMetadataForField($fieldName)['data_type'] === 'Money'; } /** @@ -253,28 +268,6 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return $this->fieldMetadata; } - /** - * Get pseudoTokens - it tokens that reflect the name or label of a pseudoconstant. - * - * @internal - this function will likely be made protected soon. - * - * @return array - */ - public function getPseudoTokens(): array { - $return = []; - foreach (array_keys($this->getBasicTokens()) as $fieldName) { - if ($this->isAddPseudoTokens($fieldName)) { - $fieldLabel = $this->fieldMetadata[$fieldName]['input_attrs']['label'] ?? $this->fieldMetadata[$fieldName]['label']; - $return[$fieldName . ':label'] = $fieldLabel; - $return[$fieldName . ':name'] = ts('Machine name') . ': ' . $fieldLabel; - } - if ($this->isBooleanField($fieldName)) { - $return[$fieldName . ':label'] = $this->getFieldMetadata()[$fieldName]['title']; - } - } - return $return; - } - /** * Get any tokens with custom calculation. */ @@ -282,35 +275,6 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return []; } - /** - * Is this a field we should add pseudo-tokens to? - * - * Pseudo-tokens allow access to name and label fields - e.g - * - * {contribution.contribution_status_id:name} might resolve to 'Completed' - * - * @param string $fieldName - */ - public function isAddPseudoTokens($fieldName): bool { - if (in_array($fieldName, $this->getCurrencyFieldName())) { - // 'currency' is manually added to the skip list as an anomaly. - // name & label aren't that suitable for 'currency' (symbol, which - // possibly maps to 'abbr' would be) and we can't gather that - // from the metadata as yet. - return FALSE; - } - if ($this->getFieldMetadata()[$fieldName]['type'] === 'Custom') { - // If we remove this early return then we get that extra nuanced goodness - // and support for the more portable v4 style field names - // on custom fields - where labels or names can be returned. - // At present the gap is that the metadata for the label is not accessed - // and tests failed on the enotice and we don't have a clear plan about - // v4 style custom tokens - but medium term this IF will probably go. - return FALSE; - } - return (bool) ($this->getFieldMetadata()[$fieldName]['options'] || !empty($this->getFieldMetadata()[$fieldName]['suffixes'])); - } - /** * Get the value for the relevant pseudo field. * @@ -323,12 +287,17 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @internal function will likely be protected soon. */ - public function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { + protected function getPseudoValue(string $realField, string $pseudoKey, $fieldValue): string { + $bao = CRM_Core_DAO_AllCoreTables::getFullName($this->getMetadataForField($realField)['entity']); if ($pseudoKey === 'name') { - $fieldValue = (string) CRM_Core_PseudoConstant::getName($this->getBAOName(), $realField, $fieldValue); + $fieldValue = (string) CRM_Core_PseudoConstant::getName($bao, $realField, $fieldValue); } if ($pseudoKey === 'label') { - $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($this->getBAOName(), $realField, $fieldValue); + $fieldValue = (string) CRM_Core_PseudoConstant::getLabel($bao, $realField, $fieldValue); + } + if ($pseudoKey === 'abbr' && $realField === 'state_province_id') { + // hack alert - currently only supported for state. + $fieldValue = (string) CRM_Core_PseudoConstant::stateProvinceAbbreviation($fieldValue); } return (string) $fieldValue; } @@ -344,11 +313,6 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return $row->context[$entityName][$field]; } - $actionSearchResult = $row->context['actionSearchResult']; - $aliasedField = $this->getEntityAlias() . $field; - if (isset($actionSearchResult->{$aliasedField})) { - return $actionSearchResult->{$aliasedField}; - } $entityID = $row->context[$this->getEntityIDField()]; if ($field === 'id') { return $entityID; @@ -360,8 +324,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * Class constructor. */ public function __construct() { - $tokens = $this->getAllTokens(); - parent::__construct($this->getEntityName(), $tokens); + parent::__construct($this->getEntityName(), []); } /** @@ -387,9 +350,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { if ($e->mapping->getEntity() !== $this->getExtendableTableName()) { return; } - foreach ($this->getReturnFields() as $token) { - $e->query->select('e.' . $token . ' AS ' . $this->getEntityAlias() . $token); - } + $e->query->select('e.id AS tokenContext_' . $this->getEntityIDField()); } /** @@ -404,18 +365,12 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { } /** - * Get tokens supporting the syntax we are migrating to. - * - * In general these are tokens that were not previously supported - * so we can add them in the preferred way or that we have - * undertaken some, as yet to be written, db update. - * - * See https://lab.civicrm.org/dev/core/-/issues/2650 + * @todo remove this function & use the metadata that is loaded. * * @return string[] * @throws \API_Exception */ - public function getBasicTokens(): array { + protected function getBasicTokens(): array { $return = []; foreach ($this->getExposedFields() as $fieldName) { // Custom fields are still added v3 style - we want to keep v4 naming 'unpoluted' @@ -449,11 +404,11 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return string[] */ - public function getSkippedFields(): array { + protected function getSkippedFields(): array { // tags is offered in 'case' & is one of the only fields that is // 'not a real field' offered up by case - seems like an oddity // we should skip at the top level for now. - $fields = ['contact_id', 'tags']; + $fields = ['tags']; if (!CRM_Campaign_BAO_Campaign::isCampaignEnable()) { $fields[] = 'campaign_id'; } @@ -467,7 +422,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return CRM_Core_DAO_AllCoreTables::convertEntityNameToLower($this->getApiEntityName()); } - public function getEntityIDField(): string { + protected function getEntityIDField(): string { return $this->getEntityName() . 'Id'; } @@ -487,7 +442,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { return $result; } - public function getCurrencyFieldName() { + protected function getCurrencyFieldName() { return []; } @@ -497,7 +452,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return string */ - public function getCurrency($row): string { + protected function getCurrency($row): string { if (!empty($this->getCurrencyFieldName())) { return $this->getFieldValue($row, $this->getCurrencyFieldName()[0]); } @@ -513,7 +468,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * @throws \API_Exception */ public function getPrefetchFields(TokenValueEvent $e): array { - $allTokens = array_keys($this->getAllTokens()); + $allTokens = array_keys($this->getTokenMetadata()); $requiredFields = array_intersect($this->getActiveTokens($e), $allTokens); if (empty($requiredFields)) { return []; @@ -534,7 +489,7 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * * @return array */ - public function getDependencies(): array { + protected function getDependencies(): array { return []; } @@ -544,13 +499,18 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { * @param int $id * * @return string + * + * @throws \CRM_Core_Exception */ protected function getCustomFieldName(int $id): string { - foreach ($this->getFieldMetadata() as $key => $field) { + foreach ($this->getTokenMetadata() as $key => $field) { if (($field['custom_field_id'] ?? NULL) === $id) { return $key; } } + throw new CRM_Core_Exception( + "A custom field with the ID {$id} does not exist" + ); } /** @@ -563,10 +523,154 @@ class CRM_Core_EntityTokens extends AbstractTokenSubscriber { */ protected function getCustomFieldValue($entityID, string $field) { $id = str_replace('custom_', '', $field); - $value = $this->prefetch[$entityID][$this->getCustomFieldName($id)] ?? NULL; - if ($value !== NULL) { - return CRM_Core_BAO_CustomField::displayValue($value, $id); + try { + $value = $this->prefetch[$entityID][$this->getCustomFieldName($id)] ?? ''; + if ($value !== NULL) { + return CRM_Core_BAO_CustomField::displayValue($value, $id); + } + } + catch (CRM_Core_Exception $exception) { + return NULL; + } + } + + /** + * Get the metadata for the field. + * + * @param string $fieldName + * + * @return array + */ + protected function getMetadataForField($fieldName): array { + if (isset($this->getTokenMetadata()[$fieldName])) { + return $this->getTokenMetadata()[$fieldName]; + } + if (isset($this->getTokenMappingsForRelatedEntities()[$fieldName])) { + return $this->getTokenMetadata()[$this->getTokenMappingsForRelatedEntities()[$fieldName]]; + } + return $this->getTokenMetadata()[$this->getDeprecatedTokens()[$fieldName]] ?? []; + } + + /** + * Get token mappings for related entities - specifically the contact entity. + * + * This function exists to help manage the way contact tokens is structured + * of an query-object style result set that needs to be mapped to apiv4. + * + * The end goal is likely to be to advertised tokens that better map to api + * v4 and deprecate the existing ones but that is a long-term migration. + * + * @return array + */ + protected function getTokenMappingsForRelatedEntities(): array { + return []; + } + + /** + * Get array of deprecated tokens and the new token they map to. + * + * @return array + */ + protected function getDeprecatedTokens(): array { + return []; + } + + /** + * Get any overrides for token metadata. + * + * This is most obviously used for setting the audience, which + * will affect widget-presence. + * + * @return \string[][] + */ + protected function getTokenMetadataOverrides(): array { + return []; + } + + /** + * To handle variable tokens, override this function and return the active tokens. + * + * @param \Civi\Token\Event\TokenValueEvent $e + * + * @return mixed + */ + public function getActiveTokens(TokenValueEvent $e) { + $messageTokens = $e->getTokenProcessor()->getMessageTokens(); + if (!isset($messageTokens[$this->entity])) { + return FALSE; + } + return array_intersect($messageTokens[$this->entity], array_keys($this->getTokenMetadata())); + } + + /** + * Add the token to the metadata based on the field spec. + * + * @param array $tokensMetadata + * @param array $field + * @param array $exposedFields + * @param string $prefix + */ + protected function addFieldToTokenMetadata(array &$tokensMetadata, array $field, array $exposedFields, string $prefix = ''): void { + if ($field['type'] !== 'Custom' && !in_array($field['name'], $exposedFields, TRUE)) { + return; + } + $field['audience'] = 'user'; + if ($field['name'] === 'contact_id') { + // Since {contact.id} is almost always present don't confuse users + // by also adding (e.g {participant.contact_id) + $field['audience'] = 'sysadmin'; + } + if (!empty($this->getTokenMetadataOverrides()[$field['name']])) { + $field = array_merge($field, $this->getTokenMetadataOverrides()[$field['name']]); + } + if ($field['type'] === 'Custom') { + // Convert to apiv3 style for now. Later we can add v4 with + // portable naming & support for labels/ dates etc so let's leave + // the space open for that. + // Not the existing quickform widget has handling for the custom field + // format based on the title using this syntax. + $parts = explode(': ', $field['label']); + $field['title'] = "{$parts[1]} :: {$parts[0]}"; + $tokenName = 'custom_' . $field['custom_field_id']; + $tokensMetadata[$tokenName] = $field; + return; + } + $tokenName = $prefix ? ($prefix . '.' . $field['name']) : $field['name']; + if (in_array($field['name'], $exposedFields, TRUE)) { + if ( + ($field['options'] || !empty($field['suffixes'])) + // At the time of writing currency didn't have a label option - this may have changed. + && !in_array($field['name'], $this->getCurrencyFieldName(), TRUE) + ) { + $tokensMetadata[$tokenName . ':label'] = $tokensMetadata[$tokenName . ':name'] = $field; + $fieldLabel = $field['input_attrs']['label'] ?? $field['label']; + $tokensMetadata[$tokenName . ':label']['name'] = $field['name'] . ':label'; + $tokensMetadata[$tokenName . ':name']['name'] = $field['name'] . ':name'; + $tokensMetadata[$tokenName . ':name']['audience'] = 'sysadmin'; + $tokensMetadata[$tokenName . ':label']['title'] = $fieldLabel; + $tokensMetadata[$tokenName . ':name']['title'] = ts('Machine name') . ': ' . $fieldLabel; + $field['audience'] = 'sysadmin'; + } + if ($field['data_type'] === 'Boolean') { + $tokensMetadata[$tokenName . ':label'] = $field; + $tokensMetadata[$tokenName . ':label']['name'] = $field['name'] . ':label'; + $field['audience'] = 'sysadmin'; + } + $tokensMetadata[$tokenName] = $field; + } + } + + /** + * Get a cache key appropriate to the current usage. + * + * @return string + */ + protected function getCacheKey(): string { + $cacheKey = __CLASS__ . 'token_metadata' . $this->getApiEntityName() . CRM_Core_Config::domainID() . '_' . CRM_Core_I18n::getLocale(); + if ($this->checkPermissions) { + $cacheKey .= '__' . CRM_Core_Session::getLoggedInContactID(); } + return $cacheKey; } }