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