diff --git a/CRM/Contact/Tokens.php b/CRM/Contact/Tokens.php index 2c6bf924c920..4dc0cc9236f6 100644 --- a/CRM/Contact/Tokens.php +++ b/CRM/Contact/Tokens.php @@ -57,15 +57,12 @@ public function registerTokens(TokenRegisterEvent $e): void { if (!$this->checkActive($e->getTokenProcessor())) { return; } - $relatedTokens = array_flip($this->getTokenMappingsForRelatedEntities()); foreach ($this->getTokenMetadata() as $tokenName => $field) { if ($field['audience'] === 'user') { $e->register([ 'entity' => $this->entity, - // Preserve legacy token names. It generally feels like - // it would be good to switch to the more specific token names - // but other code paths are still in use which can't handle them. - 'field' => $relatedTokens[$tokenName] ?? $tokenName, + // We advertise the new-style token names - but support legacy ones. + 'field' => $tokenName, 'label' => $field['title'], ]); } @@ -398,19 +395,31 @@ protected function getTokenMetadata(): array { } $metadata = (array) civicrm_api4($apiEntity, 'getfields', ['checkPermissions' => FALSE], 'name'); foreach ($metadata as $field) { - $this->addFieldToTokenMetadata($tokensMetadata, $field, $exposedFields, 'primary_' . $entity); + if ($entity === 'website') { + // It's not the primary - it's 'just one of them' - so the name is _first not _primary + $this->addFieldToTokenMetadata($tokensMetadata, $field, $exposedFields, 'website_first'); + } + else { + $this->addFieldToTokenMetadata($tokensMetadata, $field, $exposedFields, $entity . '_primary'); + $field['label'] .= ' (' . ts('Billing') . ')'; + // Set audience to sysadmin in case adding them to UI annoys people. If people ask to see this + // in the UI we could set to 'user'. + $field['audience'] = 'sysadmin'; + $this->addFieldToTokenMetadata($tokensMetadata, $field, $exposedFields, $entity . '_billing'); + } } } // Manually add in the abbreviated state province as that maps to // what has traditionally been delivered. - $tokensMetadata['primary_address.state_province_id:abbr'] = $tokensMetadata['primary_address.state_province_id:label']; - $tokensMetadata['primary_address.state_province_id:abbr']['name'] = 'state_province_id:abbr'; - $tokensMetadata['primary_address.state_province_id:abbr']['audience'] = 'user'; + $tokensMetadata['address_primary.state_province_id:abbr'] = $tokensMetadata['address_primary.state_province_id:label']; + $tokensMetadata['address_primary.state_province_id:abbr']['name'] = 'state_province_id:abbr'; + $tokensMetadata['address_primary.state_province_id:abbr']['audience'] = 'user'; // Hide the label for now because we are not sure if there are paths // where legacy token resolution is in play where this could not be resolved. - $tokensMetadata['primary_address.state_province_id:label']['audience'] = 'sysadmin'; + $tokensMetadata['address_primary.state_province_id:label']['audience'] = 'sysadmin'; // Hide this really obscure one. Just cos it annoys me. - $tokensMetadata['primary_address.manual_geo_code:label']['audience'] = 'sysadmin'; + $tokensMetadata['address_primary.manual_geo_code:label']['audience'] = 'sysadmin'; + $tokensMetadata['openid_primary.openid']['audience'] = 'sysadmin'; Civi::cache('metadata')->set($this->getCacheKey(), $tokensMetadata); return $tokensMetadata; } @@ -441,13 +450,20 @@ protected function getContact(int $contactId, array $requiredFields, bool $getAl $fieldSpec = $this->getMetadataForField($field); $prefix = ''; if (isset($fieldSpec['table_name']) && $fieldSpec['table_name'] !== 'civicrm_contact') { - $tableAlias = str_replace('civicrm_', 'primary_', $fieldSpec['table_name']); - $joins[$tableAlias] = $fieldSpec['entity']; - - $prefix = $tableAlias . '.'; - } - if ($fieldSpec['type'] === 'Custom') { - $customFields['custom_' . $fieldSpec['custom_field_id']] = $fieldSpec['name']; + if ($fieldSpec['table_name'] === 'civicrm_website') { + $tableAlias = 'website_first'; + $joins[$tableAlias] = $fieldSpec['entity']; + $prefix = $tableAlias . '.'; + } + if ($fieldSpec['table_name'] === 'civicrm_openid') { + // We could start to deprecate this one maybe..... I've made it un-advertised. + $tableAlias = 'openid_primary'; + $joins[$tableAlias] = $fieldSpec['entity']; + $prefix = $tableAlias . '.'; + } + if ($fieldSpec['type'] === 'Custom') { + $customFields['custom_' . $fieldSpec['custom_field_id']] = $fieldSpec['name']; + } } $returnProperties[] = $prefix . $this->getMetadataForField($field)['name']; } @@ -534,65 +550,81 @@ protected function getDeprecatedTokens(): array { /** * Get the tokens that are accessed by joining onto a related entity. * - * Note the original thinking was to migrate to advertising the tokens - * that more accurately reflect the schema & also add support for e.g - * billing_address.street_address - which would be hugely useful for workflow - * message templates. + * This is an array of legacy style tokens mapped to the new style - so that + * discontinued tokens still work (although they are no longer advertised). + * + * There are three types of legacy tokens + * - apiv3 style - e.g {contact.email} + * - ad hoc - hey cos it's CiviCRM + * - 'wrong' apiv4 style - ie I thought we would do 'primary_address' but we did + * 'address_primary' - these were added as the 'real token names' but not + * advertised & likely never adopted so handling them for a while is a + * conservative approach. * - * However that feels like a bridge too far for this round - * since we haven't quite hit the goal of all token processing going through - * the token processor & we risk advertising tokens that don't work if we get - * ahead of that process. + * The new type maps to the v4 api. * * @return string[] */ protected function getTokenMappingsForRelatedEntities(): array { - return [ - 'on_hold' => 'primary_email.on_hold', - 'on_hold:label' => 'primary_email.on_hold:label', - 'phone_type_id' => 'primary_phone.phone_type_id', - 'phone_type_id:label' => 'primary_phone.phone_type_id:label', + $legacyFieldMapping = [ + 'on_hold' => 'email_primary.on_hold:label', + 'phone_type_id' => 'phone_primary.phone_type_id', + 'phone_type_id:label' => 'phone_primary.phone_type_id:label', + 'phone_type' => 'phone_primary.phone_type_id:label', + 'phone' => 'phone_primary.phone', + 'primary_phone.phone' => 'phone_primary.phone', + 'phone_ext' => 'phone_primary.phone_ext', + 'primary_phone.phone_ext' => 'phone_primary.phone_ext', 'current_employer' => 'employer_id.display_name', - 'location_type_id' => 'primary_address.location_type_id', - 'location_type' => 'primary_address.location_type_id:label', - 'location_type_id:label' => 'primary_address.location_type_id:label', - 'street_address' => 'primary_address.street_address', - 'address_id' => 'primary_address.id', - 'address_name' => 'primary_address.name', - 'street_number' => 'primary_address.street_number', - 'street_number_suffix' => 'primary_address.street_number_suffix', - 'street_name' => 'primary_address.street_name', - 'street_unit' => 'primary_address.street_unit', - 'supplemental_address_1' => 'primary_address.supplemental_address_1', - 'supplemental_address_2' => 'primary_address.supplemental_address_2', - 'supplemental_address_3' => 'primary_address.supplemental_address_3', - 'city' => 'primary_address.city', - 'postal_code' => 'primary_address.postal_code', - 'postal_code_suffix' => 'primary_address.postal_code_suffix', - 'geo_code_1' => 'primary_address.geo_code_1', - 'geo_code_2' => 'primary_address.geo_code_2', - 'manual_geo_code' => 'primary_address.manual_geo_code', - 'master_id' => 'primary_address.master_id', - 'county' => 'primary_address.county_id:label', - 'county_id' => 'primary_address.county_id', - 'state_province' => 'primary_address.state_province_id:abbr', - 'state_province_id' => 'primary_address.state_province_id', - 'country' => 'primary_address.country_id:label', - 'country_id' => 'primary_address.country_id', - 'world_region' => 'primary_address.country_id.region_id:name', - 'phone_type' => 'primary_phone.phone_type_id:label', - 'phone' => 'primary_phone.phone', - 'phone_ext' => 'primary_phone.phone_ext', - 'email' => 'primary_email.email', - 'signature_text' => 'primary_email.signature_text', - 'signature_html' => 'primary_email.signature_html', - 'im' => 'primary_im.name', - 'im_provider' => 'primary_im.provider_id', - 'provider_id:label' => 'primary_im.provider_id:label', - 'provider_id' => 'primary_im.provider_id', - 'openid' => 'primary_openid.openid', - 'url' => 'primary_website.url', + 'location_type_id' => 'address_primary.location_type_id', + 'location_type' => 'address_primary.location_type_id:label', + 'location_type_id:label' => 'address_primary.location_type_id:label', + 'street_address' => 'address_primary.street_address', + 'address_id' => 'address_primary.id', + 'address_name' => 'address_primary.name', + 'street_number' => 'address_primary.street_number', + 'street_number_suffix' => 'address_primary.street_number_suffix', + 'street_name' => 'address_primary.street_name', + 'street_unit' => 'address_primary.street_unit', + 'supplemental_address_1' => 'address_primary.supplemental_address_1', + 'supplemental_address_2' => 'address_primary.supplemental_address_2', + 'supplemental_address_3' => 'address_primary.supplemental_address_3', + 'city' => 'address_primary.city', + 'postal_code' => 'address_primary.postal_code', + 'postal_code_suffix' => 'address_primary.postal_code_suffix', + 'geo_code_1' => 'address_primary.geo_code_1', + 'geo_code_2' => 'address_primary.geo_code_2', + 'manual_geo_code' => 'address_primary.manual_geo_code', + 'master_id' => 'address_primary.master_id', + 'county' => 'address_primary.county_id:label', + 'county_id' => 'address_primary.county_id', + 'state_province' => 'address_primary.state_province_id:abbr', + 'state_province_id' => 'address_primary.state_province_id', + 'country' => 'address_primary.country_id:label', + 'country_id' => 'address_primary.country_id', + 'world_region' => 'address_primary.country_id.region_id:name', + 'email' => 'email_primary.email', + 'signature_text' => 'email_primary.signature_text', + 'signature_html' => 'email_primary.signature_html', + 'im' => 'im_primary.name', + 'im_provider' => 'im_primary.provider_id:label', + 'openid' => 'openid_primary.openid', + 'url' => 'website_first.url', ]; + foreach ($legacyFieldMapping as $fieldName) { + // Add in our briefly-used 'primary_address' variants. + // ie add 'primary_email.email' => 'email_primary.email' + // so allow the former to be mapped to the latter. + // We can deprecate these out later as they were likely never adopted. + $oldPrimaryName = str_replace( + ['email_primary', 'im_primary', 'phone_primary', 'address_primary', 'openid_primary', 'website_first'], + ['primary_email', 'primary_im', 'primary_phone', 'primary_address', 'primary_openid', 'primary_website'], + $fieldName); + if ($oldPrimaryName !== $fieldName) { + $legacyFieldMapping[$oldPrimaryName] = $fieldName; + } + } + return $legacyFieldMapping; } /** @@ -619,7 +651,7 @@ protected function getBespokeTokens(): array { 'data_type' => 'String', 'audience' => 'user', ], - 'primary_address.country_id.region_id:name' => [ + 'address_primary.country_id.region_id:name' => [ 'title' => ts('World Region'), 'name' => 'country_id.region_id.name', 'type' => 'mapped', diff --git a/CRM/Core/EntityTokens.php b/CRM/Core/EntityTokens.php index b33ae4ebe3ba..af3ba17efd7b 100644 --- a/CRM/Core/EntityTokens.php +++ b/CRM/Core/EntityTokens.php @@ -614,7 +614,7 @@ protected function addFieldToTokenMetadata(array &$tokensMetadata, array $field, if ($field['type'] !== 'Custom' && !in_array($field['name'], $exposedFields, TRUE)) { return; } - $field['audience'] = 'user'; + $field['audience'] = $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) @@ -627,7 +627,7 @@ protected function addFieldToTokenMetadata(array &$tokensMetadata, array $field, // 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 + // 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]}"; diff --git a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php index 78357cf69bc0..095dd65b1b62 100644 --- a/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php +++ b/tests/phpunit/CRM/Core/BAO/MessageTemplateTest.php @@ -123,7 +123,7 @@ public function getLocaleTemplates(array $allTemplates, array $locales): array { * @dataProvider getLocaleConfigurations */ public function testRenderTranslatedTemplate($settings, $templates, $preferredLanguage, $expectRendered): void { - if (empty($settings['partial_locales']) && count(\CRM_Core_I18n::languages(FALSE)) <= 1) { + if (empty($settings['partial_locales']) && count(CRM_Core_I18n::languages(FALSE)) <= 1) { $this->markTestIncomplete('Full testing of localization requires l10n data.'); } $cleanup = \CRM_Utils_AutoClean::swapSettings($settings); @@ -658,39 +658,38 @@ public function getAdvertisedTokens(): array { '{contact.addressee_display}' => 'Addressee', '{contact.email_greeting_display}' => 'Email Greeting', '{contact.postal_greeting_display}' => 'Postal Greeting', - '{contact.current_employer}' => 'Current Employer', - '{contact.location_type_id:label}' => 'Address Location Type', - '{contact.address_id}' => 'Address ID', - '{contact.street_address}' => 'Street Address', - '{contact.street_number}' => 'Street Number', - '{contact.street_number_suffix}' => 'Street Number Suffix', - '{contact.street_name}' => 'Street Name', - '{contact.street_unit}' => 'Street Unit', - '{contact.supplemental_address_1}' => 'Supplemental Address 1', - '{contact.supplemental_address_2}' => 'Supplemental Address 2', - '{contact.supplemental_address_3}' => 'Supplemental Address 3', - '{contact.city}' => 'City', - '{contact.postal_code_suffix}' => 'Postal Code Suffix', - '{contact.postal_code}' => 'Postal Code', - '{contact.geo_code_1}' => 'Latitude', - '{contact.geo_code_2}' => 'Longitude', - '{contact.address_name}' => 'Address Name', - '{contact.master_id}' => 'Master Address ID', - '{contact.county}' => 'County', - '{contact.state_province}' => 'State/Province', - '{contact.country}' => 'Country', - '{contact.phone}' => 'Phone', - '{contact.phone_ext}' => 'Phone Extension', - '{contact.phone_type}' => 'Phone Type', - '{contact.email}' => 'Email', - '{contact.on_hold:label}' => 'On Hold', - '{contact.signature_text}' => 'Signature Text', - '{contact.signature_html}' => 'Signature Html', - '{contact.provider_id:label}' => 'IM Provider', - '{contact.im}' => 'IM Screen Name', - '{contact.openid}' => 'OpenID', - '{contact.world_region}' => 'World Region', - '{contact.url}' => 'Website', + '{contact.employer_id.display_name}' => 'Current Employer', + '{contact.address_primary.location_type_id:label}' => 'Address Location Type', + '{contact.address_primary.id}' => 'Address ID', + '{contact.address_primary.street_address}' => 'Street Address', + '{contact.address_primary.street_number}' => 'Street Number', + '{contact.address_primary.street_number_suffix}' => 'Street Number Suffix', + '{contact.address_primary.street_name}' => 'Street Name', + '{contact.address_primary.street_unit}' => 'Street Unit', + '{contact.address_primary.supplemental_address_1}' => 'Supplemental Address 1', + '{contact.address_primary.supplemental_address_2}' => 'Supplemental Address 2', + '{contact.address_primary.supplemental_address_3}' => 'Supplemental Address 3', + '{contact.address_primary.city}' => 'City', + '{contact.address_primary.postal_code_suffix}' => 'Postal Code Suffix', + '{contact.address_primary.postal_code}' => 'Postal Code', + '{contact.address_primary.geo_code_1}' => 'Latitude', + '{contact.address_primary.geo_code_2}' => 'Longitude', + '{contact.address_primary.name}' => 'Address Name', + '{contact.address_primary.master_id}' => 'Master Address ID', + '{contact.address_primary.county_id:label}' => 'County', + '{contact.address_primary.state_province_id:abbr}' => 'State/Province', + '{contact.address_primary.country_id:label}' => 'Country', + '{contact.phone_primary.phone}' => 'Phone', + '{contact.phone_primary.phone_ext}' => 'Phone Extension', + '{contact.phone_primary.phone_type_id:label}' => 'Phone Type', + '{contact.email_primary.email}' => 'Email', + '{contact.email_primary.on_hold:label}' => 'On Hold', + '{contact.email_primary.signature_text}' => 'Signature Text', + '{contact.email_primary.signature_html}' => 'Signature Html', + '{contact.im_primary.provider_id:label}' => 'IM Provider', + '{contact.im_primary.name}' => 'IM Screen Name', + '{contact.address_primary.country_id.region_id:name}' => 'World Region', + '{contact.website_first.url}' => 'Website', '{contact.custom_9}' => 'Contact reference field :: Custom Group', '{contact.custom_7}' => 'Country :: Custom Group', '{contact.custom_8}' => 'Country-multi :: Custom Group', @@ -722,6 +721,7 @@ public function getAdvertisedTokens(): array { * Note it will render additional custom fields if they exist. * * @return array + * * @throws \CRM_Core_Exception */ public function getOldContactTokens(): array { @@ -828,6 +828,46 @@ public function getOldContactTokens(): array { ]; } + /** + * Test tokens that we briefly introduced before changing our minds.... + * + * The style I thought we were going to go with looked like + * + * {contact.primary_address.street_address} but.... we went with + * {contact.address_primary.street_address} at the apiv4 level - which is what + * we are trying to mirror. It's likely no-one ever used these as we didn't + * advertise them and the old 'random' v3 style tokens continued to work. + * + * But, we should support them for a bit - which means testing them... + * + * @throws \CRM_Core_Exception + */ + public function testBrieflyPopularTokens(): void { + $this->createCustomGroupWithFieldsOfAllTypes([]); + $tokenData = $this->getOldContactTokens(); + $this->setupContactFromTokeData($tokenData); + // One token from each entity.... + $tokenString = "primary_email:{contact.primary_email.email}\n" + . "primary_address:{contact.primary_address.street_address}\n" + . "primary_im:{contact.primary_im.name}\n" + . "primary_website:{contact.primary_website.url}\n" + . "primary_openid:{contact.primary_openid.openid}\n" + . "primary_phone:{contact.primary_phone.phone}\n"; + + $tokenProcessor = new TokenProcessor(Civi::dispatcher(), []); + $tokenProcessor->addMessage('html', $tokenString, 'text/html'); + $tokenProcessor->addRow(['contactId' => $tokenData['contact_id']]); + $tokenProcessor->evaluate(); + $messageHtml = $tokenProcessor->getRow(0)->render('html'); + $this->assertEquals('primary_email:anthony_anderson@civicrm.org +primary_address:Street Address +primary_im:IM Screen Name +primary_website:https://civicrm.org +primary_openid:OpenID +primary_phone:123-456 +', $messageHtml); + } + /** * @param array $tokenData * @@ -927,10 +967,10 @@ protected function getExpectedContactOutput(int $id, array $tokenData, string $a phone_type_id:2 phone_type:Mobile email:anthony_anderson@civicrm.org -on_hold:0 +on_hold:No signature_text:Yours sincerely signature_html:
Yours
-im_provider:1 +im_provider:Yahoo im:IM Screen Name openid:OpenID world_region:America South, Central, North and Caribbean @@ -1000,39 +1040,38 @@ protected function getExpectedContactOutputNewStyle(int $id, array $tokenData, s addressee_display |Mr. Robert Frank Smith II email_greeting_display |Dear Robert postal_greeting_display |Dear Robert -current_employer |Unit Test Organization -location_type_id:label |Home -address_id |' . $id . ' -street_address |Street Address -street_number |123 -street_number_suffix |S -street_name |Main St -street_unit |45B -supplemental_address_1 |Round the corner -supplemental_address_2 |Up the road -supplemental_address_3 |By the big tree -city |New York -postal_code_suffix |4578 -postal_code |90210 -geo_code_1 |48.858093 -geo_code_2 |2.294694 -address_name |The white house -master_id |' . $tokenData['master_id'] . ' -county | -state_province |TX -country |United States -phone |123-456 -phone_ext |77 -phone_type |Mobile -email |anthony_anderson@civicrm.org -on_hold:label |No -signature_text |Yours sincerely -signature_html |Yours
-provider_id:label |Yahoo -im |IM Screen Name -openid |OpenID -world_region |America South, Central, North and Caribbean -url |https://civicrm.org +employer_id.display_name |Unit Test Organization +address_primary.location_type_id:label |Home +address_primary.id |' . $id . ' +address_primary.street_address |Street Address +address_primary.street_number |123 +address_primary.street_number_suffix |S +address_primary.street_name |Main St +address_primary.street_unit |45B +address_primary.supplemental_address_1 |Round the corner +address_primary.supplemental_address_2 |Up the road +address_primary.supplemental_address_3 |By the big tree +address_primary.city |New York +address_primary.postal_code_suffix |4578 +address_primary.postal_code |90210 +address_primary.geo_code_1 |48.858093 +address_primary.geo_code_2 |2.294694 +address_primary.name |The white house +address_primary.master_id |' . $tokenData['master_id'] . ' +address_primary.county_id:label | +address_primary.state_province_id:abbr |TX +address_primary.country_id:label |United States +phone_primary.phone |123-456 +phone_primary.phone_ext |77 +phone_primary.phone_type_id:label |Mobile +email_primary.email |anthony_anderson@civicrm.org +email_primary.on_hold:label |No +email_primary.signature_text |Yours sincerely +email_primary.signature_html |<p>Yours</p> +im_primary.provider_id:label |Yahoo +im_primary.name |IM Screen Name +address_primary.country_id.region_id:name |America South, Central, North and Caribbean +website_first.url |https://civicrm.org custom_9 |Mr. Spider Man II custom_7 |New Zealand custom_8 |France, Canada