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