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