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