Merge pull request #4314 from cividesk/CRM-13227
[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
806 /**
807 * @param $token
808 * @param $contact
809 * @param $category
810 * @param bool $html
811 * @param bool $escapeSmarty
812 *
813 * @return mixed|string
814 */public static function getHookTokenReplacement(
815 $token,
816 &$contact,
817 $category,
818 $html = FALSE,
819 $escapeSmarty = FALSE
820 ) {
821 $value = CRM_Utils_Array::value("{$category}.{$token}", $contact);
822
823 if ($value && !$html) {
824 $value = str_replace('&amp;', '&', $value);
825 }
826
827 if ($escapeSmarty) {
828 $value = self::tokenEscapeSmarty($value);
829 }
830
831 return $value;
832 }
833
834 /**
835 * unescapeTokens removes any characters that caused the replacement routines to skip token replacement
836 * for example {{token}} or \{token} will result in {token} in the final email
837 *
838 * this routine will remove the extra backslashes and braces
839 *
840 * @param $str ref to the string that will be scanned and modified
841 * @return void this function works directly on the string that is passed
842 * @access public
843 * @static
844 */
845 public static function unescapeTokens(&$str) {
846 $str = preg_replace('/\\\\|\{(\{\w+\.\w+\})\}/', '\\1', $str);
847 }
848
849 /**
850 * Replace unsubscribe tokens
851 *
852 * @param string $str the string with tokens to be replaced
853 * @param object $domain The domain BAO
854 * @param array $groups The groups (if any) being unsubscribed
855 * @param boolean $html Replace tokens with html or plain text
856 * @param int $contact_id The contact ID
857 * @param string hash The security hash of the unsub event
858 *
859 * @return string The processed string
860 * @access public
861 * @static
862 */
863 public static function &replaceUnsubscribeTokens(
864 $str,
865 &$domain,
866 &$groups,
867 $html,
868 $contact_id,
869 $hash
870 ) {
871 if (self::token_match('unsubscribe', 'group', $str)) {
872 if (!empty($groups)) {
873 $config = CRM_Core_Config::singleton();
874 $base = CRM_Utils_System::baseURL();
875
876 // FIXME: an ugly hack for CRM-2035, to be dropped once CRM-1799 is implemented
877 $dao = new CRM_Contact_DAO_Group();
878 $dao->find();
879 while ($dao->fetch()) {
880 if (substr($dao->visibility, 0, 6) == 'Public') {
881 $visibleGroups[] = $dao->id;
882 }
883 }
884 $value = implode(', ', $groups);
885 self::token_replace('unsubscribe', 'group', $value, $str);
886 }
887 }
888 return $str;
889 }
890
891 /**
892 * Replace resubscribe tokens
893 *
894 * @param string $str the string with tokens to be replaced
895 * @param object $domain The domain BAO
896 * @param array $groups The groups (if any) being resubscribed
897 * @param boolean $html Replace tokens with html or plain text
898 * @param int $contact_id The contact ID
899 * @param string hash The security hash of the resub event
900 *
901 * @return string The processed string
902 * @access public
903 * @static
904 */
905 public static function &replaceResubscribeTokens($str, &$domain, &$groups, $html,
906 $contact_id, $hash
907 ) {
908 if (self::token_match('resubscribe', 'group', $str)) {
909 if (!empty($groups)) {
910 $value = implode(', ', $groups);
911 self::token_replace('resubscribe', 'group', $value, $str);
912 }
913 }
914 return $str;
915 }
916
917 /**
918 * Replace subscription-confirmation-request tokens
919 *
920 * @param string $str The string with tokens to be replaced
921 * @param string $group The name of the group being subscribed
922 * @param $url
923 * @param boolean $html Replace tokens with html or plain text
924 *
925 * @return string The processed string
926 * @access public
927 * @static
928 */
929 public static function &replaceSubscribeTokens($str, $group, $url, $html) {
930 if (self::token_match('subscribe', 'group', $str)) {
931 self::token_replace('subscribe', 'group', $group, $str);
932 }
933 if (self::token_match('subscribe', 'url', $str)) {
934 self::token_replace('subscribe', 'url', $url, $str);
935 }
936 return $str;
937 }
938
939 /**
940 * Replace subscription-invitation tokens
941 *
942 * @param string $str The string with tokens to be replaced
943 *
944 * @return string The processed string
945 * @access public
946 * @static
947 */
948 public static function &replaceSubscribeInviteTokens($str) {
949 if (preg_match('/\{action\.subscribeUrl\}/', $str)) {
950 $url = CRM_Utils_System::url('civicrm/mailing/subscribe',
951 'reset=1',
952 TRUE, NULL, TRUE, TRUE
953 );
954 $str = preg_replace('/\{action\.subscribeUrl\}/', $url, $str);
955 }
956
957 if (preg_match('/\{action\.subscribeUrl.\d+\}/', $str, $matches)) {
958 foreach ($matches as $key => $value) {
959 $gid = substr($value, 21, -1);
960 $url = CRM_Utils_System::url('civicrm/mailing/subscribe',
961 "reset=1&gid={$gid}",
962 TRUE, NULL, TRUE, TRUE
963 );
964 $url = str_replace('&amp;', '&', $url);
965 $str = preg_replace('/' . preg_quote($value) . '/', $url, $str);
966 }
967 }
968
969 if (preg_match('/\{action\.subscribe.\d+\}/', $str, $matches)) {
970 foreach ($matches as $key => $value) {
971 $gid = substr($value, 18, -1);
972 $config = CRM_Core_Config::singleton();
973 $domain = CRM_Core_BAO_MailSettings::defaultDomain();
974 $localpart = CRM_Core_BAO_MailSettings::defaultLocalpart();
975 // we add the 0.0000000000000000 part to make this match the other email patterns (with action, two ids and a hash)
976 $str = preg_replace('/' . preg_quote($value) . '/', "mailto:{$localpart}s.{$gid}.0.0000000000000000@$domain", $str);
977 }
978 }
979 return $str;
980 }
981
982 /**
983 * Replace welcome/confirmation tokens
984 *
985 * @param string $str The string with tokens to be replaced
986 * @param string $group The name of the group being subscribed
987 * @param boolean $html Replace tokens with html or plain text
988 *
989 * @return string The processed string
990 * @access public
991 * @static
992 */
993 public static function &replaceWelcomeTokens($str, $group, $html) {
994 if (self::token_match('welcome', 'group', $str)) {
995 self::token_replace('welcome', 'group', $group, $str);
996 }
997 return $str;
998 }
999
1000 /**
1001 * Find unprocessed tokens (call this last)
1002 *
1003 * @param string $str The string to search
1004 *
1005 * @return array Array of tokens that weren't replaced
1006 * @access public
1007 * @static
1008 */
1009 public static function &unmatchedTokens(&$str) {
1010 //preg_match_all('/[^\{\\\\]\{(\w+\.\w+)\}[^\}]/', $str, $match);
1011 preg_match_all('/\{(\w+\.\w+)\}/', $str, $match);
1012 return $match[1];
1013 }
1014
1015 /**
1016 * Find and replace tokens for each component
1017 *
1018 * @param string $str The string to search
1019 * @param array $contact Associative array of contact properties
1020 * @param array $components A list of tokens that are known to exist in the email body
1021 *
1022 * @param bool $escapeSmarty
1023 * @param bool $returnEmptyToken
1024 *
1025 * @return string The processed string
1026 * @access public
1027 * @static
1028 */
1029 public static function &replaceComponentTokens(&$str, $contact, $components, $escapeSmarty = FALSE, $returnEmptyToken = TRUE) {
1030 if (!is_array($components) || empty($contact)) {
1031 return $str;
1032 }
1033
1034 foreach ($components as $name => $tokens) {
1035 if (!is_array($tokens) || empty($tokens)) {
1036 continue;
1037 }
1038
1039 foreach ($tokens as $token) {
1040 if (self::token_match($name, $token, $str) && isset($contact[$name . '.' . $token])) {
1041 self::token_replace($name, $token, $contact[$name . '.' . $token], $str, $escapeSmarty);
1042 }
1043 elseif (!$returnEmptyToken) {
1044 //replacing empty token
1045 self::token_replace($name, $token, "", $str, $escapeSmarty);
1046 }
1047 }
1048 }
1049 return $str;
1050 }
1051
1052 /**
1053 * Get array of string tokens
1054 *
1055 * @param $string the input string to parse for tokens
1056 *
1057 * @return array $tokens array of tokens mentioned in field@access public
1058 * @static
1059 */
1060 static function getTokens($string) {
1061 $matches = array();
1062 $tokens = array();
1063 preg_match_all('/(?<!\{|\\\\)\{(\w+\.\w+)\}(?!\})/',
1064 $string,
1065 $matches,
1066 PREG_PATTERN_ORDER
1067 );
1068
1069 if ($matches[1]) {
1070 foreach ($matches[1] as $token) {
1071 list($type, $name) = preg_split('/\./', $token, 2);
1072 if ($name && $type) {
1073 if (!isset($tokens[$type])) {
1074 $tokens[$type] = array();
1075 }
1076 $tokens[$type][] = $name;
1077 }
1078 }
1079 }
1080 return $tokens;
1081 }
1082
1083 /**
1084 * gives required details of contacts in an indexed array format so we
1085 * can iterate in a nice loop and do token evaluation
1086 *
1087 * @param $contactIDs
1088 * @param array $returnProperties of required properties
1089 * @param boolean $skipOnHold don't return on_hold contact info also.
1090 * @param boolean $skipDeceased don't return deceased contact info.
1091 * @param array $extraParams extra params
1092 * @param array $tokens the list of tokens we've extracted from the content
1093 * @param null $className
1094 * @param int $jobID the mailing list jobID - this is a legacy param
1095 *
1096 * @internal param array $contactIds of contacts
1097 * @return array
1098 * @access public
1099 * @static
1100 */
1101 static function getTokenDetails($contactIDs,
1102 $returnProperties = NULL,
1103 $skipOnHold = TRUE,
1104 $skipDeceased = TRUE,
1105 $extraParams = NULL,
1106 $tokens = array(),
1107 $className = NULL,
1108 $jobID = NULL
1109 ) {
1110 if (empty($contactIDs)) {
1111 // putting a fatal here so we can track if/when this happens
1112 CRM_Core_Error::fatal();
1113 }
1114
1115 $params = array();
1116 foreach ($contactIDs as $key => $contactID) {
1117 $params[] = array(
1118 CRM_Core_Form::CB_PREFIX . $contactID,
1119 '=', 1, 0, 0,
1120 );
1121 }
1122
1123 // fix for CRM-2613
1124 if ($skipDeceased) {
1125 $params[] = array('is_deceased', '=', 0, 0, 0);
1126 }
1127
1128 //fix for CRM-3798
1129 if ($skipOnHold) {
1130 $params[] = array('on_hold', '=', 0, 0, 0);
1131 }
1132
1133 if ($extraParams) {
1134 $params = array_merge($params, $extraParams);
1135 }
1136
1137 // if return properties are not passed then get all return properties
1138 if (empty($returnProperties)) {
1139 $fields = array_merge(array_keys(CRM_Contact_BAO_Contact::exportableFields()),
1140 array('display_name', 'checksum', 'contact_id')
1141 );
1142 foreach ($fields as $key => $val) {
1143 $returnProperties[$val] = 1;
1144 }
1145 }
1146
1147 $custom = array();
1148 foreach ($returnProperties as $name => $dontCare) {
1149 $cfID = CRM_Core_BAO_CustomField::getKeyID($name);
1150 if ($cfID) {
1151 $custom[] = $cfID;
1152 }
1153 }
1154
1155 //get the total number of contacts to fetch from database.
1156 $numberofContacts = count($contactIDs);
1157 $query = new CRM_Contact_BAO_Query($params, $returnProperties);
1158
1159 $details = $query->apiQuery($params, $returnProperties, NULL, NULL, 0, $numberofContacts);
1160
1161 $contactDetails = &$details[0];
1162
1163 foreach ($contactIDs as $key => $contactID) {
1164 if (array_key_exists($contactID, $contactDetails)) {
1165 if (CRM_Utils_Array::value('preferred_communication_method', $returnProperties) == 1
1166 && array_key_exists('preferred_communication_method', $contactDetails[$contactID])
1167 ) {
1168 $pcm = CRM_Core_PseudoConstant::get('CRM_Contact_DAO_Contact', 'preferred_communication_method');
1169
1170 // communication Prefferance
1171 $contactPcm = explode(CRM_Core_DAO::VALUE_SEPARATOR,
1172 $contactDetails[$contactID]['preferred_communication_method']
1173 );
1174 $result = array();
1175 foreach ($contactPcm as $key => $val) {
1176 if ($val) {
1177 $result[$val] = $pcm[$val];
1178 }
1179 }
1180 $contactDetails[$contactID]['preferred_communication_method'] = implode(', ', $result);
1181 }
1182
1183 foreach ($custom as $cfID) {
1184 if (isset($contactDetails[$contactID]["custom_{$cfID}"])) {
1185 $contactDetails[$contactID]["custom_{$cfID}"] = CRM_Core_BAO_CustomField::getDisplayValue($contactDetails[$contactID]["custom_{$cfID}"],
1186 $cfID, $details[1]
1187 );
1188 }
1189 }
1190
1191 //special case for greeting replacement
1192 foreach (array(
1193 'email_greeting', 'postal_greeting', 'addressee') as $val) {
1194 if (!empty($contactDetails[$contactID][$val])) {
1195 $contactDetails[$contactID][$val] = $contactDetails[$contactID]["{$val}_display"];
1196 }
1197 }
1198 }
1199 }
1200
1201 // also call a hook and get token details
1202 CRM_Utils_Hook::tokenValues($details[0],
1203 $contactIDs,
1204 $jobID,
1205 $tokens,
1206 $className
1207 );
1208 return $details;
1209 }
1210
1211 /**
1212 * Call hooks on tokens for anonymous users - contact id is set to 0 - this allows non-contact
1213 * specific tokens to be rendered
1214 *
1215 * @param array $contactIDs - this should always be array(0) or its not anonymous - left to keep signature same
1216 * as main fn
1217 * @param string $returnProperties
1218 * @param boolean $skipOnHold
1219 * @param boolean $skipDeceased
1220 * @param string $extraParams
1221 * @param array $tokens
1222 * @param string $className sent as context to the hook
1223 * @param string $jobID
1224 * @return array contactDetails with hooks swapped out
1225 */
1226 function getAnonymousTokenDetails($contactIDs = array(0),
1227 $returnProperties = NULL,
1228 $skipOnHold = TRUE,
1229 $skipDeceased = TRUE,
1230 $extraParams = NULL,
1231 $tokens = array(),
1232 $className = NULL,
1233 $jobID = NULL) {
1234 $details = array(0 => array());
1235 // also call a hook and get token details
1236 CRM_Utils_Hook::tokenValues($details[0],
1237 $contactIDs,
1238 $jobID,
1239 $tokens,
1240 $className
1241 );
1242 return $details;
1243 }
1244
1245 /**
1246 * gives required details of contribuion in an indexed array format so we
1247 * can iterate in a nice loop and do token evaluation
1248 *
1249 * @param $contributionIDs
1250 * @param array $returnProperties of required properties
1251 * @param array $extraParams extra params
1252 * @param array $tokens the list of tokens we've extracted from the content
1253 *
1254 * @param null $className
1255 *
1256 * @internal param array $contributionId one contribution id
1257 * @internal param bool $skipOnHold don't return on_hold contact info.
1258 * @internal param bool $skipDeceased don't return deceased contact info.
1259 * @return array
1260 * @access public
1261 * @static
1262 */
1263 static function getContributionTokenDetails($contributionIDs,
1264 $returnProperties = NULL,
1265 $extraParams = NULL,
1266 $tokens = array(),
1267 $className = NULL
1268 ) {
1269 //@todo - this function basically replications calling civicrm_api3('contribution', 'get', array('id' => array('IN' => array())
1270 if (empty($contributionIDs)) {
1271 // putting a fatal here so we can track if/when this happens
1272 CRM_Core_Error::fatal();
1273 }
1274
1275 $details = array();
1276
1277 // no apiQuery helper yet, so do a loop and find contribution by id
1278 foreach ($contributionIDs as $contributionID) {
1279
1280 $dao = new CRM_Contribute_DAO_Contribution();
1281 $dao->id = $contributionID;
1282
1283 if ($dao->find(TRUE)) {
1284
1285 $details[$dao->id] = array();
1286 CRM_Core_DAO::storeValues($dao, $details[$dao->id]);
1287
1288 // do the necessary transformation
1289 if (!empty($details[$dao->id]['payment_instrument_id'])) {
1290 $piId = $details[$dao->id]['payment_instrument_id'];
1291 $pis = CRM_Contribute_PseudoConstant::paymentInstrument();
1292 $details[$dao->id]['payment_instrument'] = $pis[$piId];
1293 }
1294 if (!empty($details[$dao->id]['campaign_id'])) {
1295 $campaignId = $details[$dao->id]['campaign_id'];
1296 $campaigns = CRM_Campaign_BAO_Campaign::getCampaigns($campaignId);
1297 $details[$dao->id]['campaign'] = $campaigns[$campaignId];
1298 }
1299
1300 if (!empty($details[$dao->id]['financial_type_id'])) {
1301 $financialtypeId = $details[$dao->id]['financial_type_id'];
1302 $ftis = CRM_Contribute_PseudoConstant::financialType();
1303 $details[$dao->id]['financial_type'] = $ftis[$financialtypeId];
1304 }
1305
1306 // TODO: call a hook to get token contribution details
1307 }
1308 }
1309
1310 return $details;
1311 }
1312
1313 /**
1314 * Get Membership Token Details
1315 * @param array $membershipIDs array of membership IDS
1316 */
1317 static function getMembershipTokenDetails($membershipIDs) {
1318 $memberships = civicrm_api3('membership', 'get', array('options' => array('limit' => 200000), 'membership_id' => array('IN' => (array) $membershipIDs)));
1319 return $memberships['values'];
1320 }
1321 /**
1322 * replace greeting tokens exists in message/subject
1323 *
1324 * @access public
1325 */
1326 static function replaceGreetingTokens(&$tokenString, $contactDetails = NULL, $contactId = NULL, $className = NULL, $escapeSmarty = FALSE) {
1327
1328 if (!$contactDetails && !$contactId) {
1329 return;
1330 }
1331
1332 // check if there are any tokens
1333 $greetingTokens = self::getTokens($tokenString);
1334
1335 if (!empty($greetingTokens)) {
1336 // first use the existing contact object for token replacement
1337 if (!empty($contactDetails)) {
1338 $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString, $contactDetails, TRUE, $greetingTokens, TRUE, $escapeSmarty);
1339 }
1340
1341 // check if there are any unevaluated tokens
1342 $greetingTokens = self::getTokens($tokenString);
1343
1344 // $greetingTokens not empty, means there are few tokens which are not evaluated, like custom data etc
1345 // so retrieve it from database
1346 if (!empty($greetingTokens) && array_key_exists('contact', $greetingTokens)) {
1347 $greetingsReturnProperties = array_flip(CRM_Utils_Array::value('contact', $greetingTokens));
1348 $greetingsReturnProperties = array_fill_keys(array_keys($greetingsReturnProperties), 1);
1349 $contactParams = array('contact_id' => $contactId);
1350
1351 $greetingDetails = self::getTokenDetails($contactParams,
1352 $greetingsReturnProperties,
1353 FALSE, FALSE, NULL,
1354 $greetingTokens,
1355 $className
1356 );
1357
1358 // again replace tokens
1359 $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString,
1360 $greetingDetails,
1361 TRUE,
1362 $greetingTokens,
1363 FALSE,
1364 $escapeSmarty
1365 );
1366 }
1367 // check if there are still any unevaluated tokens
1368 $greetingTokens = self::getTokens($tokenString);
1369
1370 // $greetingTokens not empty, there are hook tokens to replace
1371 if (!empty($greetingTokens) ) {
1372 // Fill the return properties array
1373 reset($greetingTokens);
1374 $greetingsReturnProperties = array();
1375 while(list($key) = each($greetingTokens)) {
1376 $props = array_flip(CRM_Utils_Array::value($key, $greetingTokens));
1377 $props = array_fill_keys(array_keys($props), 1);
1378 $greetingsReturnProperties = $greetingsReturnProperties + $props;
1379 }
1380 $contactParams = array('contact_id' => $contactId);
1381 $greetingDetails = self::getTokenDetails($contactParams,
1382 $greetingsReturnProperties,
1383 FALSE, FALSE, NULL,
1384 $greetingTokens,
1385 $className
1386 );
1387 // Prepare variables for calling replaceHookTokens
1388 $categories = array_keys($greetingTokens);
1389 list($contact) = $greetingDetails;
1390 // Replace tokens defined in Hooks.
1391 $tokenString = CRM_Utils_Token::replaceHookTokens($tokenString, $contact[$contactId], $categories);
1392 }
1393 }
1394 }
1395
1396 /**
1397 * @param $tokens
1398 *
1399 * @return array
1400 */
1401 static function flattenTokens(&$tokens) {
1402 $flattenTokens = array();
1403
1404 foreach (array(
1405 'html', 'text', 'subject') as $prop) {
1406 if (!isset($tokens[$prop])) {
1407 continue;
1408 }
1409 foreach ($tokens[$prop] as $type => $names) {
1410 if (!isset($flattenTokens[$type])) {
1411 $flattenTokens[$type] = array();
1412 }
1413 foreach ($names as $name) {
1414 $flattenTokens[$type][$name] = 1;
1415 }
1416 }
1417 }
1418
1419 return $flattenTokens;
1420 }
1421
1422 /**
1423 * Replace all user tokens in $str
1424 *
1425 * @param string $str The string with tokens to be replaced
1426 *
1427 * @param null $knownTokens
1428 * @param bool $escapeSmarty
1429 *
1430 * @return string The processed string
1431 * @access public
1432 * @static
1433 */
1434 public static function &replaceUserTokens($str, $knownTokens = NULL, $escapeSmarty = FALSE) {
1435 $key = 'user';
1436 if (!$knownTokens ||
1437 !isset($knownTokens[$key])
1438 ) {
1439 return $str;
1440 }
1441
1442 $str = preg_replace_callback(
1443 self::tokenRegex($key),
1444 function ($matches) use($escapeSmarty) {
1445 return CRM_Utils_Token::getUserTokenReplacement($matches[1], $escapeSmarty);
1446 },
1447 $str
1448 );
1449 return $str;
1450 }
1451
1452 /**
1453 * @param $token
1454 * @param bool $escapeSmarty
1455 *
1456 * @return string
1457 */
1458 public static function getUserTokenReplacement($token, $escapeSmarty = FALSE) {
1459 $value = '';
1460
1461 list($objectName, $objectValue) = explode('-', $token, 2);
1462
1463 switch ($objectName) {
1464 case 'permission':
1465 $value = CRM_Core_Permission::permissionEmails($objectValue);
1466 break;
1467
1468 case 'role':
1469 $value = CRM_Core_Permission::roleEmails($objectValue);
1470 break;
1471 }
1472
1473 if ($escapeSmarty) {
1474 $value = self::tokenEscapeSmarty($value);
1475 }
1476
1477 return $value;
1478 }
1479
1480 /**
1481 *
1482 */
1483 protected static function _buildContributionTokens() {
1484 $key = 'contribution';
1485 if (self::$_tokens[$key] == NULL) {
1486 self::$_tokens[$key] = array_keys(array_merge(CRM_Contribute_BAO_Contribution::exportableFields('All'),
1487 array('campaign', 'financial_type')
1488 ));
1489 }
1490 }
1491
1492 /**
1493 * store membership tokens on the static _tokens array
1494 */
1495 protected static function _buildMembershipTokens() {
1496 $key = 'membership';
1497 if (!isset(self::$_tokens[$key]) || self::$_tokens[$key] == NULL) {
1498 $membershipTokens = array();
1499 $tokens = CRM_Core_SelectValues::membershipTokens();
1500 foreach ($tokens as $token => $dontCare) {
1501 $membershipTokens[] = substr($token, (strpos($token, '.') + 1), -1);
1502 }
1503 self::$_tokens[$key] = $membershipTokens;
1504 }
1505 }
1506
1507 /**
1508 * Replace tokens for an entity
1509 * @param string $entity
1510 * @param array $entityArray (e.g. in format from api)
1511 * @param string $str string to replace in
1512 * @param array $knownTokens array of tokens present
1513 * @param boolean $escapeSmarty
1514 * @return string string with replacements made
1515 */
1516 public static function replaceEntityTokens($entity, $entityArray, $str, $knownTokens = array(), $escapeSmarty = FALSE) {
1517 if (!$knownTokens || empty($knownTokens[$entity])) {
1518 return $str;
1519 }
1520
1521 $fn = 'get' . ucFirst($entity) . 'tokenReplacement';
1522 //since we already know the tokens lets just use them & do str_replace which is faster & simpler than preg_replace
1523 foreach ($knownTokens[$entity] as $token) {
1524 $replaceMent = CRM_Utils_Token::$fn($token, $entityArray, $escapeSmarty);
1525 $str = str_replace('{' . $entity . '.' . $token . '}', $replaceMent, $str);
1526 }
1527 $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
1528 return $str;
1529 }
1530
1531 /**
1532 * Replace Contribution tokens in html
1533 *
1534 * @param string $str
1535 * @param array $contribution
1536 * @param bool|string $html
1537 * @param string $knownTokens
1538 * @param bool|string $escapeSmarty
1539 *
1540 * @return unknown|Ambigous <string, mixed>|mixed
1541 */
1542 public static function replaceContributionTokens($str, &$contribution, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE) {
1543 $key = 'contribution';
1544 if (!$knownTokens || !CRM_Utils_Array::value($key, $knownTokens)) {
1545 return $str; //early return
1546 }
1547 self::_buildContributionTokens();
1548
1549 // here we intersect with the list of pre-configured valid tokens
1550 // so that we remove anything we do not recognize
1551 // I hope to move this step out of here soon and
1552 // then we will just iterate on a list of tokens that are passed to us
1553
1554 $str = preg_replace_callback(
1555 self::tokenRegex($key),
1556 function ($matches) use(&$contribution, $html, $escapeSmarty) {
1557 return CRM_Utils_Token::getContributionTokenReplacement($matches[1], $contribution, $html, $escapeSmarty);
1558 },
1559 $str
1560 );
1561
1562 $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
1563 return $str;
1564 }
1565
1566 /**
1567 * We have a situation where we are rendering more than one token in each field because we are combining
1568 * tokens from more than one contribution when pdf thank you letters are grouped (CRM-14367)
1569 *
1570 * The replaceContributionToken doesn't handle receive_date correctly in this scenario because of the formatting
1571 * it applies (other tokens are OK including date fields)
1572 *
1573 * So we sort this out & then call the main function. Note that we are not escaping smarty on this fields like the main function
1574 * does - but the fields is already being formatted through a date function
1575 *
1576 * @param string $separator
1577 * @param string $str
1578 * @param array $contribution
1579 * @param bool|string $html
1580 * @param string $knownTokens
1581 * @param bool|string $escapeSmarty
1582 *
1583 * @return \Ambigous|mixed|string|\unknown
1584 */
1585 public static function replaceMultipleContributionTokens($separator, $str, &$contribution, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE) {
1586 if(empty($knownTokens['contribution'])) {
1587 return $str;
1588 }
1589
1590 if(in_array('receive_date', $knownTokens['contribution'])) {
1591 $formattedDates = array();
1592 $dates = explode($separator, $contribution['receive_date']);
1593 foreach ($dates as $date) {
1594 $formattedDates[] = CRM_Utils_Date::customFormat($date, NULL, array('j', 'm', 'Y'));
1595 }
1596 $str = str_replace("{contribution.receive_date}", implode($separator, $formattedDates), $str);
1597 unset($knownTokens['contribution']['receive_date']);
1598 }
1599 return self::replaceContributionTokens($str, $contribution, $html, $knownTokens, $escapeSmarty);
1600 }
1601
1602 /**
1603 * Get replacement strings for any membership tokens (only a small number of tokens are implemnted in the first instance
1604 * - this is used by the pdfLetter task from membership search
1605 * @param string $token
1606 * @param array $membership an api result array for a single membership
1607 * @param boolean $escapeSmarty
1608 * @return string token replacement
1609 */
1610 public static function getMembershipTokenReplacement($token, $membership, $escapeSmarty = FALSE) {
1611 $entity = 'membership';
1612 self::_buildMembershipTokens();
1613 switch ($token) {
1614 case 'type':
1615 $value = $membership['membership_name'];
1616 break;
1617 case 'status':
1618 $statuses = CRM_Member_BAO_Membership::buildOptions('status_id');
1619 $value = $statuses[$membership['status_id']];
1620 break;
1621 case 'fee':
1622 try{
1623 $value = civicrm_api3('membership_type', 'getvalue', array('id' => $membership['membership_type_id'], 'return' => 'minimum_fee'));
1624 }
1625 catch (CiviCRM_API3_Exception $e) {
1626 // we can anticipate we will get an error if the minimum fee is set to 'NULL' because of the way the
1627 // api handles NULL (4.4)
1628 $value = 0;
1629 }
1630 break;
1631 default:
1632 if (in_array($token, self::$_tokens[$entity])) {
1633 $value = $membership[$token];
1634 }
1635 else {
1636 //ie unchanged
1637 $value = "{$entity}.{$token}";
1638 }
1639 break;
1640 }
1641
1642 if ($escapeSmarty) {
1643 $value = self::tokenEscapeSmarty($value);
1644 }
1645 return $value;
1646 }
1647
1648 /**
1649 * @param $token
1650 * @param $contribution
1651 * @param bool $html
1652 * @param bool $escapeSmarty
1653 *
1654 * @return mixed|string
1655 */
1656 public static function getContributionTokenReplacement($token, &$contribution, $html = FALSE, $escapeSmarty = FALSE) {
1657 self::_buildContributionTokens();
1658
1659 switch ($token) {
1660 case 'total_amount':
1661 case 'net_amount':
1662 case 'fee_amount':
1663 case 'non_deductible_amount':
1664 $value = CRM_Utils_Money::format(CRM_Utils_Array::retrieveValueRecursive($contribution, $token));
1665 break;
1666
1667 case 'receive_date':
1668 $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
1669 $value = CRM_Utils_Date::customFormat($value, NULL, array('j', 'm', 'Y'));
1670 break;
1671
1672 default:
1673 if (!in_array($token, self::$_tokens['contribution'])) {
1674 $value = "{contribution.$token}";
1675 }
1676 else {
1677 $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
1678 }
1679 break;
1680 }
1681
1682
1683 if ($escapeSmarty) {
1684 $value = self::tokenEscapeSmarty($value);
1685 }
1686 return $value;
1687 }
1688
1689 /**
1690 * @return array: legacy_token => new_token
1691 */
1692 static function legacyContactTokens() {
1693 return array(
1694 'individual_prefix' => 'prefix_id',
1695 'individual_suffix' => 'suffix_id',
1696 'gender' => 'gender_id',
1697 'communication_style' => 'communication_style_id',
1698 );
1699 }
1700
1701 /**
1702 * Formats a token list for the select2 widget
1703 * @param $tokens
1704 * @return array
1705 */
1706 static function formatTokensForDisplay($tokens) {
1707 $sorted = $output = array();
1708
1709 // Sort in ascending order by ignoring word case
1710 natcasesort($tokens);
1711
1712 // Attempt to place tokens into optgroups
1713 // TODO: These groupings could be better and less hackish. Getting them pre-grouped from upstream would be nice.
1714 foreach ($tokens as $k => $v) {
1715 // Check to see if this token is already in a group e.g. for custom fields
1716 $split = explode(' :: ', $v);
1717 if (!empty($split[1])) {
1718 $sorted[$split[1]][] = array('id' => $k, 'text' => $split[0]);
1719 }
1720 // Group by entity
1721 else {
1722 $split = explode('.', trim($k, '{}'));
1723 if (isset($split[1])) {
1724 $entity = array_key_exists($split[1], CRM_Core_DAO_Address::export()) ? 'Address' : ucfirst($split[0]);
1725 }
1726 else {
1727 $entity = 'Contact';
1728 }
1729 $sorted[ts($entity)][] = array('id' => $k, 'text' => $v);
1730 }
1731 }
1732
1733 ksort($sorted);
1734 foreach ($sorted as $k => $v) {
1735 $output[] = array('text' => $k, 'children' => $v);
1736 }
1737
1738 return $output;
1739 }
1740 }