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