4 use Civi\Token\Event\TokenRegisterEvent
;
5 use Civi\Token\Event\TokenRenderEvent
;
6 use Civi\Token\Event\TokenValueEvent
;
7 use Symfony\Component\EventDispatcher\EventSubscriberInterface
;
10 * Class TokenCompatSubscriber
13 * This class provides a compatibility layer for using CRM_Utils_Token
14 * helpers within TokenProcessor.
16 * THIS IS NOT A GOOD EXAMPLE TO EMULATE. The class exists to two
17 * bridge two different designs. CRM_Utils_Token has some
18 * undesirable elements (like iterative token substitution).
19 * However, if you're refactor CRM_Utils_Token or improve the
20 * bridge, then it makes sense to update this class.
22 class TokenCompatSubscriber
implements EventSubscriberInterface
{
24 protected $entity = 'contact';
29 public static function getSubscribedEvents(): array {
31 'civi.token.eval' => [
32 ['setupSmartyAliases', 1000],
35 'civi.token.render' => 'onRender',
36 'civi.token.list' => 'registerTokens',
41 * Register the declared tokens.
43 * @param \Civi\Token\Event\TokenRegisterEvent $e
44 * The registration event. Add new tokens using register().
46 public function registerTokens(TokenRegisterEvent
$e): void
{
47 if (!$this->checkActive($e->getTokenProcessor())) {
50 foreach (array_merge($this->getContactTokens(), $this->getCustomFieldTokens()) as $name => $label) {
52 'entity' => $this->entity
,
57 foreach ($this->getLegacyHookTokens() as $legacyHookToken) {
59 'entity' => $legacyHookToken['category'],
60 'field' => $legacyHookToken['name'],
61 'label' => $legacyHookToken['label'],
67 * Determine whether this token-handler should be used with
68 * the given processor.
70 * To short-circuit token-processing in irrelevant contexts,
73 * @param \Civi\Token\TokenProcessor $processor
76 public function checkActive(\Civi\Token\TokenProcessor
$processor) {
77 return in_array($this->getEntityIDField(), $processor->context
['schema'], TRUE);
83 public function getEntityIDField(): string {
88 * Get functions declared using the legacy hook.
90 * Note that these only extend the contact entity (
91 * ie they are based on having a contact ID which they.
92 * may or may not use, but they don't have other
97 public function getLegacyHookTokens(): array {
100 \CRM_Utils_Hook
::tokens($hookTokens);
101 foreach ($hookTokens as $tokenValues) {
102 foreach ($tokenValues as $key => $value) {
103 if (is_numeric($key)) {
104 // This appears to be an attempt to compensate for
105 // inconsistencies described in https://docs.civicrm.org/dev/en/latest/hooks/hook_civicrm_tokenValues/#example
106 // in effect there is a suggestion that
107 // Send an Email" and "CiviMail" send different parameters to the tokenValues hook
108 // As of now 'send an email' renders hooks through this class.
109 // CiviMail it depends on the use or otherwise of flexmailer.
112 if (preg_match('/^\{([^\}]+)\}$/', $value, $matches)) {
113 $value = $matches[1];
115 $keyParts = explode('.', $key);
117 'category' => $keyParts[0],
118 'name' => $keyParts[1],
128 * @throws \CRM_Core_Exception
130 public function getCustomFieldTokens(): array {
132 $customFields = \CRM_Core_BAO_CustomField
::getFields(['Individual', 'Address', 'Contact']);
133 foreach ($customFields as $customField) {
134 $tokens['custom_' . $customField['id']] = $customField['label'] . " :: " . $customField['groupTitle'];
140 * Get all tokens advertised as contact tokens.
144 public function getContactTokens(): array {
146 'contact_type' => 'Contact Type',
147 'do_not_email' => 'Do Not Email',
148 'do_not_phone' => 'Do Not Phone',
149 'do_not_mail' => 'Do Not Mail',
150 'do_not_sms' => 'Do Not Sms',
151 'do_not_trade' => 'Do Not Trade',
152 'is_opt_out' => 'No Bulk Emails (User Opt Out)',
153 'external_identifier' => 'External Identifier',
154 'sort_name' => 'Sort Name',
155 'display_name' => 'Display Name',
156 'nick_name' => 'Nickname',
157 'image_URL' => 'Image Url',
158 'preferred_communication_method' => 'Preferred Communication Method',
159 'preferred_language' => 'Preferred Language',
160 'preferred_mail_format' => 'Preferred Mail Format',
161 'hash' => 'Contact Hash',
162 'contact_source' => 'Contact Source',
163 'first_name' => 'First Name',
164 'middle_name' => 'Middle Name',
165 'last_name' => 'Last Name',
166 'individual_prefix' => 'Individual Prefix',
167 'individual_suffix' => 'Individual Suffix',
168 'formal_title' => 'Formal Title',
169 'communication_style' => 'Communication Style',
170 'job_title' => 'Job Title',
171 'gender' => 'Gender ID',
172 'birth_date' => 'Birth Date',
173 'current_employer_id' => 'Current Employer ID',
174 'contact_is_deleted' => 'Contact is in Trash',
175 'created_date' => 'Created Date',
176 'modified_date' => 'Modified Date',
177 'addressee' => 'Addressee',
178 'email_greeting' => 'Email Greeting',
179 'postal_greeting' => 'Postal Greeting',
180 'current_employer' => 'Current Employer',
181 'location_type' => 'Location Type',
182 'address_id' => 'Address ID',
183 'street_address' => 'Street Address',
184 'street_number' => 'Street Number',
185 'street_number_suffix' => 'Street Number Suffix',
186 'street_name' => 'Street Name',
187 'street_unit' => 'Street Unit',
188 'supplemental_address_1' => 'Supplemental Address 1',
189 'supplemental_address_2' => 'Supplemental Address 2',
190 'supplemental_address_3' => 'Supplemental Address 3',
192 'postal_code_suffix' => 'Postal Code Suffix',
193 'postal_code' => 'Postal Code',
194 'geo_code_1' => 'Latitude',
195 'geo_code_2' => 'Longitude',
196 'manual_geo_code' => 'Is Manually Geocoded',
197 'address_name' => 'Address Name',
198 'master_id' => 'Master Address ID',
199 'county' => 'County',
200 'state_province' => 'State',
201 'country' => 'Country',
203 'phone_ext' => 'Phone Extension',
204 'phone_type_id' => 'Phone Type ID',
205 'phone_type' => 'Phone Type',
207 'on_hold' => 'On Hold',
208 'signature_text' => 'Signature Text',
209 'signature_html' => 'Signature Html',
210 'im_provider' => 'IM Provider',
211 'im' => 'IM Screen Name',
212 'openid' => 'OpenID',
213 'world_region' => 'World Region',
215 'checksum' => 'Checksum',
216 'contact_id' => 'Internal Contact ID',
221 * Interpret the variable `$context['smartyTokenAlias']` (e.g. `mySmartyField' => `tkn_entity.tkn_field`).
223 * We need to ensure that any tokens like `{tkn_entity.tkn_field}` are hydrated, so
224 * we pretend that they are in use.
226 * @param \Civi\Token\Event\TokenValueEvent $e
228 public function setupSmartyAliases(TokenValueEvent
$e) {
230 foreach ($e->getRows() as $row) {
231 $aliasedTokens = array_unique(array_merge($aliasedTokens,
232 array_values($row->context
['smartyTokenAlias'] ??
[])));
235 $fakeMessage = implode('', array_map(function ($f) {
236 return '{' . $f . '}';
239 $proc = $e->getTokenProcessor();
240 $proc->addMessage('TokenCompatSubscriber.aliases', $fakeMessage, 'text/plain');
246 * @param \Civi\Token\Event\TokenValueEvent $e
247 * @throws TokenException
249 public function onEvaluate(TokenValueEvent
$e) {
250 // For reasons unknown, replaceHookTokens used to require a pre-computed list of
251 // hook *categories* (aka entities aka namespaces). We cache
252 // this in the TokenProcessor's context but can likely remove it now.
254 $e->getTokenProcessor()->context
['hookTokenCategories'] = \CRM_Utils_Token
::getTokenCategories();
256 $messageTokens = $e->getTokenProcessor()->getMessageTokens();
257 $returnProperties = array_fill_keys($messageTokens['contact'] ??
[], 1);
258 $returnProperties = array_merge(\CRM_Contact_BAO_Query
::defaultReturnProperties(), $returnProperties);
260 foreach ($e->getRows() as $row) {
261 if (empty($row->context
['contactId'])) {
266 $swapLocale = empty($row->context
['locale']) ?
NULL : \CRM_Utils_AutoClean
::swapLocale($row->context
['locale']);
268 /** @var int $contactId */
269 $contactId = $row->context
['contactId'];
270 if (empty($row->context
['contact'])) {
272 ['contact_id', '=', $contactId, 0, 0],
274 [$contact] = \CRM_Contact_BAO_Query
::apiQuery($params, $returnProperties ??
NULL);
276 $contact = reset($contact);
277 // Test cover for greeting in CRM_Core_BAO_ActionScheduleTest::testMailer
278 $contact['email_greeting'] = $contact['email_greeting_display'] ??
'';
279 $contact['postal_greeting'] = $contact['postal_greeting_display'] ??
'';
280 $contact['addressee'] = $contact['address_display'] ??
'';
282 //update value of custom field token
283 if (!empty($messageTokens['contact'])) {
284 foreach ($messageTokens['contact'] as $token) {
285 if (\CRM_Core_BAO_CustomField
::getKeyID($token)) {
286 $contact[$token] = \CRM_Core_BAO_CustomField
::displayValue($contact[$token], \CRM_Core_BAO_CustomField
::getKeyID($token));
292 $contact = $row->context
['contact'];
295 $contactArray = [$contactId => $contact];
296 \CRM_Utils_Hook
::tokenValues($contactArray,
298 empty($row->context
['mailingJobId']) ?
NULL : $row->context
['mailingJobId'],
300 $row->context
['controller']
303 // merge the custom tokens in the $contact array
304 if (!empty($contactArray[$contactId])) {
305 $contact = array_merge($contact, $contactArray[$contactId]);
307 $row->context('contact', $contact);
312 * Apply the various CRM_Utils_Token helpers.
314 * @param \Civi\Token\Event\TokenRenderEvent $e
316 public function onRender(TokenRenderEvent
$e) {
317 $isHtml = ($e->message
['format'] == 'text/html');
318 $useSmarty = !empty($e->context
['smarty']);
320 $domain = \CRM_Core_BAO_Domain
::getDomain();
321 $e->string = \CRM_Utils_Token
::replaceDomainTokens($e->string, $domain, $isHtml, $e->message
['tokens'], $useSmarty);
323 if (!empty($e->context
['contact'])) {
324 \CRM_Utils_Token
::replaceGreetingTokens($e->string, $e->context
['contact'], $e->context
['contact']['contact_id'], NULL, $useSmarty);
329 foreach ($e->context
['smartyTokenAlias'] ??
[] as $smartyName => $tokenName) {
330 // Note: $e->row->tokens resolves event-based tokens (eg CRM_*_Tokens). But if the target token relies on the
331 // above bits (replaceGreetingTokens=>replaceContactTokens=>replaceHookTokens) then this lookup isn't sufficient.
332 $smartyVars[$smartyName] = \CRM_Utils_Array
::pathGet($e->row
->tokens
, explode('.', $tokenName));
334 \CRM_Core_Smarty
::singleton()->pushScope($smartyVars);
336 $e->string = \CRM_Utils_String
::parseOneOffStringThroughSmarty($e->string);
339 \CRM_Core_Smarty
::singleton()->popScope();