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