Merge pull request #21430 from mattwire/flushcache
[civicrm-core.git] / CRM / Utils / Token.php
CommitLineData
6a488035
TO
1<?php
2/*
3 +--------------------------------------------------------------------+
bc77d7c0 4 | Copyright CiviCRM LLC. All rights reserved. |
6a488035 5 | |
bc77d7c0
TO
6 | This work is published under the GNU AGPLv3 license with some |
7 | permitted exceptions and without any warranty. For full license |
8 | and copyright information, see https://civicrm.org/licensing |
6a488035 9 +--------------------------------------------------------------------+
d25dd0ee 10 */
6a488035
TO
11
12/**
13 *
14 * @package CRM
ca5cec67 15 * @copyright CiviCRM LLC https://civicrm.org/licensing
6a488035
TO
16 */
17
18/**
b8c71ffa 19 * Class to abstract token replacement.
6a488035
TO
20 */
21class CRM_Utils_Token {
6714d8d2 22 public static $_requiredTokens = NULL;
6a488035 23
6714d8d2 24 public static $_tokens = [
be2fb01f 25 'action' => [
6a488035
TO
26 'forward',
27 'optOut',
28 'optOutUrl',
29 'reply',
30 'unsubscribe',
31 'unsubscribeUrl',
32 'resubscribe',
33 'resubscribeUrl',
34 'subscribeUrl',
be2fb01f
CW
35 ],
36 'mailing' => [
6a488035 37 'id',
b56b8b0e 38 'key',
6a488035
TO
39 'name',
40 'group',
41 'subject',
42 'viewUrl',
43 'editUrl',
44 'scheduleUrl',
45 'approvalStatus',
46 'approvalNote',
47 'approveUrl',
48 'creator',
49 'creatorEmail',
be2fb01f
CW
50 ],
51 'user' => [
6a488035
TO
52 // we extract the stuff after the role / permission and return the
53 // civicrm email addresses of all users with that role / permission
54 // useful with rules integration
55 'permission:',
56 'role:',
be2fb01f 57 ],
6a488035
TO
58 // populate this dynamically
59 'contact' => NULL,
60 // populate this dynamically
61 'contribution' => NULL,
be2fb01f 62 'domain' => [
6a488035
TO
63 'name',
64 'phone',
65 'address',
66 'email',
e3470b79 67 'id',
68 'description',
be2fb01f
CW
69 ],
70 'subscribe' => ['group'],
71 'unsubscribe' => ['group'],
72 'resubscribe' => ['group'],
73 'welcome' => ['group'],
74 ];
6a488035
TO
75
76 /**
dfbda5e5
TO
77 * @deprecated
78 * This is used by CiviMail but will be made redundant by FlexMailer.
5f56e085 79 * @return array
6a488035 80 */
5f56e085 81 public static function getRequiredTokens() {
6a488035 82 if (self::$_requiredTokens == NULL) {
be2fb01f 83 self::$_requiredTokens = [
6a488035 84 'domain.address' => ts("Domain address - displays your organization's postal address."),
be2fb01f 85 'action.optOutUrl or action.unsubscribeUrl' => [
6a488035
TO
86 'action.optOut' => ts("'Opt out via email' - displays an email address for recipients to opt out of receiving emails from your organization."),
87 'action.optOutUrl' => ts("'Opt out via web page' - creates a link for recipients to click if they want to opt out of receiving emails from your organization. Alternatively, you can include the 'Opt out via email' token."),
88 'action.unsubscribe' => ts("'Unsubscribe via email' - displays an email address for recipients to unsubscribe from the specific mailing list used to send this message."),
d72434db 89 'action.unsubscribeUrl' => ts("'Unsubscribe via web page' - creates a link for recipients to unsubscribe from the specific mailing list used to send this message. Alternatively, you can include the 'Unsubscribe via email' token or one of the Opt-out tokens."),
be2fb01f
CW
90 ],
91 ];
6a488035 92 }
5f56e085
TO
93 return self::$_requiredTokens;
94 }
95
96 /**
97 * Check a string (mailing body) for required tokens.
98 *
99 * @param string $str
100 * The message.
101 *
102 * @return bool|array
6714d8d2 103 * true if all required tokens are found,
5f56e085
TO
104 * else an array of the missing tokens
105 */
106 public static function requiredTokens(&$str) {
b845ba46 107 // FlexMailer is a refactoring of CiviMail which provides new hooks/APIs/docs. If the sysadmin has opted to enable it, then use that instead of CiviMail.
be2fb01f 108 $requiredTokens = defined('CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS') ? Civi\Core\Resolver::singleton()->call(CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS, []) : CRM_Utils_Token::getRequiredTokens();
6a488035 109
be2fb01f 110 $missing = [];
5f56e085 111 foreach ($requiredTokens as $token => $value) {
6a488035
TO
112 if (!is_array($value)) {
113 if (!preg_match('/(^|[^\{])' . preg_quote('{' . $token . '}') . '/', $str)) {
114 $missing[$token] = $value;
115 }
116 }
117 else {
118 $present = FALSE;
119 $desc = NULL;
120 foreach ($value as $t => $d) {
121 $desc = $d;
122 if (preg_match('/(^|[^\{])' . preg_quote('{' . $t . '}') . '/', $str)) {
123 $present = TRUE;
124 }
125 }
126 if (!$present) {
127 $missing[$token] = $desc;
128 }
129 }
130 }
131
132 if (empty($missing)) {
133 return TRUE;
134 }
135 return $missing;
136 }
137
138 /**
fe482240 139 * Wrapper for token matching.
6a488035 140 *
77855840
TO
141 * @param string $type
142 * The token type (domain,mailing,contact,action).
143 * @param string $var
144 * The token variable.
145 * @param string $str
146 * The string to search.
6a488035 147 *
e7483cbe 148 * @return bool
a6c01b45 149 * Was there a match
6a488035
TO
150 */
151 public static function token_match($type, $var, &$str) {
152 $token = preg_quote('{' . "$type.$var") . '(\|.+?)?' . preg_quote('}');
93f087c7 153 return preg_match("/(^|[^\{])$token/", $str);
6a488035
TO
154 }
155
156 /**
fe482240 157 * Wrapper for token replacing.
6a488035 158 *
77855840
TO
159 * @param string $type
160 * The token type.
161 * @param string $var
162 * The token variable.
163 * @param string $value
164 * The value to substitute for the token.
6714d8d2 165 * @param string $str (reference) The string to replace in
e39893f5
EM
166 *
167 * @param bool $escapeSmarty
6a488035 168 *
a6c01b45
CW
169 * @return string
170 * The processed string
6a488035 171 */
40022216 172 public static function token_replace($type, $var, $value, &$str, $escapeSmarty = FALSE) {
6a488035
TO
173 $token = preg_quote('{' . "$type.$var") . '(\|([^\}]+?))?' . preg_quote('}');
174 if (!$value) {
175 $value = '$3';
176 }
177 if ($escapeSmarty) {
178 $value = self::tokenEscapeSmarty($value);
179 }
180 $str = preg_replace("/([^\{])?$token/", "\${1}$value", $str);
181 return $str;
182 }
183
184 /**
ad06f98f 185 * Get the regex for token replacement
6a488035 186 *
77855840
TO
187 * @param string $token_type
188 * A string indicating the the type of token to be used in the expression.
6a488035 189 *
a6c01b45 190 * @return string
ad06f98f 191 * regular expression suitable for using in preg_replace
6a488035 192 */
ad06f98f
EM
193 private static function tokenRegex(string $token_type) {
194 return '/(?<!\{|\\\\)\{' . $token_type . '\.([\w]+(:|\.)?\w*(\-[\w\s]+)?)\}(?!\})/';
6a488035
TO
195 }
196
197 /**
fe482240 198 * Escape the string so a malicious user cannot inject smarty code into the template.
6a488035 199 *
77855840
TO
200 * @param string $string
201 * A string that needs to be escaped from smarty parsing.
6a488035 202 *
a6c01b45
CW
203 * @return string
204 * the escaped string
6a488035 205 */
43ceab3f 206 public static function tokenEscapeSmarty($string) {
6a488035 207 // need to use negative look-behind, as both str_replace() and preg_replace() are sequential
be2fb01f 208 return preg_replace(['/{/', '/(?<!{ldelim)}/'], ['{ldelim}', '{rdelim}'], $string);
6a488035
TO
209 }
210
e7292422 211 /**
6a488035
TO
212 * Replace all the domain-level tokens in $str
213 *
77855840
TO
214 * @param string $str
215 * The string with tokens to be replaced.
216 * @param object $domain
217 * The domain BAO.
218 * @param bool $html
219 * Replace tokens with HTML or plain text.
e39893f5
EM
220 *
221 * @param null $knownTokens
222 * @param bool $escapeSmarty
6a488035 223 *
a6c01b45
CW
224 * @return string
225 * The processed string
6a488035 226 */
17aa5355 227 public static function replaceDomainTokens(
21bb6c7b 228 $str,
17aa5355 229 $domain,
21bb6c7b
DL
230 $html = FALSE,
231 $knownTokens = NULL,
232 $escapeSmarty = FALSE
233 ) {
6a488035 234 $key = 'domain';
21bb6c7b 235 if (
353ffa53
TO
236 !$knownTokens || empty($knownTokens[$key])
237 ) {
6a488035
TO
238 return $str;
239 }
240
8bab0eb0 241 $str = preg_replace_callback(
21bb6c7b 242 self::tokenRegex($key),
17aa5355 243 function ($matches) use ($domain, $html, $escapeSmarty) {
353ffa53 244 return CRM_Utils_Token::getDomainTokenReplacement($matches[1], $domain, $html, $escapeSmarty);
8bab0eb0 245 },
21bb6c7b
DL
246 $str
247 );
6a488035
TO
248 return $str;
249 }
250
e39893f5 251 /**
160e7328 252 * @param string $token
d357f225 253 * @param CRM_Core_BAO_Domain $domain
e39893f5
EM
254 * @param bool $html
255 * @param bool $escapeSmarty
256 *
160e7328 257 * @return null|string
e39893f5 258 */
160e7328 259 public static function getDomainTokenReplacement($token, $domain, $html = FALSE, $escapeSmarty = FALSE): ?string {
3c78698e 260 $tokens = CRM_Core_DomainTokens::getDomainTokenValues($domain->id, $html);
60299771 261 $value = $tokens[$token] ?? "{domain.$token}";
6a488035
TO
262 if ($escapeSmarty) {
263 $value = self::tokenEscapeSmarty($value);
264 }
6a488035
TO
265 return $value;
266 }
267
268 /**
269 * Replace all the org-level tokens in $str
270 *
fc0ac495 271 * @fixme: This function appears to be broken, as it depended on
c21a9b7f 272 * nonexistant method: CRM_Core_BAO_CustomValue::getContactValues()
fc0ac495 273 * Marking as deprecated until this is clarified.
274 *
c21a9b7f 275 * @deprecated
fc0ac495 276 * - the above hard-breakage was there from 2015 to 2021 and
277 * no error was ever reported on it -does that mean
278 * 1) the code is never hit because the only function that
279 * calls this function is never called or
280 * 2) it was called but never required to resolve any tokens
281 * or more specifically custom field tokens
282 *
283 * The handling for custom fields with the removed token has
284 * now been removed.
c21a9b7f 285 *
77855840
TO
286 * @param string $str
287 * The string with tokens to be replaced.
288 * @param object $org
289 * Associative array of org properties.
290 * @param bool $html
291 * Replace tokens with HTML or plain text.
e39893f5
EM
292 *
293 * @param bool $escapeSmarty
6a488035 294 *
a6c01b45
CW
295 * @return string
296 * The processed string
6a488035 297 */
fc0ac495 298 public static function replaceOrgTokens($str, &$org, $html = FALSE, $escapeSmarty = FALSE) {
79594ce7 299 CRM_Core_Error::deprecatedFunctionWarning('token processor');
e7483cbe
J
300 self::$_tokens['org']
301 = array_merge(
21bb6c7b 302 array_keys(CRM_Contact_BAO_Contact::importableFields('Organization')),
be2fb01f 303 ['address', 'display_name', 'checksum', 'contact_id']
21bb6c7b 304 );
6a488035 305
6a488035
TO
306 foreach (self::$_tokens['org'] as $token) {
307 // print "Getting token value for $token<br/><br/>";
fc0ac495 308 if ($token === '') {
6a488035
TO
309 continue;
310 }
311
50bfb460 312 // If the string doesn't contain this token, skip it.
6a488035
TO
313
314 if (!self::token_match('org', $token, $str)) {
315 continue;
316 }
317
50bfb460 318 // Construct value from $token and $contact
6a488035
TO
319
320 $value = NULL;
321
fc0ac495 322 if ($token === 'checksum') {
6a488035
TO
323 $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($org['contact_id']);
324 $value = "cs={$cs}";
325 }
fc0ac495 326 elseif ($token === 'address') {
50bfb460 327 // Build the location values array
6a488035 328
be2fb01f 329 $loc = [];
6a488035
TO
330 $loc['display_name'] = CRM_Utils_Array::retrieveValueRecursive($org, 'display_name');
331 $loc['street_address'] = CRM_Utils_Array::retrieveValueRecursive($org, 'street_address');
332 $loc['city'] = CRM_Utils_Array::retrieveValueRecursive($org, 'city');
333 $loc['state_province'] = CRM_Utils_Array::retrieveValueRecursive($org, 'state_province');
334 $loc['postal_code'] = CRM_Utils_Array::retrieveValueRecursive($org, 'postal_code');
335
50bfb460 336 // Construct the address token
6a488035
TO
337
338 $value = CRM_Utils_Address::format($loc);
339 if ($html) {
340 $value = str_replace("\n", '<br />', $value);
341 }
342 }
343 else {
344 $value = CRM_Utils_Array::retrieveValueRecursive($org, $token);
345 }
346
347 self::token_replace('org', $token, $value, $str, $escapeSmarty);
348 }
349
350 return $str;
351 }
352
353 /**
354 * Replace all mailing tokens in $str
355 *
77855840
TO
356 * @param string $str
357 * The string with tokens to be replaced.
358 * @param object $mailing
359 * The mailing BAO, or null for validation.
360 * @param bool $html
361 * Replace tokens with HTML or plain text.
e39893f5
EM
362 *
363 * @param null $knownTokens
364 * @param bool $escapeSmarty
6a488035 365 *
a6c01b45 366 * @return string
50bfb460 367 * The processed string
6a488035 368 */
21bb6c7b
DL
369 public static function &replaceMailingTokens(
370 $str,
371 &$mailing,
372 $html = FALSE,
373 $knownTokens = NULL,
374 $escapeSmarty = FALSE
375 ) {
6a488035
TO
376 $key = 'mailing';
377 if (!$knownTokens || !isset($knownTokens[$key])) {
378 return $str;
379 }
380
8bab0eb0 381 $str = preg_replace_callback(
6a488035 382 self::tokenRegex($key),
353ffa53
TO
383 function ($matches) use (&$mailing, $escapeSmarty) {
384 return CRM_Utils_Token::getMailingTokenReplacement($matches[1], $mailing, $escapeSmarty);
8bab0eb0
DL
385 },
386 $str
6a488035
TO
387 );
388 return $str;
389 }
390
e39893f5
EM
391 /**
392 * @param $token
393 * @param $mailing
394 * @param bool $escapeSmarty
395 *
396 * @return string
397 */
6a488035
TO
398 public static function getMailingTokenReplacement($token, &$mailing, $escapeSmarty = FALSE) {
399 $value = '';
400 switch ($token) {
401 // CRM-7663
402
403 case 'id':
404 $value = $mailing ? $mailing->id : 'undefined';
405 break;
406
b56b8b0e
J
407 // Key is the ID, or the hash when the hash URLs setting is enabled
408 case 'key':
409 $value = $mailing->id;
410 if ($hash = CRM_Mailing_BAO_Mailing::getMailingHash($value)) {
411 $value = $hash;
412 }
413 break;
6a488035
TO
414
415 case 'name':
416 $value = $mailing ? $mailing->name : 'Mailing Name';
417 break;
418
419 case 'group':
be2fb01f 420 $groups = $mailing ? $mailing->getGroupNames() : ['Mailing Groups'];
6a488035
TO
421 $value = implode(', ', $groups);
422 break;
423
424 case 'subject':
425 $value = $mailing->subject;
426 break;
427
428 case 'viewUrl':
c57f36a1
PJ
429 $mailingKey = $mailing->id;
430 if ($hash = CRM_Mailing_BAO_Mailing::getMailingHash($mailingKey)) {
431 $mailingKey = $hash;
432 }
6a488035 433 $value = CRM_Utils_System::url('civicrm/mailing/view',
c57f36a1 434 "reset=1&id={$mailingKey}",
6a488035
TO
435 TRUE, NULL, FALSE, TRUE
436 );
437 break;
438
439 case 'editUrl':
0083c73f
TO
440 case 'scheduleUrl':
441 // Note: editUrl and scheduleUrl used to be different, but now there's
442 // one screen which can adapt based on permissions (in workflow mode).
6a488035
TO
443 $value = CRM_Utils_System::url('civicrm/mailing/send',
444 "reset=1&mid={$mailing->id}&continue=true",
445 TRUE, NULL, FALSE, TRUE
446 );
447 break;
448
6a488035
TO
449 case 'html':
450 $page = new CRM_Mailing_Page_View();
c57f36a1 451 $value = $page->run($mailing->id, NULL, FALSE, TRUE);
6a488035
TO
452 break;
453
454 case 'approvalStatus':
a8c23526 455 $value = CRM_Core_PseudoConstant::getLabel('CRM_Mailing_DAO_Mailing', 'approval_status_id', $mailing->approval_status_id);
6a488035
TO
456 break;
457
458 case 'approvalNote':
459 $value = $mailing->approval_note;
460 break;
461
462 case 'approveUrl':
463 $value = CRM_Utils_System::url('civicrm/mailing/approve',
464 "reset=1&mid={$mailing->id}",
465 TRUE, NULL, FALSE, TRUE
466 );
467 break;
468
469 case 'creator':
470 $value = CRM_Contact_BAO_Contact::displayName($mailing->created_id);
471 break;
472
473 case 'creatorEmail':
474 $value = CRM_Contact_BAO_Contact::getPrimaryEmail($mailing->created_id);
475 break;
476
477 default:
478 $value = "{mailing.$token}";
479 break;
480 }
481
482 if ($escapeSmarty) {
483 $value = self::tokenEscapeSmarty($value);
484 }
485 return $value;
486 }
487
488 /**
489 * Replace all action tokens in $str
490 *
77855840
TO
491 * @param string $str
492 * The string with tokens to be replaced.
493 * @param array $addresses
494 * Assoc. array of VERP event addresses.
495 * @param array $urls
496 * Assoc. array of action URLs.
497 * @param bool $html
498 * Replace tokens with HTML or plain text.
499 * @param array $knownTokens
500 * A list of tokens that are known to exist in the email body.
e39893f5
EM
501 *
502 * @param bool $escapeSmarty
6a488035 503 *
a6c01b45
CW
504 * @return string
505 * The processed string
6a488035
TO
506 */
507 public static function &replaceActionTokens(
508 $str,
509 &$addresses,
510 &$urls,
511 $html = FALSE,
512 $knownTokens = NULL,
513 $escapeSmarty = FALSE
514 ) {
515 $key = 'action';
516 // here we intersect with the list of pre-configured valid tokens
517 // so that we remove anything we do not recognize
518 // I hope to move this step out of here soon and
519 // then we will just iterate on a list of tokens that are passed to us
8cc574cf 520 if (!$knownTokens || empty($knownTokens[$key])) {
6a488035
TO
521 return $str;
522 }
523
8bab0eb0
DL
524 $str = preg_replace_callback(
525 self::tokenRegex($key),
353ffa53
TO
526 function ($matches) use (&$addresses, &$urls, $html, $escapeSmarty) {
527 return CRM_Utils_Token::getActionTokenReplacement($matches[1], $addresses, $urls, $html, $escapeSmarty);
8bab0eb0 528 },
6a488035
TO
529 $str
530 );
531 return $str;
532 }
533
e39893f5
EM
534 /**
535 * @param $token
536 * @param $addresses
537 * @param $urls
538 * @param bool $html
539 * @param bool $escapeSmarty
540 *
541 * @return mixed|string
542 */
21bb6c7b
DL
543 public static function getActionTokenReplacement(
544 $token,
545 &$addresses,
546 &$urls,
547 $html = FALSE,
548 $escapeSmarty = FALSE
549 ) {
50bfb460
SB
550 // If the token is an email action, use it. Otherwise, find the
551 // appropriate URL.
6a488035
TO
552
553 if (!in_array($token, self::$_tokens['action'])) {
554 $value = "{action.$token}";
555 }
556 else {
9c1bc317 557 $value = $addresses[$token] ?? NULL;
6a488035
TO
558
559 if ($value == NULL) {
9c1bc317 560 $value = $urls[$token] ?? NULL;
6a488035
TO
561 }
562
563 if ($value && $html) {
50bfb460 564 // fix for CRM-2318
6a488035
TO
565 if ((substr($token, -3) != 'Url') && ($token != 'forward')) {
566 $value = "mailto:$value";
567 }
568 }
569 elseif ($value && !$html) {
570 $value = str_replace('&amp;', '&', $value);
571 }
572 }
573
574 if ($escapeSmarty) {
575 $value = self::tokenEscapeSmarty($value);
576 }
577 return $value;
578 }
579
580 /**
581 * Replace all the contact-level tokens in $str with information from
582 * $contact.
583 *
77855840
TO
584 * @param string $str
585 * The string with tokens to be replaced.
586 * @param array $contact
587 * Associative array of contact properties.
588 * @param bool $html
589 * Replace tokens with HTML or plain text.
590 * @param array $knownTokens
591 * A list of tokens that are known to exist in the email body.
592 * @param bool $returnBlankToken
593 * Return unevaluated token if value is null.
e39893f5
EM
594 *
595 * @param bool $escapeSmarty
6a488035 596 *
a6c01b45
CW
597 * @return string
598 * The processed string
6a488035 599 */
17aa5355 600 public static function replaceContactTokens(
21bb6c7b
DL
601 $str,
602 &$contact,
603 $html = FALSE,
604 $knownTokens = NULL,
605 $returnBlankToken = FALSE,
606 $escapeSmarty = FALSE
6a488035 607 ) {
7c990617 608 // Refresh contact tokens in case they have changed. There is heavy caching
609 // in exportable fields so there is no benefit in doing this conditionally.
610 self::$_tokens['contact'] = array_merge(
611 array_keys(CRM_Contact_BAO_Contact::exportableFields('All')),
be2fb01f 612 ['checksum', 'contact_id']
7c990617 613 );
6a488035 614
7c990617 615 $key = 'contact';
6a488035
TO
616 // here we intersect with the list of pre-configured valid tokens
617 // so that we remove anything we do not recognize
618 // I hope to move this step out of here soon and
619 // then we will just iterate on a list of tokens that are passed to us
8cc574cf 620 if (!$knownTokens || empty($knownTokens[$key])) {
6a488035
TO
621 return $str;
622 }
623
8bab0eb0 624 $str = preg_replace_callback(
21bb6c7b 625 self::tokenRegex($key),
353ffa53
TO
626 function ($matches) use (&$contact, $html, $returnBlankToken, $escapeSmarty) {
627 return CRM_Utils_Token::getContactTokenReplacement($matches[1], $contact, $html, $returnBlankToken, $escapeSmarty);
8bab0eb0 628 },
6a488035
TO
629 $str
630 );
631
632 $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
633 return $str;
634 }
635
e39893f5
EM
636 /**
637 * @param $token
638 * @param $contact
639 * @param bool $html
640 * @param bool $returnBlankToken
641 * @param bool $escapeSmarty
642 *
643 * @return bool|mixed|null|string
644 */
21bb6c7b
DL
645 public static function getContactTokenReplacement(
646 $token,
647 &$contact,
648 $html = FALSE,
649 $returnBlankToken = FALSE,
650 $escapeSmarty = FALSE
6a488035
TO
651 ) {
652 if (self::$_tokens['contact'] == NULL) {
653 /* This should come from UF */
654
e7483cbe
J
655 self::$_tokens['contact']
656 = array_merge(
21bb6c7b 657 array_keys(CRM_Contact_BAO_Contact::exportableFields('All')),
be2fb01f 658 ['checksum', 'contact_id']
21bb6c7b 659 );
6a488035
TO
660 }
661
50bfb460 662 // Construct value from $token and $contact
6a488035
TO
663
664 $value = NULL;
73d64eb6 665 $noReplace = FALSE;
6a488035 666
091133bb
CW
667 // Support legacy tokens
668 $token = CRM_Utils_Array::value($token, self::legacyContactTokens(), $token);
669
6a488035
TO
670 // check if the token we were passed is valid
671 // we have to do this because this function is
672 // called only when we find a token in the string
673
674 if (!in_array($token, self::$_tokens['contact'])) {
73d64eb6 675 $noReplace = TRUE;
6a488035
TO
676 }
677 elseif ($token == 'checksum') {
9c1bc317 678 $hash = $contact['hash'] ?? NULL;
6a488035
TO
679 $contactID = CRM_Utils_Array::retrieveValueRecursive($contact, 'contact_id');
680 $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID,
681 NULL,
682 NULL,
683 $hash
684 );
685 $value = "cs={$cs}";
686 }
687 else {
ea8be131 688 $value = (array) CRM_Utils_Array::retrieveValueRecursive($contact, $token);
ee70f332 689
ea8be131 690 foreach ($value as $index => $item) {
691 $value[$index] = self::convertPseudoConstantsUsingMetadata($value[$index], $token);
7ab8b45b 692 }
ea8be131 693 $value = implode(', ', $value);
6a488035
TO
694 }
695
696 if (!$html) {
697 $value = str_replace('&amp;', '&', $value);
698 }
699
700 // if null then return actual token
701 if ($returnBlankToken && !$value) {
73d64eb6
OB
702 $noReplace = TRUE;
703 }
704
705 if ($noReplace) {
6a488035
TO
706 $value = "{contact.$token}";
707 }
708
73d64eb6 709 if ($escapeSmarty
353ffa53 710 && !($returnBlankToken && $noReplace)
e7483cbe
J
711 ) {
712 // $returnBlankToken means the caller wants to do further attempts at
713 // processing unreplaced tokens -- so don't escape them yet in this case.
6a488035
TO
714 $value = self::tokenEscapeSmarty($value);
715 }
716
717 return $value;
718 }
719
720 /**
721 * Replace all the hook tokens in $str with information from
722 * $contact.
723 *
77855840
TO
724 * @param string $str
725 * The string with tokens to be replaced.
726 * @param array $contact
727 * Associative array of contact properties (including hook token values).
e39893f5 728 * @param $categories
77855840
TO
729 * @param bool $html
730 * Replace tokens with HTML or plain text.
e39893f5
EM
731 *
732 * @param bool $escapeSmarty
6a488035 733 *
a6c01b45
CW
734 * @return string
735 * The processed string
6a488035 736 */
21bb6c7b
DL
737 public static function &replaceHookTokens(
738 $str,
739 &$contact,
12d88807 740 $categories = NULL,
21bb6c7b
DL
741 $html = FALSE,
742 $escapeSmarty = FALSE
743 ) {
12d88807 744 if (!$categories) {
745 $categories = self::getTokenCategories();
746 }
6a488035 747 foreach ($categories as $key) {
8bab0eb0 748 $str = preg_replace_callback(
21bb6c7b 749 self::tokenRegex($key),
353ffa53
TO
750 function ($matches) use (&$contact, $key, $html, $escapeSmarty) {
751 return CRM_Utils_Token::getHookTokenReplacement($matches[1], $contact, $key, $html, $escapeSmarty);
8bab0eb0 752 },
6a488035
TO
753 $str
754 );
755 }
756 return $str;
757 }
758
12d88807 759 /**
760 * Get the categories required for rendering tokens.
761 *
762 * @return array
763 */
764 public static function getTokenCategories(): array {
765 if (!isset(\Civi::$statics[__CLASS__]['token_categories'])) {
766 $tokens = [];
767 \CRM_Utils_Hook::tokens($tokens);
768 \Civi::$statics[__CLASS__]['token_categories'] = array_keys($tokens);
769 }
770 return \Civi::$statics[__CLASS__]['token_categories'];
771 }
772
2d3e3c7b 773 /**
fe482240 774 * Parse html through Smarty resolving any smarty functions.
2d3e3c7b 775 * @param string $tokenHtml
776 * @param array $entity
777 * @param string $entityType
a6c01b45
CW
778 * @return string
779 * html parsed through smarty
79594ce7 780 * @deprecated
2d3e3c7b 781 */
782 public static function parseThroughSmarty($tokenHtml, $entity, $entityType = 'contact') {
783 if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY) {
784 $smarty = CRM_Core_Smarty::singleton();
785 // also add the tokens to the template
786 $smarty->assign_by_ref($entityType, $entity);
787 $tokenHtml = $smarty->fetch("string:$tokenHtml");
788 }
789 return $tokenHtml;
790 }
5bc392e6
EM
791
792 /**
793 * @param $token
794 * @param $contact
795 * @param $category
796 * @param bool $html
797 * @param bool $escapeSmarty
798 *
799 * @return mixed|string
95ea96be
EM
800 */
801 public static function getHookTokenReplacement(
21bb6c7b
DL
802 $token,
803 &$contact,
804 $category,
805 $html = FALSE,
806 $escapeSmarty = FALSE
807 ) {
9c1bc317 808 $value = $contact["{$category}.{$token}"] ?? NULL;
6a488035 809
21bb6c7b 810 if ($value && !$html) {
6a488035
TO
811 $value = str_replace('&amp;', '&', $value);
812 }
813
814 if ($escapeSmarty) {
815 $value = self::tokenEscapeSmarty($value);
816 }
817
818 return $value;
819 }
820
821 /**
50bfb460
SB
822 * unescapeTokens removes any characters that caused the replacement routines to skip token replacement
823 * for example {{token}} or \{token} will result in {token} in the final email
6a488035 824 *
50bfb460 825 * this routine will remove the extra backslashes and braces
6a488035 826 *
79594ce7
EM
827 * @deprecated
828 *
e7292422 829 * @param $str ref to the string that will be scanned and modified
6a488035
TO
830 */
831 public static function unescapeTokens(&$str) {
832 $str = preg_replace('/\\\\|\{(\{\w+\.\w+\})\}/', '\\1', $str);
833 }
834
835 /**
fe482240 836 * Replace unsubscribe tokens.
6a488035 837 *
77855840
TO
838 * @param string $str
839 * The string with tokens to be replaced.
840 * @param object $domain
841 * The domain BAO.
842 * @param array $groups
843 * The groups (if any) being unsubscribed.
844 * @param bool $html
845 * Replace tokens with html or plain text.
846 * @param int $contact_id
847 * The contact ID.
e7483cbe 848 * @param string $hash The security hash of the unsub event
6a488035 849 *
a6c01b45
CW
850 * @return string
851 * The processed string
6a488035
TO
852 */
853 public static function &replaceUnsubscribeTokens(
854 $str,
855 &$domain,
856 &$groups,
857 $html,
858 $contact_id,
859 $hash
860 ) {
861 if (self::token_match('unsubscribe', 'group', $str)) {
862 if (!empty($groups)) {
863 $config = CRM_Core_Config::singleton();
864 $base = CRM_Utils_System::baseURL();
865
866 // FIXME: an ugly hack for CRM-2035, to be dropped once CRM-1799 is implemented
867 $dao = new CRM_Contact_DAO_Group();
868 $dao->find();
869 while ($dao->fetch()) {
870 if (substr($dao->visibility, 0, 6) == 'Public') {
871 $visibleGroups[] = $dao->id;
872 }
873 }
874 $value = implode(', ', $groups);
875 self::token_replace('unsubscribe', 'group', $value, $str);
876 }
877 }
878 return $str;
879 }
880
881 /**
fe482240 882 * Replace resubscribe tokens.
6a488035 883 *
77855840
TO
884 * @param string $str
885 * The string with tokens to be replaced.
886 * @param object $domain
887 * The domain BAO.
888 * @param array $groups
889 * The groups (if any) being resubscribed.
890 * @param bool $html
891 * Replace tokens with html or plain text.
892 * @param int $contact_id
893 * The contact ID.
e7483cbe 894 * @param string $hash The security hash of the resub event
6a488035 895 *
a6c01b45
CW
896 * @return string
897 * The processed string
6a488035 898 */
a3e55d9c
TO
899 public static function &replaceResubscribeTokens(
900 $str, &$domain, &$groups, $html,
6a488035
TO
901 $contact_id, $hash
902 ) {
903 if (self::token_match('resubscribe', 'group', $str)) {
904 if (!empty($groups)) {
905 $value = implode(', ', $groups);
906 self::token_replace('resubscribe', 'group', $value, $str);
907 }
908 }
909 return $str;
910 }
911
912 /**
913 * Replace subscription-confirmation-request tokens
914 *
77855840
TO
915 * @param string $str
916 * The string with tokens to be replaced.
917 * @param string $group
918 * The name of the group being subscribed.
e39893f5 919 * @param $url
77855840
TO
920 * @param bool $html
921 * Replace tokens with html or plain text.
6a488035 922 *
a6c01b45
CW
923 * @return string
924 * The processed string
6a488035
TO
925 */
926 public static function &replaceSubscribeTokens($str, $group, $url, $html) {
927 if (self::token_match('subscribe', 'group', $str)) {
928 self::token_replace('subscribe', 'group', $group, $str);
929 }
930 if (self::token_match('subscribe', 'url', $str)) {
931 self::token_replace('subscribe', 'url', $url, $str);
932 }
933 return $str;
934 }
935
936 /**
937 * Replace subscription-invitation tokens
938 *
77855840
TO
939 * @param string $str
940 * The string with tokens to be replaced.
6a488035 941 *
a6c01b45
CW
942 * @return string
943 * The processed string
6a488035
TO
944 */
945 public static function &replaceSubscribeInviteTokens($str) {
946 if (preg_match('/\{action\.subscribeUrl\}/', $str)) {
947 $url = CRM_Utils_System::url('civicrm/mailing/subscribe',
948 'reset=1',
3f390aa6 949 TRUE, NULL, FALSE, TRUE
6a488035
TO
950 );
951 $str = preg_replace('/\{action\.subscribeUrl\}/', $url, $str);
952 }
953
954 if (preg_match('/\{action\.subscribeUrl.\d+\}/', $str, $matches)) {
955 foreach ($matches as $key => $value) {
956 $gid = substr($value, 21, -1);
957 $url = CRM_Utils_System::url('civicrm/mailing/subscribe',
958 "reset=1&gid={$gid}",
3f390aa6 959 TRUE, NULL, FALSE, TRUE
6a488035 960 );
6a488035
TO
961 $str = preg_replace('/' . preg_quote($value) . '/', $url, $str);
962 }
963 }
964
965 if (preg_match('/\{action\.subscribe.\d+\}/', $str, $matches)) {
966 foreach ($matches as $key => $value) {
353ffa53
TO
967 $gid = substr($value, 18, -1);
968 $config = CRM_Core_Config::singleton();
969 $domain = CRM_Core_BAO_MailSettings::defaultDomain();
6a488035
TO
970 $localpart = CRM_Core_BAO_MailSettings::defaultLocalpart();
971 // we add the 0.0000000000000000 part to make this match the other email patterns (with action, two ids and a hash)
972 $str = preg_replace('/' . preg_quote($value) . '/', "mailto:{$localpart}s.{$gid}.0.0000000000000000@$domain", $str);
973 }
974 }
975 return $str;
976 }
977
978 /**
979 * Replace welcome/confirmation tokens
980 *
77855840
TO
981 * @param string $str
982 * The string with tokens to be replaced.
983 * @param string $group
984 * The name of the group being subscribed.
985 * @param bool $html
986 * Replace tokens with html or plain text.
6a488035 987 *
a6c01b45
CW
988 * @return string
989 * The processed string
6a488035
TO
990 */
991 public static function &replaceWelcomeTokens($str, $group, $html) {
992 if (self::token_match('welcome', 'group', $str)) {
993 self::token_replace('welcome', 'group', $group, $str);
994 }
995 return $str;
996 }
997
998 /**
999 * Find unprocessed tokens (call this last)
1000 *
77855840
TO
1001 * @param string $str
1002 * The string to search.
6a488035 1003 *
a6c01b45
CW
1004 * @return array
1005 * Array of tokens that weren't replaced
6a488035
TO
1006 */
1007 public static function &unmatchedTokens(&$str) {
1008 //preg_match_all('/[^\{\\\\]\{(\w+\.\w+)\}[^\}]/', $str, $match);
1009 preg_match_all('/\{(\w+\.\w+)\}/', $str, $match);
1010 return $match[1];
1011 }
1012
1013 /**
fe482240 1014 * Find and replace tokens for each component.
6a488035 1015 *
77855840
TO
1016 * @param string $str
1017 * The string to search.
1018 * @param array $contact
1019 * Associative array of contact properties.
1020 * @param array $components
1021 * A list of tokens that are known to exist in the email body.
6a488035 1022 *
e39893f5
EM
1023 * @param bool $escapeSmarty
1024 * @param bool $returnEmptyToken
1025 *
a6c01b45
CW
1026 * @return string
1027 * The processed string
79594ce7
EM
1028 *
1029 * @deprecated
6a488035 1030 */
79594ce7
EM
1031 public static function replaceComponentTokens(&$str, $contact, $components, $escapeSmarty = FALSE, $returnEmptyToken = TRUE) {
1032 CRM_Core_Error::deprecatedFunctionWarning('use the token processor');
6a488035
TO
1033 if (!is_array($components) || empty($contact)) {
1034 return $str;
1035 }
1036
1037 foreach ($components as $name => $tokens) {
1038 if (!is_array($tokens) || empty($tokens)) {
1039 continue;
1040 }
1041
1042 foreach ($tokens as $token) {
1043 if (self::token_match($name, $token, $str) && isset($contact[$name . '.' . $token])) {
1044 self::token_replace($name, $token, $contact[$name . '.' . $token], $str, $escapeSmarty);
1045 }
1046 elseif (!$returnEmptyToken) {
1047 //replacing empty token
1048 self::token_replace($name, $token, "", $str, $escapeSmarty);
1049 }
1050 }
1051 }
1052 return $str;
1053 }
1054
1055 /**
fe482240 1056 * Get array of string tokens.
6a488035 1057 *
5a4f6742 1058 * @param string $string
77855840 1059 * The input string to parse for tokens.
6a488035 1060 *
a6c01b45
CW
1061 * @return array
1062 * array of tokens mentioned in field
6a488035 1063 */
00be9182 1064 public static function getTokens($string) {
be2fb01f
CW
1065 $matches = [];
1066 $tokens = [];
ad06f98f 1067 preg_match_all('/(?<!\{|\\\\)\{(\w+\.\w+(:|.)?\w*)\}(?!\})/',
6a488035
TO
1068 $string,
1069 $matches,
1070 PREG_PATTERN_ORDER
1071 );
1072
1073 if ($matches[1]) {
1074 foreach ($matches[1] as $token) {
ad06f98f
EM
1075 $parts = explode('.', $token, 3);
1076 $type = $parts[0];
1077 $name = $parts[1];
1078 $suffix = !empty($parts[2]) ? ('.' . $parts[2]) : '';
6a488035
TO
1079 if ($name && $type) {
1080 if (!isset($tokens[$type])) {
be2fb01f 1081 $tokens[$type] = [];
6a488035 1082 }
ad06f98f 1083 $tokens[$type][] = $name . $suffix;
6a488035
TO
1084 }
1085 }
1086 }
1087 return $tokens;
1088 }
1089
bdd49e38
EM
1090 /**
1091 * Function to determine which values to retrieve to insert into tokens. The heavy resemblance between this function
1092 * and getTokens appears to be historical rather than intentional and should be reviewed
1093 * @param $string
a6c01b45
CW
1094 * @return array
1095 * fields to pass in as return properties when populating token
bdd49e38
EM
1096 */
1097 public static function getReturnProperties(&$string) {
be2fb01f
CW
1098 $returnProperties = [];
1099 $matches = [];
e7292422 1100 preg_match_all('/(?<!\{|\\\\)\{(\w+\.\w+)\}(?!\})/',
353ffa53
TO
1101 $string,
1102 $matches,
1103 PREG_PATTERN_ORDER
1104 );
e7292422
TO
1105 if ($matches[1]) {
1106 foreach ($matches[1] as $token) {
3017ca78 1107 [$type, $name] = preg_split('/\./', $token, 2);
e7292422
TO
1108 if ($name) {
1109 $returnProperties["{$name}"] = 1;
bdd49e38
EM
1110 }
1111 }
e7292422 1112 }
bdd49e38 1113
e7292422 1114 return $returnProperties;
bdd49e38
EM
1115 }
1116
6a488035 1117 /**
100fef9d 1118 * Gives required details of contacts in an indexed array format so we
6a488035
TO
1119 * can iterate in a nice loop and do token evaluation
1120 *
36f5faa3 1121 * @param array $contactIDs
77855840
TO
1122 * @param array $returnProperties
1123 * Of required properties.
e7483cbe 1124 * @param bool $skipOnHold Don't return on_hold contact info also.
77855840 1125 * Don't return on_hold contact info also.
e7483cbe 1126 * @param bool $skipDeceased Don't return deceased contact info.
77855840
TO
1127 * Don't return deceased contact info.
1128 * @param array $extraParams
bf02bdac 1129 * Extra params - DEPRECATED
77855840
TO
1130 * @param array $tokens
1131 * The list of tokens we've extracted from the content.
4d385723 1132 * @param string|null $className
1133 * @param int|null $jobID
77855840 1134 * The mailing list jobID - this is a legacy param.
6a488035 1135 *
4d385723 1136 * @return array - e.g [[1 => ['first_name' => 'bob'...], 34 => ['first_name' => 'fred'...]]]
6a488035 1137 */
e7483cbe 1138 public static function getTokenDetails(
a3e55d9c 1139 $contactIDs,
6a488035 1140 $returnProperties = NULL,
e7292422
TO
1141 $skipOnHold = TRUE,
1142 $skipDeceased = TRUE,
1143 $extraParams = NULL,
be2fb01f 1144 $tokens = [],
e7292422
TO
1145 $className = NULL,
1146 $jobID = NULL
6a488035 1147 ) {
0b3cb19d 1148
be2fb01f 1149 $params = [];
36f5faa3 1150 foreach ($contactIDs as $contactID) {
be2fb01f 1151 $params[] = [
6a488035 1152 CRM_Core_Form::CB_PREFIX . $contactID,
353ffa53
TO
1153 '=',
1154 1,
1155 0,
1156 0,
be2fb01f 1157 ];
6a488035
TO
1158 }
1159
1160 // fix for CRM-2613
1161 if ($skipDeceased) {
be2fb01f 1162 $params[] = ['is_deceased', '=', 0, 0, 0];
6a488035
TO
1163 }
1164
1165 //fix for CRM-3798
1166 if ($skipOnHold) {
be2fb01f 1167 $params[] = ['on_hold', '=', 0, 0, 0];
6a488035
TO
1168 }
1169
1170 if ($extraParams) {
bf02bdac 1171 CRM_Core_Error::deprecatedWarning('Passing $extraParams to getTokenDetails() is not supported and will be removed in a future version');
6a488035
TO
1172 $params = array_merge($params, $extraParams);
1173 }
1174
1175 // if return properties are not passed then get all return properties
1176 if (empty($returnProperties)) {
1177 $fields = array_merge(array_keys(CRM_Contact_BAO_Contact::exportableFields()),
be2fb01f 1178 ['display_name', 'checksum', 'contact_id']
6a488035 1179 );
36f5faa3 1180 foreach ($fields as $val) {
b8a4ead8 1181 // The unavailable fields are not available as tokens, do not have a one-2-one relationship
1182 // with contacts and are expensive to resolve.
1183 // @todo see CRM-17253 - there are some other fields (e.g note) that should be excluded
1184 // and upstream calls to this should populate return properties.
be2fb01f 1185 $unavailableFields = ['group', 'tag'];
b8a4ead8 1186 if (!in_array($val, $unavailableFields)) {
1187 $returnProperties[$val] = 1;
1188 }
6a488035
TO
1189 }
1190 }
1191
be2fb01f 1192 $custom = [];
6a488035
TO
1193 foreach ($returnProperties as $name => $dontCare) {
1194 $cfID = CRM_Core_BAO_CustomField::getKeyID($name);
1195 if ($cfID) {
1196 $custom[] = $cfID;
1197 }
1198 }
1199
4d385723 1200 [$contactDetails] = CRM_Contact_BAO_Query::apiQuery($params, $returnProperties, NULL, NULL, 0, count($contactIDs), TRUE, FALSE, TRUE, CRM_Contact_BAO_Query::MODE_CONTACTS, NULL, TRUE);
6a488035 1201
36f5faa3 1202 foreach ($contactIDs as $contactID) {
6a488035 1203 if (array_key_exists($contactID, $contactDetails)) {
5973d2e1 1204 if (!empty($contactDetails[$contactID]['preferred_communication_method'])
6a488035 1205 ) {
be2fb01f 1206 $communicationPreferences = [];
160e7328 1207 foreach ((array) $contactDetails[$contactID]['preferred_communication_method'] as $val) {
6a488035 1208 if ($val) {
5973d2e1 1209 $communicationPreferences[$val] = CRM_Core_PseudoConstant::getLabel('CRM_Contact_DAO_Contact', 'preferred_communication_method', $val);
6a488035
TO
1210 }
1211 }
5973d2e1 1212 $contactDetails[$contactID]['preferred_communication_method'] = implode(', ', $communicationPreferences);
6a488035
TO
1213 }
1214
1215 foreach ($custom as $cfID) {
1216 if (isset($contactDetails[$contactID]["custom_{$cfID}"])) {
8cee0c70 1217 $contactDetails[$contactID]["custom_{$cfID}"] = CRM_Core_BAO_CustomField::displayValue($contactDetails[$contactID]["custom_{$cfID}"], $cfID);
6a488035
TO
1218 }
1219 }
1220
50bfb460 1221 // special case for greeting replacement
be2fb01f 1222 foreach ([
6714d8d2
SL
1223 'email_greeting',
1224 'postal_greeting',
1225 'addressee',
1226 ] as $val) {
a7488080 1227 if (!empty($contactDetails[$contactID][$val])) {
6a488035
TO
1228 $contactDetails[$contactID][$val] = $contactDetails[$contactID]["{$val}_display"];
1229 }
1230 }
1231 }
1232 }
1233
590111ef 1234 // $contactDetails = &$details[0] = is an array of [ contactID => contactDetails ]
6a488035 1235 // also call a hook and get token details
590111ef 1236 CRM_Utils_Hook::tokenValues($contactDetails,
6a488035
TO
1237 $contactIDs,
1238 $jobID,
1239 $tokens,
1240 $className
1241 );
4d385723 1242 return [$contactDetails];
6a488035
TO
1243 }
1244
d20c4dad
EM
1245 /**
1246 * Call hooks on tokens for anonymous users - contact id is set to 0 - this allows non-contact
1247 * specific tokens to be rendered
1248 *
77855840
TO
1249 * @param array $contactIDs
1250 * This should always be array(0) or its not anonymous - left to keep signature same.
16b10e64 1251 * as main fn
d20c4dad 1252 * @param string $returnProperties
77855840
TO
1253 * @param bool $skipOnHold
1254 * @param bool $skipDeceased
d20c4dad
EM
1255 * @param string $extraParams
1256 * @param array $tokens
77855840
TO
1257 * @param string $className
1258 * Sent as context to the hook.
d20c4dad 1259 * @param string $jobID
a6c01b45
CW
1260 * @return array
1261 * contactDetails with hooks swapped out
d20c4dad 1262 */
590111ef 1263 public static function getAnonymousTokenDetails($contactIDs = [0],
353ffa53
TO
1264 $returnProperties = NULL,
1265 $skipOnHold = TRUE,
1266 $skipDeceased = TRUE,
1267 $extraParams = NULL,
be2fb01f 1268 $tokens = [],
353ffa53
TO
1269 $className = NULL,
1270 $jobID = NULL) {
be2fb01f 1271 $details = [0 => []];
e7292422
TO
1272 // also call a hook and get token details
1273 CRM_Utils_Hook::tokenValues($details[0],
d20c4dad
EM
1274 $contactIDs,
1275 $jobID,
1276 $tokens,
1277 $className
1278 );
1279 return $details;
1280 }
e39893f5 1281
2d3e3c7b 1282 /**
fe482240 1283 * Get Membership Token Details.
77855840
TO
1284 * @param array $membershipIDs
1285 * Array of membership IDS.
2d3e3c7b 1286 */
00be9182 1287 public static function getMembershipTokenDetails($membershipIDs) {
be2fb01f
CW
1288 $memberships = civicrm_api3('membership', 'get', [
1289 'options' => ['limit' => 0],
1290 'membership_id' => ['IN' => (array) $membershipIDs],
1291 ]);
2d3e3c7b 1292 return $memberships['values'];
1293 }
353ffa53 1294
6a488035 1295 /**
50bfb460 1296 * Replace existing greeting tokens in message/subject.
54957108 1297 *
206f0198
CB
1298 * This function operates by reference, modifying the first parameter. Other
1299 * methods for token replacement in this class return the modified string.
1300 * This leads to inconsistency in how these methods must be applied.
1301 *
1302 * @TODO Remove that inconsistency in usage.
1303 *
1304 * ::replaceContactTokens() may need to be called after this method, to
1305 * replace tokens supplied from this method.
1306 *
54957108 1307 * @param string $tokenString
1308 * @param array $contactDetails
1309 * @param int $contactId
1310 * @param string $className
1311 * @param bool $escapeSmarty
6a488035 1312 */
00be9182 1313 public static function replaceGreetingTokens(&$tokenString, $contactDetails = NULL, $contactId = NULL, $className = NULL, $escapeSmarty = FALSE) {
6a488035
TO
1314
1315 if (!$contactDetails && !$contactId) {
1316 return;
1317 }
1318
1319 // check if there are any tokens
1320 $greetingTokens = self::getTokens($tokenString);
1321
1322 if (!empty($greetingTokens)) {
1323 // first use the existing contact object for token replacement
1324 if (!empty($contactDetails)) {
73d64eb6 1325 $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString, $contactDetails, TRUE, $greetingTokens, TRUE, $escapeSmarty);
6a488035
TO
1326 }
1327
578c7a3a 1328 self::removeNullContactTokens($tokenString, $contactDetails, $greetingTokens);
6a488035
TO
1329 // check if there are any unevaluated tokens
1330 $greetingTokens = self::getTokens($tokenString);
1331
e7483cbe
J
1332 // $greetingTokens not empty, means there are few tokens which are not
1333 // evaluated, like custom data etc
6a488035
TO
1334 // so retrieve it from database
1335 if (!empty($greetingTokens) && array_key_exists('contact', $greetingTokens)) {
1336 $greetingsReturnProperties = array_flip(CRM_Utils_Array::value('contact', $greetingTokens));
1337 $greetingsReturnProperties = array_fill_keys(array_keys($greetingsReturnProperties), 1);
be2fb01f 1338 $contactParams = ['contact_id' => $contactId];
6a488035
TO
1339
1340 $greetingDetails = self::getTokenDetails($contactParams,
1341 $greetingsReturnProperties,
1342 FALSE, FALSE, NULL,
1343 $greetingTokens,
1344 $className
1345 );
1346
1347 // again replace tokens
1348 $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString,
1349 $greetingDetails,
1350 TRUE,
73d64eb6 1351 $greetingTokens,
f551f7d3 1352 TRUE,
73d64eb6 1353 $escapeSmarty
6a488035
TO
1354 );
1355 }
d75f2f47 1356
ac21a108 1357 // check if there are still any unevaluated tokens
562cf4d7 1358 $remainingTokens = self::getTokens($tokenString);
ac21a108 1359
f551f7d3
NH
1360 // $greetingTokens not empty, there are customized or hook tokens to replace
1361 if (!empty($remainingTokens)) {
ac21a108 1362 // Fill the return properties array
f551f7d3 1363 $greetingTokens = $remainingTokens;
ac21a108 1364 reset($greetingTokens);
be2fb01f 1365 $greetingsReturnProperties = [];
78f2de98
SL
1366 foreach ($greetingTokens as $value) {
1367 $props = array_flip($value);
ac21a108
JM
1368 $props = array_fill_keys(array_keys($props), 1);
1369 $greetingsReturnProperties = $greetingsReturnProperties + $props;
1370 }
be2fb01f 1371 $contactParams = ['contact_id' => $contactId];
ac21a108
JM
1372 $greetingDetails = self::getTokenDetails($contactParams,
1373 $greetingsReturnProperties,
1374 FALSE, FALSE, NULL,
1375 $greetingTokens,
1376 $className
1377 );
1378 // Prepare variables for calling replaceHookTokens
1379 $categories = array_keys($greetingTokens);
3017ca78 1380 [$contact] = $greetingDetails;
ac21a108
JM
1381 // Replace tokens defined in Hooks.
1382 $tokenString = CRM_Utils_Token::replaceHookTokens($tokenString, $contact[$contactId], $categories);
1383 }
6a488035
TO
1384 }
1385 }
1386
663cc0b4
J
1387 /**
1388 * At this point, $contactDetails has loaded the contact from the DAO. Any
1389 * (non-custom) missing fields are null. By removing them, we can avoid
1390 * expensive calls to CRM_Contact_BAO_Query.
1391 *
1392 * @param string $tokenString
1393 * @param array $contactDetails
6714d8d2 1394 * @param array $greetingTokens
663cc0b4
J
1395 */
1396 private static function removeNullContactTokens(&$tokenString, $contactDetails, &$greetingTokens) {
dcdff6e6 1397
a2a7ed37
JK
1398 // Only applies to contact tokens
1399 if (!array_key_exists('contact', $greetingTokens)) {
1400 return;
1401 }
dcdff6e6 1402
663cc0b4
J
1403 $greetingTokensOriginal = $greetingTokens;
1404 $contactFieldList = CRM_Contact_DAO_Contact::fields();
578c7a3a
J
1405 // Sometimes contactDetails are in a multidemensional array, sometimes a
1406 // single-dimension array.
1407 if (array_key_exists(0, $contactDetails) && is_array($contactDetails[0])) {
1408 $contactDetails = current($contactDetails[0]);
1409 }
663cc0b4
J
1410 $nullFields = array_keys(array_diff_key($contactFieldList, $contactDetails));
1411
1412 // Handle legacy tokens
1413 foreach (self::legacyContactTokens() as $oldToken => $newToken) {
1414 if (CRM_Utils_Array::key($newToken, $nullFields)) {
1415 $nullFields[] = $oldToken;
1416 }
1417 }
1418
1419 // Remove null contact fields from $greetingTokens
1420 $greetingTokens['contact'] = array_diff($greetingTokens['contact'], $nullFields);
1421
1422 // Also remove them from $tokenString
1423 $removedTokens = array_diff($greetingTokensOriginal['contact'], $greetingTokens['contact']);
1424 // Handle legacy tokens again, sigh
1425 if (!empty($removedTokens)) {
1426 foreach ($removedTokens as $token) {
1427 if (CRM_Utils_Array::value($token, self::legacyContactTokens()) !== NULL) {
1428 $removedTokens[] = CRM_Utils_Array::value($token, self::legacyContactTokens());
1429 }
1430 }
1431 foreach ($removedTokens as $token) {
1432 $tokenString = str_replace("{contact.$token}", '', $tokenString);
1433 }
1434 }
1435 }
1436
5bc392e6
EM
1437 /**
1438 * @param $tokens
1439 *
1440 * @return array
1441 */
00be9182 1442 public static function flattenTokens(&$tokens) {
be2fb01f 1443 $flattenTokens = [];
6a488035 1444
be2fb01f 1445 foreach ([
6714d8d2
SL
1446 'html',
1447 'text',
1448 'subject',
1449 ] as $prop) {
6a488035
TO
1450 if (!isset($tokens[$prop])) {
1451 continue;
1452 }
1453 foreach ($tokens[$prop] as $type => $names) {
1454 if (!isset($flattenTokens[$type])) {
be2fb01f 1455 $flattenTokens[$type] = [];
6a488035
TO
1456 }
1457 foreach ($names as $name) {
1458 $flattenTokens[$type][$name] = 1;
1459 }
1460 }
1461 }
1462
1463 return $flattenTokens;
1464 }
1465
1466 /**
1467 * Replace all user tokens in $str
1468 *
77855840
TO
1469 * @param string $str
1470 * The string with tokens to be replaced.
e39893f5
EM
1471 *
1472 * @param null $knownTokens
1473 * @param bool $escapeSmarty
6a488035 1474 *
a6c01b45
CW
1475 * @return string
1476 * The processed string
6a488035
TO
1477 */
1478 public static function &replaceUserTokens($str, $knownTokens = NULL, $escapeSmarty = FALSE) {
1479 $key = 'user';
1480 if (!$knownTokens ||
1481 !isset($knownTokens[$key])
1482 ) {
1483 return $str;
1484 }
1485
8bab0eb0
DL
1486 $str = preg_replace_callback(
1487 self::tokenRegex($key),
353ffa53
TO
1488 function ($matches) use ($escapeSmarty) {
1489 return CRM_Utils_Token::getUserTokenReplacement($matches[1], $escapeSmarty);
8bab0eb0
DL
1490 },
1491 $str
6a488035
TO
1492 );
1493 return $str;
1494 }
1495
e39893f5
EM
1496 /**
1497 * @param $token
1498 * @param bool $escapeSmarty
1499 *
1500 * @return string
1501 */
6a488035
TO
1502 public static function getUserTokenReplacement($token, $escapeSmarty = FALSE) {
1503 $value = '';
1504
3017ca78 1505 [$objectName, $objectValue] = explode('-', $token, 2);
6a488035
TO
1506
1507 switch ($objectName) {
1508 case 'permission':
1509 $value = CRM_Core_Permission::permissionEmails($objectValue);
1510 break;
1511
1512 case 'role':
1513 $value = CRM_Core_Permission::roleEmails($objectValue);
1514 break;
1515 }
1516
1517 if ($escapeSmarty) {
1518 $value = self::tokenEscapeSmarty($value);
1519 }
1520
1521 return $value;
1522 }
1523
6a488035
TO
1524 protected static function _buildContributionTokens() {
1525 $key = 'contribution';
9b3cb77d 1526
17cca51e 1527 if (!isset(Civi::$statics[__CLASS__][__FUNCTION__][$key])) {
9b3cb77d 1528 $processor = new CRM_Contribute_Tokens();
da3ea3d0
SL
1529 $tokens = array_merge(CRM_Contribute_BAO_Contribution::exportableFields('All'),
1530 ['campaign' => [], 'financial_type' => [], 'payment_instrument' => []],
9b3cb77d
EM
1531 self::getCustomFieldTokens('Contribution'),
1532 $processor->getPseudoTokens()
da3ea3d0
SL
1533 );
1534 foreach ($tokens as $token) {
1535 if (!empty($token['name'])) {
1536 $tokens[$token['name']] = [];
1537 }
1538 }
17cca51e 1539 Civi::$statics[__CLASS__][__FUNCTION__][$key] = array_keys($tokens);
6a488035 1540 }
17cca51e 1541 self::$_tokens[$key] = Civi::$statics[__CLASS__][__FUNCTION__][$key];
6a488035
TO
1542 }
1543
2d3e3c7b 1544 /**
fe482240 1545 * Replace tokens for an entity.
2d3e3c7b 1546 * @param string $entity
77855840
TO
1547 * @param array $entityArray
1548 * (e.g. in format from api).
1549 * @param string $str
1550 * String to replace in.
1551 * @param array $knownTokens
1552 * Array of tokens present.
1553 * @param bool $escapeSmarty
a6c01b45
CW
1554 * @return string
1555 * string with replacements made
2d3e3c7b 1556 */
be2fb01f 1557 public static function replaceEntityTokens($entity, $entityArray, $str, $knownTokens = [], $escapeSmarty = FALSE) {
8cc574cf 1558 if (!$knownTokens || empty($knownTokens[$entity])) {
2d3e3c7b 1559 return $str;
1560 }
1561
07945b3c 1562 $fn = 'get' . ucfirst($entity) . 'TokenReplacement';
be2fb01f 1563 $fn = is_callable(['CRM_Utils_Token', $fn]) ? $fn : 'getApiTokenReplacement';
50bfb460 1564 // since we already know the tokens lets just use them & do str_replace which is faster & simpler than preg_replace
2d3e3c7b 1565 foreach ($knownTokens[$entity] as $token) {
2f5c0408
EM
1566 // We are now supporting the syntax case_type_id:label
1567 // so strip anything after the ':'
1568 // (we aren't supporting 'name' at this stage, so we can assume 'label'
1569 // test cover in TokenConsistencyTest.
1570 $parts = explode(':', $token);
1571 $replacement = self::$fn($entity, $parts[0], $entityArray);
07945b3c
CW
1572 if ($escapeSmarty) {
1573 $replacement = self::tokenEscapeSmarty($replacement);
1574 }
1575 $str = str_replace('{' . $entity . '.' . $token . '}', $replacement, $str);
2d3e3c7b 1576 }
07945b3c
CW
1577 return preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
1578 }
1579
1580 /**
662bee85
EM
1581 * @deprecated
1582 *
07945b3c 1583 * @param int $caseId
f274450a 1584 * @param string $str
07945b3c
CW
1585 * @param array $knownTokens
1586 * @param bool $escapeSmarty
1587 * @return string
1588 * @throws \CiviCRM_API3_Exception
1589 */
d0ce76fd
EM
1590 public static function replaceCaseTokens($caseId, $str, $knownTokens = NULL, $escapeSmarty = FALSE): string {
1591 if (strpos($str, '{case.') === FALSE) {
07945b3c
CW
1592 return $str;
1593 }
d0ce76fd
EM
1594 if (!$knownTokens) {
1595 $knownTokens = self::getTokens($str);
1596 }
be2fb01f 1597 $case = civicrm_api3('case', 'getsingle', ['id' => $caseId]);
07945b3c
CW
1598 return self::replaceEntityTokens('case', $case, $str, $knownTokens, $escapeSmarty);
1599 }
1600
1601 /**
1602 * Generic function for formatting token replacement for an api field
1603 *
79594ce7
EM
1604 * @deprecated
1605 *
07945b3c
CW
1606 * @param string $entity
1607 * @param string $token
1608 * @param array $entityArray
1609 * @return string
1610 * @throws \CiviCRM_API3_Exception
1611 */
1612 public static function getApiTokenReplacement($entity, $token, $entityArray) {
1613 if (!isset($entityArray[$token])) {
1614 return '';
1615 }
be2fb01f 1616 $field = civicrm_api3($entity, 'getfield', ['action' => 'get', 'name' => $token, 'get_options' => 'get']);
07945b3c 1617 $field = $field['values'];
9c1bc317 1618 $fieldType = $field['type'] ?? NULL;
6f9518ec
CW
1619 // Boolean fields
1620 if ($fieldType == CRM_Utils_Type::T_BOOLEAN && empty($field['options'])) {
be2fb01f 1621 $field['options'] = [ts('No'), ts('Yes')];
6f9518ec 1622 }
07945b3c
CW
1623 // Match pseudoconstants
1624 if (!empty($field['options'])) {
be2fb01f 1625 $ret = [];
07945b3c
CW
1626 foreach ((array) $entityArray[$token] as $val) {
1627 $ret[] = $field['options'][$val];
1628 }
1629 return implode(', ', $ret);
1630 }
6f9518ec 1631 // Format date fields
78ffc4d7 1632 elseif ($entityArray[$token] && in_array($fieldType, [CRM_Utils_Type::T_DATE, CRM_Utils_Type::T_TIMESTAMP, (CRM_Utils_Type::T_DATE + CRM_Utils_Type::T_TIME)])) {
07945b3c
CW
1633 return CRM_Utils_Date::customFormat($entityArray[$token]);
1634 }
6f9518ec 1635 return implode(', ', (array) $entityArray[$token]);
2d3e3c7b 1636 }
1637
383c047b 1638 /**
fe482240 1639 * Replace Contribution tokens in html.
e39893f5 1640 *
115dba92
EM
1641 * @param string $str
1642 * @param array $contribution
e39893f5 1643 * @param bool|string $html
383c047b 1644 * @param string $knownTokens
e39893f5
EM
1645 * @param bool|string $escapeSmarty
1646 *
72b3a70c 1647 * @return mixed
383c047b
DG
1648 */
1649 public static function replaceContributionTokens($str, &$contribution, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE) {
1650 $key = 'contribution';
b99f3e96 1651 if (!$knownTokens || empty($knownTokens[$key])) {
6714d8d2
SL
1652 //early return
1653 return $str;
383c047b 1654 }
6a488035
TO
1655
1656 // here we intersect with the list of pre-configured valid tokens
1657 // so that we remove anything we do not recognize
1658 // I hope to move this step out of here soon and
1659 // then we will just iterate on a list of tokens that are passed to us
6a488035 1660
8bab0eb0
DL
1661 $str = preg_replace_callback(
1662 self::tokenRegex($key),
353ffa53
TO
1663 function ($matches) use (&$contribution, $html, $escapeSmarty) {
1664 return CRM_Utils_Token::getContributionTokenReplacement($matches[1], $contribution, $html, $escapeSmarty);
8bab0eb0 1665 },
6a488035
TO
1666 $str
1667 );
1668
1669 $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
1670 return $str;
1671 }
1672
383c047b
DG
1673 /**
1674 * We have a situation where we are rendering more than one token in each field because we are combining
1675 * tokens from more than one contribution when pdf thank you letters are grouped (CRM-14367)
1676 *
1677 * The replaceContributionToken doesn't handle receive_date correctly in this scenario because of the formatting
1678 * it applies (other tokens are OK including date fields)
1679 *
1680 * So we sort this out & then call the main function. Note that we are not escaping smarty on this fields like the main function
1681 * does - but the fields is already being formatted through a date function
1682 *
1683 * @param string $separator
1684 * @param string $str
2f5a20da 1685 * @param array $contributions
1686 * @param array $knownTokens
e39893f5 1687 *
6f9518ec 1688 * @return string
383c047b 1689 */
2f5a20da 1690 public static function replaceMultipleContributionTokens(string $separator, string $str, array $contributions, array $knownTokens): string {
1691 foreach ($knownTokens['contribution'] ?? [] as $token) {
1692 $resolvedTokens = [];
1693 foreach ($contributions as $contribution) {
1694 $resolvedTokens[] = self::replaceContributionTokens('{contribution.' . $token . '}', $contribution, FALSE, $knownTokens);
383c047b 1695 }
2f5a20da 1696 $str = self::token_replace('contribution', $token, implode($separator, $resolvedTokens), $str);
383c047b 1697 }
2f5a20da 1698 return $str;
383c047b
DG
1699 }
1700
2d3e3c7b 1701 /**
1702 * Get replacement strings for any membership tokens (only a small number of tokens are implemnted in the first instance
1703 * - this is used by the pdfLetter task from membership search
d41a5d53
EM
1704 *
1705 * This is called via replaceEntityTokens.
1706 *
1707 * In the near term it will not be called at all from core as
1708 * the pdf letter task is updated to use the processor.
1709 *
1710 * @deprecated
1711 *
6f9518ec
CW
1712 * @param string $entity
1713 * should always be "membership"
2d3e3c7b 1714 * @param string $token
6f9518ec 1715 * field name
77855840
TO
1716 * @param array $membership
1717 * An api result array for a single membership.
6f9518ec 1718 * @return string token replacement
2d3e3c7b 1719 */
07945b3c 1720 public static function getMembershipTokenReplacement($entity, $token, $membership) {
d41a5d53
EM
1721 $supportedTokens = [
1722 'id',
1723 'status',
1724 'status_id',
1725 'type',
1726 'membership_type_id',
1727 'start_date',
1728 'join_date',
1729 'end_date',
1730 'fee',
1731 ];
e7292422
TO
1732 switch ($token) {
1733 case 'type':
d41a5d53
EM
1734 // membership_type_id would only be requested if the calling
1735 // class is mapping it to '{membership:membership_type_id:label'}
1736 case 'membership_type_id':
e7292422
TO
1737 $value = $membership['membership_name'];
1738 break;
1739
1740 case 'status':
d41a5d53
EM
1741 // status_id would only be requested if the calling
1742 // class is mapping it to '{membership:status_id:label'}
1743 case 'status_id':
e7292422
TO
1744 $statuses = CRM_Member_BAO_Membership::buildOptions('status_id');
1745 $value = $statuses[$membership['status_id']];
1746 break;
1747
1748 case 'fee':
92e4c2a5 1749 try {
be2fb01f 1750 $value = civicrm_api3('membership_type', 'getvalue', [
6714d8d2
SL
1751 'id' => $membership['membership_type_id'],
1752 'return' => 'minimum_fee',
1753 ]);
daec3f08 1754 $value = CRM_Utils_Money::format($value, NULL, NULL, TRUE);
e7292422
TO
1755 }
1756 catch (CiviCRM_API3_Exception $e) {
1757 // we can anticipate we will get an error if the minimum fee is set to 'NULL' because of the way the
1758 // api handles NULL (4.4)
1759 $value = 0;
1760 }
1761 break;
1762
1763 default:
d41a5d53 1764 if (in_array($token, $supportedTokens)) {
e7292422 1765 $value = $membership[$token];
7ab8b45b 1766 if (CRM_Utils_String::endsWith($token, '_date')) {
1767 $value = CRM_Utils_Date::customFormat($value);
1768 }
e7292422
TO
1769 }
1770 else {
50bfb460 1771 // ie unchanged
e7292422
TO
1772 $value = "{$entity}.{$token}";
1773 }
1774 break;
2d3e3c7b 1775 }
1776
2d3e3c7b 1777 return $value;
1778 }
1779
e39893f5
EM
1780 /**
1781 * @param $token
1782 * @param $contribution
1783 * @param bool $html
1784 * @param bool $escapeSmarty
1785 *
1786 * @return mixed|string
3017ca78 1787 * @throws \CRM_Core_Exception
e39893f5 1788 */
6a488035
TO
1789 public static function getContributionTokenReplacement($token, &$contribution, $html = FALSE, $escapeSmarty = FALSE) {
1790 self::_buildContributionTokens();
1791
1792 switch ($token) {
1793 case 'total_amount':
1794 case 'net_amount':
1795 case 'fee_amount':
1796 case 'non_deductible_amount':
2aa64508
JG
1797 // FIXME: Is this ever a multi-dimensional array? Why use retrieveValueRecursive()?
1798 $amount = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
1799 $currency = CRM_Utils_Array::retrieveValueRecursive($contribution, 'currency');
1800 $value = CRM_Utils_Money::format($amount, $currency);
6a488035
TO
1801 break;
1802
1803 case 'receive_date':
9a32c3e5 1804 case 'receipt_date':
6a488035 1805 $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
9a32c3e5
JF
1806 $config = CRM_Core_Config::singleton();
1807 $value = CRM_Utils_Date::customFormat($value, $config->dateformatDatetime);
6a488035
TO
1808 break;
1809
1810 default:
1811 if (!in_array($token, self::$_tokens['contribution'])) {
1812 $value = "{contribution.$token}";
1813 }
1814 else {
1815 $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
1816 }
1817 break;
1818 }
1819
6a488035
TO
1820 if ($escapeSmarty) {
1821 $value = self::tokenEscapeSmarty($value);
1822 }
1823 return $value;
1824 }
1825
091133bb 1826 /**
a6c01b45 1827 * @return array
6f9518ec 1828 * [legacy_token => new_token]
091133bb 1829 */
00be9182 1830 public static function legacyContactTokens() {
be2fb01f 1831 return [
091133bb
CW
1832 'individual_prefix' => 'prefix_id',
1833 'individual_suffix' => 'suffix_id',
1834 'gender' => 'gender_id',
aa62b355 1835 'communication_style' => 'communication_style_id',
be2fb01f 1836 ];
091133bb
CW
1837 }
1838
18c017c8 1839 /**
1840 * Get all custom field tokens of $entity
1841 *
1842 * @param string $entity
6714d8d2 1843 * @return array
18c017c8 1844 * return custom field tokens in array('custom_N' => 'label') format
1845 */
075c519b 1846 public static function getCustomFieldTokens($entity) {
be2fb01f 1847 $customTokens = [];
18c017c8 1848 foreach (CRM_Core_BAO_CustomField::getFields($entity) as $id => $info) {
075c519b 1849 $customTokens['custom_' . $id] = $info['label'] . ' :: ' . $info['groupTitle'];
18c017c8 1850 }
18c017c8 1851 return $customTokens;
1852 }
1853
ac0a3db5
CW
1854 /**
1855 * Formats a token list for the select2 widget
9950a1c9 1856 *
ac0a3db5
CW
1857 * @param $tokens
1858 * @return array
1859 */
00be9182 1860 public static function formatTokensForDisplay($tokens) {
be2fb01f 1861 $sorted = $output = [];
ac0a3db5
CW
1862
1863 // Sort in ascending order by ignoring word case
1864 natcasesort($tokens);
1865
1866 // Attempt to place tokens into optgroups
50bfb460 1867 // @todo These groupings could be better and less hackish. Getting them pre-grouped from upstream would be nice.
ac0a3db5
CW
1868 foreach ($tokens as $k => $v) {
1869 // Check to see if this token is already in a group e.g. for custom fields
1870 $split = explode(' :: ', $v);
1871 if (!empty($split[1])) {
be2fb01f 1872 $sorted[$split[1]][] = ['id' => $k, 'text' => $split[0]];
ac0a3db5
CW
1873 }
1874 // Group by entity
1875 else {
1876 $split = explode('.', trim($k, '{}'));
cc666210 1877 if (isset($split[1])) {
f274450a 1878 $entity = array_key_exists($split[1], CRM_Core_DAO_Address::export()) ? 'Address' : ucwords(str_replace('_', ' ', $split[0]));
cc666210
CW
1879 }
1880 else {
1881 $entity = 'Contact';
1882 }
be2fb01f 1883 $sorted[ts($entity)][] = ['id' => $k, 'text' => $v];
ac0a3db5
CW
1884 }
1885 }
1886
1887 ksort($sorted);
1888 foreach ($sorted as $k => $v) {
be2fb01f 1889 $output[] = ['text' => $k, 'children' => $v];
ac0a3db5
CW
1890 }
1891
1892 return $output;
1893 }
96025800 1894
ea8be131 1895 /**
1896 * @param $value
1897 * @param $token
1898 *
1899 * @return bool|int|mixed|string|null
1900 */
1901 protected static function convertPseudoConstantsUsingMetadata($value, $token) {
1902 // Convert pseudoconstants using metadata
1903 if ($value && is_numeric($value)) {
1904 $allFields = CRM_Contact_BAO_Contact::exportableFields('All');
1905 if (!empty($allFields[$token]['pseudoconstant'])) {
1906 $value = CRM_Core_PseudoConstant::getLabel('CRM_Contact_BAO_Contact', $token, $value);
1907 }
1908 }
1909 elseif ($value && CRM_Utils_String::endsWith($token, '_date')) {
1910 $value = CRM_Utils_Date::customFormat($value);
1911 }
1912 return $value;
1913 }
1914
58169187
EM
1915 /**
1916 * Get token deprecation information.
1917 *
1918 * @return array
1919 */
1920 public static function getTokenDeprecations(): array {
1921 return [
1922 'WorkFlowMessageTemplates' => [
1923 'contribution_invoice_receipt' => [
1924 '$display_name' => 'contact.display_name',
1925 ],
1f7cb15c
EM
1926 'contribution_online_receipt' => [
1927 '$contributeMode' => 'no longer available / relevant',
1928 '$first_name' => 'contact.first_name',
1929 '$last_name' => 'contact.last_name',
1930 '$displayName' => 'contact.display_name',
1931 ],
58169187
EM
1932 ],
1933 ];
1934 }
1935
6a488035 1936}