Merge pull request #679 from davecivicrm/CRM-12397
[civicrm-core.git] / CRM / Utils / Token.php
1 <?php
2 /*
3 +--------------------------------------------------------------------+
4 | CiviCRM version 4.3 |
5 +--------------------------------------------------------------------+
6 | Copyright CiviCRM LLC (c) 2004-2013 |
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-2013
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.subscribeUrl' =>
108 array(
109 'action.optOut' => ts("'Opt out via email' - displays an email address for recipients to opt out of receiving emails from your organization."),
110 'action.optOutUrl' => ts("'Opt out via web page' - creates a link for recipients to click if they want to opt out of receiving emails from your organization. Alternatively, you can include the 'Opt out via email' token."),
111 'action.unsubscribe' => ts("'Unsubscribe via email' - displays an email address for recipients to unsubscribe from the specific mailing list used to send this message."),
112 'action.unsubscribe' => ts("'Unsubscribe via web page' - creates a link for recipients to unsubscribe from the specific mailing list used to send this message. Alternatively, you can include the 'Unsubscribe via email' token or one of the Opt-out tokens."),
113 ),
114 );
115 }
116
117 $missing = array();
118 foreach (self::$_requiredTokens as $token => $value) {
119 if (!is_array($value)) {
120 if (!preg_match('/(^|[^\{])' . preg_quote('{' . $token . '}') . '/', $str)) {
121 $missing[$token] = $value;
122 }
123 }
124 else {
125 $present = FALSE;
126 $desc = NULL;
127 foreach ($value as $t => $d) {
128 $desc = $d;
129 if (preg_match('/(^|[^\{])' . preg_quote('{' . $t . '}') . '/', $str)) {
130 $present = TRUE;
131 }
132 }
133 if (!$present) {
134 $missing[$token] = $desc;
135 }
136 }
137 }
138
139 if (empty($missing)) {
140 return TRUE;
141 }
142 return $missing;
143 }
144
145 /**
146 * Wrapper for token matching
147 *
148 * @param string $type The token type (domain,mailing,contact,action)
149 * @param string $var The token variable
150 * @param string $str The string to search
151 *
152 * @return boolean Was there a match
153 * @access public
154 * @static
155 */
156 public static function token_match($type, $var, &$str) {
157 $token = preg_quote('{' . "$type.$var") . '(\|.+?)?' . preg_quote('}');
158 return preg_match("/(^|[^\{])$token/", $str);
159 }
160
161 /**
162 * Wrapper for token replacing
163 *
164 * @param string $type The token type
165 * @param string $var The token variable
166 * @param string $value The value to substitute for the token
167 * @param string (reference) $str The string to replace in
168 *
169 * @return string The processed string
170 * @access public
171 * @static
172 */
173 public static function &token_replace($type, $var, $value, &$str, $escapeSmarty = FALSE) {
174 $token = preg_quote('{' . "$type.$var") . '(\|([^\}]+?))?' . preg_quote('}');
175 if (!$value) {
176 $value = '$3';
177 }
178 if ($escapeSmarty) {
179 $value = self::tokenEscapeSmarty($value);
180 }
181 $str = preg_replace("/([^\{])?$token/", "\${1}$value", $str);
182 return $str;
183 }
184
185 /**
186 * get< the regex for token replacement
187 *
188 * @param string $key a string indicating the the type of token to be used in the expression
189 *
190 * @return string regular expression sutiable for using in preg_replace
191 * @access private
192 * @static
193 */
194 private static function tokenRegex($token_type) {
195 return '/(?<!\{|\\\\)\{' . $token_type . '\.([\w]+(\-[\w\s]+)?)\}(?!\})/e';
196 }
197
198 /**
199 * escape the string so a malicious user cannot inject smarty code into the template
200 *
201 * @param string $string a string that needs to be escaped from smarty parsing
202 *
203 * @return string the escaped string
204 * @access private
205 * @static
206 */
207 private static function tokenEscapeSmarty($string) {
208 // need to use negative look-behind, as both str_replace() and preg_replace() are sequential
209 return preg_replace(array('/{/', '/(?<!{ldelim)}/'), array('{ldelim}', '{rdelim}'), $string);
210 }
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 * @return string The processed string
221 * @access public
222 * @static
223 */
224 public static function &replaceDomainTokens(
225 $str,
226 &$domain,
227 $html = FALSE,
228 $knownTokens = NULL,
229 $escapeSmarty = FALSE
230 ) {
231 $key = 'domain';
232 if (
233 !$knownTokens ||
234 !CRM_Utils_Array::value($key, $knownTokens)
235 ) {
236 return $str;
237 }
238
239 $str = preg_replace(
240 self::tokenRegex($key),
241 'self::getDomainTokenReplacement(\'\\1\',$domain,$html)',
242 $str
243 );
244 return $str;
245 }
246
247 public static function getDomainTokenReplacement($token, &$domain, $html = FALSE, $escapeSmarty = FALSE) {
248 // check if the token we were passed is valid
249 // we have to do this because this function is
250 // called only when we find a token in the string
251
252 $loc = &$domain->getLocationValues();
253
254 if (!in_array($token, self::$_tokens['domain'])) {
255 $value = "{domain.$token}";
256 }
257 elseif ($token == 'address') {
258 static $addressCache = array();
259
260 $cache_key = $html ? 'address-html' : 'address-text';
261 if (array_key_exists($cache_key, $addressCache)) {
262 return $addressCache[$cache_key];
263 }
264
265 $value = NULL;
266 /* Construct the address token */
267
268 if (CRM_Utils_Array::value($token, $loc)) {
269 if ($html) {
270 $value = $loc[$token][1]['display'];
271 $value = str_replace("\n", '<br />', $value);
272 }
273 else {
274 $value = $loc[$token][1]['display_text'];
275 }
276 $addressCache[$cache_key] = $value;
277 }
278 }
279 elseif ($token == 'name' || $token == 'id' || $token == 'description') {
280 $value = $domain->$token;
281 }
282 elseif ($token == 'phone' || $token == 'email') {
283 /* Construct the phone and email tokens */
284
285 $value = NULL;
286 if (CRM_Utils_Array::value($token, $loc)) {
287 foreach ($loc[$token] as $index => $entity) {
288 $value = $entity[$token];
289 break;
290 }
291 }
292 }
293
294 if ($escapeSmarty) {
295 $value = self::tokenEscapeSmarty($value);
296 }
297
298 return $value;
299 }
300
301 /**
302 * Replace all the org-level tokens in $str
303 *
304 * @param string $str The string with tokens to be replaced
305 * @param object $org Associative array of org properties
306 * @param boolean $html Replace tokens with HTML or plain text
307 *
308 * @return string The processed string
309 * @access public
310 * @static
311 */
312 public static function &replaceOrgTokens($str, &$org, $html = FALSE, $escapeSmarty = FALSE) {
313 self::$_tokens['org'] =
314 array_merge(
315 array_keys(CRM_Contact_BAO_Contact::importableFields('Organization')),
316 array('address', 'display_name', 'checksum', 'contact_id')
317 );
318
319 $cv = NULL;
320 foreach (self::$_tokens['org'] as $token) {
321 // print "Getting token value for $token<br/><br/>";
322 if ($token == '') {
323 continue;
324 }
325
326 /* If the string doesn't contain this token, skip it. */
327
328 if (!self::token_match('org', $token, $str)) {
329 continue;
330 }
331
332 /* Construct value from $token and $contact */
333
334 $value = NULL;
335
336 if ($cfID = CRM_Core_BAO_CustomField::getKeyID($token)) {
337 // only generate cv if we need it
338 if ($cv === NULL) {
339 $cv = CRM_Core_BAO_CustomValue::getContactValues($org['contact_id']);
340 }
341 foreach ($cv as $cvFieldID => $value) {
342 if ($cvFieldID == $cfID) {
343 $value = CRM_Core_BAO_CustomOption::getOptionLabel($cfID, $value);
344 break;
345 }
346 }
347 }
348 elseif ($token == 'checksum') {
349 $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($org['contact_id']);
350 $value = "cs={$cs}";
351 }
352 elseif ($token == 'address') {
353 /* Build the location values array */
354
355 $loc = array();
356 $loc['display_name'] = CRM_Utils_Array::retrieveValueRecursive($org, 'display_name');
357 $loc['street_address'] = CRM_Utils_Array::retrieveValueRecursive($org, 'street_address');
358 $loc['city'] = CRM_Utils_Array::retrieveValueRecursive($org, 'city');
359 $loc['state_province'] = CRM_Utils_Array::retrieveValueRecursive($org, 'state_province');
360 $loc['postal_code'] = CRM_Utils_Array::retrieveValueRecursive($org, 'postal_code');
361
362 /* Construct the address token */
363
364 $value = CRM_Utils_Address::format($loc);
365 if ($html) {
366 $value = str_replace("\n", '<br />', $value);
367 }
368 }
369 else {
370 $value = CRM_Utils_Array::retrieveValueRecursive($org, $token);
371 }
372
373 self::token_replace('org', $token, $value, $str, $escapeSmarty);
374 }
375
376 return $str;
377 }
378
379 /**
380 * Replace all mailing tokens in $str
381 *
382 * @param string $str The string with tokens to be replaced
383 * @param object $mailing The mailing BAO, or null for validation
384 * @param boolean $html Replace tokens with HTML or plain text
385 *
386 * @return string The processed sstring
387 * @access public
388 * @static
389 */
390 public static function &replaceMailingTokens(
391 $str,
392 &$mailing,
393 $html = FALSE,
394 $knownTokens = NULL,
395 $escapeSmarty = FALSE
396 ) {
397 $key = 'mailing';
398 if (!$knownTokens || !isset($knownTokens[$key])) {
399 return $str;
400 }
401
402 $str = preg_replace(
403 self::tokenRegex($key),
404 'self::getMailingTokenReplacement(\'\\1\',$mailing,$escapeSmarty)', $str
405 );
406 return $str;
407 }
408
409 public static function getMailingTokenReplacement($token, &$mailing, $escapeSmarty = FALSE) {
410 $value = '';
411 switch ($token) {
412 // CRM-7663
413
414 case 'id':
415 $value = $mailing ? $mailing->id : 'undefined';
416 break;
417
418 case 'name':
419 $value = $mailing ? $mailing->name : 'Mailing Name';
420 break;
421
422 case 'group':
423 $groups = $mailing ? $mailing->getGroupNames() : array('Mailing Groups');
424 $value = implode(', ', $groups);
425 break;
426
427 case 'subject':
428 $value = $mailing->subject;
429 break;
430
431 case 'viewUrl':
432 $value = CRM_Utils_System::url('civicrm/mailing/view',
433 "reset=1&id={$mailing->id}",
434 TRUE, NULL, FALSE, TRUE
435 );
436 break;
437
438 case 'editUrl':
439 $value = CRM_Utils_System::url('civicrm/mailing/send',
440 "reset=1&mid={$mailing->id}&continue=true",
441 TRUE, NULL, FALSE, TRUE
442 );
443 break;
444
445 case 'scheduleUrl':
446 $value = CRM_Utils_System::url('civicrm/mailing/schedule',
447 "reset=1&mid={$mailing->id}",
448 TRUE, NULL, FALSE, TRUE
449 );
450 break;
451
452 case 'html':
453 $page = new CRM_Mailing_Page_View();
454 $value = $page->run($mailing->id, NULL, FALSE);
455 break;
456
457 case 'approvalStatus':
458 $mailApprovalStatus = CRM_Mailing_PseudoConstant::approvalStatus();
459 $value = $mailApprovalStatus[$mailing->approval_status_id];
460 break;
461
462 case 'approvalNote':
463 $value = $mailing->approval_note;
464 break;
465
466 case 'approveUrl':
467 $value = CRM_Utils_System::url('civicrm/mailing/approve',
468 "reset=1&mid={$mailing->id}",
469 TRUE, NULL, FALSE, TRUE
470 );
471 break;
472
473 case 'creator':
474 $value = CRM_Contact_BAO_Contact::displayName($mailing->created_id);
475 break;
476
477 case 'creatorEmail':
478 $value = CRM_Contact_BAO_Contact::getPrimaryEmail($mailing->created_id);
479 break;
480
481 default:
482 $value = "{mailing.$token}";
483 break;
484 }
485
486 if ($escapeSmarty) {
487 $value = self::tokenEscapeSmarty($value);
488 }
489 return $value;
490 }
491
492 /**
493 * Replace all action tokens in $str
494 *
495 * @param string $str The string with tokens to be replaced
496 * @param array $addresses Assoc. array of VERP event addresses
497 * @param array $urls Assoc. array of action URLs
498 * @param boolean $html Replace tokens with HTML or plain text
499 * @param array $knownTokens A list of tokens that are known to exist in the email body
500 *
501 * @return string The processed string
502 * @access public
503 * @static
504 */
505 public static function &replaceActionTokens(
506 $str,
507 &$addresses,
508 &$urls,
509 $html = FALSE,
510 $knownTokens = NULL,
511 $escapeSmarty = FALSE
512 ) {
513 $key = 'action';
514 // here we intersect with the list of pre-configured valid tokens
515 // so that we remove anything we do not recognize
516 // I hope to move this step out of here soon and
517 // then we will just iterate on a list of tokens that are passed to us
518 if (!$knownTokens || !CRM_Utils_Array::value($key, $knownTokens)) {
519 return $str;
520 }
521
522 $str = preg_replace(self::tokenRegex($key),
523 'self::getActionTokenReplacement(\'\\1\',$addresses,$urls,$escapeSmarty)',
524 $str
525 );
526 return $str;
527 }
528
529 public static function getActionTokenReplacement(
530 $token,
531 &$addresses,
532 &$urls,
533 $html = FALSE,
534 $escapeSmarty = FALSE
535 ) {
536 /* If the token is an email action, use it. Otherwise, find the
537 * appropriate URL */
538
539 if (!in_array($token, self::$_tokens['action'])) {
540 $value = "{action.$token}";
541 }
542 else {
543 $value = CRM_Utils_Array::value($token, $addresses);
544
545 if ($value == NULL) {
546 $value = CRM_Utils_Array::value($token, $urls);
547 }
548
549 if ($value && $html) {
550 //fix for CRM-2318
551 if ((substr($token, -3) != 'Url') && ($token != 'forward')) {
552 $value = "mailto:$value";
553 }
554 }
555 elseif ($value && !$html) {
556 $value = str_replace('&amp;', '&', $value);
557 }
558 }
559
560 if ($escapeSmarty) {
561 $value = self::tokenEscapeSmarty($value);
562 }
563 return $value;
564 }
565
566 /**
567 * Replace all the contact-level tokens in $str with information from
568 * $contact.
569 *
570 * @param string $str The string with tokens to be replaced
571 * @param array $contact Associative array of contact properties
572 * @param boolean $html Replace tokens with HTML or plain text
573 * @param array $knownTokens A list of tokens that are known to exist in the email body
574 * @param boolean $returnBlankToken return unevaluated token if value is null
575 *
576 * @return string The processed string
577 * @access public
578 * @static
579 */
580 public static function &replaceContactTokens(
581 $str,
582 &$contact,
583 $html = FALSE,
584 $knownTokens = NULL,
585 $returnBlankToken = FALSE,
586 $escapeSmarty = FALSE
587 ) {
588 $key = 'contact';
589 if (self::$_tokens[$key] == NULL) {
590 /* This should come from UF */
591
592 self::$_tokens[$key] =
593 array_merge(
594 array_keys(CRM_Contact_BAO_Contact::exportableFields('All')),
595 array('checksum', 'contact_id')
596 );
597 }
598
599 // here we intersect with the list of pre-configured valid tokens
600 // so that we remove anything we do not recognize
601 // I hope to move this step out of here soon and
602 // then we will just iterate on a list of tokens that are passed to us
603 if (!$knownTokens || !CRM_Utils_Array::value($key, $knownTokens)) {
604 return $str;
605 }
606
607 $str = preg_replace(
608 self::tokenRegex($key),
609 'self::getContactTokenReplacement(\'\\1\', $contact, $html, $returnBlankToken, $escapeSmarty)',
610 $str
611 );
612
613 $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
614 return $str;
615 }
616
617 public static function getContactTokenReplacement(
618 $token,
619 &$contact,
620 $html = FALSE,
621 $returnBlankToken = FALSE,
622 $escapeSmarty = FALSE
623 ) {
624 if (self::$_tokens['contact'] == NULL) {
625 /* This should come from UF */
626
627 self::$_tokens['contact'] =
628 array_merge(
629 array_keys(CRM_Contact_BAO_Contact::exportableFields('All')),
630 array('checksum', 'contact_id')
631 );
632 }
633
634 /* Construct value from $token and $contact */
635
636 $value = NULL;
637
638 // check if the token we were passed is valid
639 // we have to do this because this function is
640 // called only when we find a token in the string
641
642 if (!in_array($token, self::$_tokens['contact'])) {
643 $value = "{contact.$token}";
644 }
645 elseif ($token == 'checksum') {
646 $hash = CRM_Utils_Array::value('hash', $contact);
647 $contactID = CRM_Utils_Array::retrieveValueRecursive($contact, 'contact_id');
648 $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID,
649 NULL,
650 NULL,
651 $hash
652 );
653 $value = "cs={$cs}";
654 }
655 else {
656 $value = CRM_Utils_Array::retrieveValueRecursive($contact, $token);
657 }
658
659 if (!$html) {
660 $value = str_replace('&amp;', '&', $value);
661 }
662
663 // if null then return actual token
664 if ($returnBlankToken && !$value) {
665 $value = "{contact.$token}";
666 }
667
668 if ($escapeSmarty) {
669 $value = self::tokenEscapeSmarty($value);
670 }
671
672 return $value;
673 }
674
675 /**
676 * Replace all the hook tokens in $str with information from
677 * $contact.
678 *
679 * @param string $str The string with tokens to be replaced
680 * @param array $contact Associative array of contact properties (including hook token values)
681 * @param boolean $html Replace tokens with HTML or plain text
682 *
683 * @return string The processed string
684 * @access public
685 * @static
686 */
687 public static function &replaceHookTokens(
688 $str,
689 &$contact,
690 &$categories,
691 $html = FALSE,
692 $escapeSmarty = FALSE
693 ) {
694 foreach ($categories as $key) {
695 $str = preg_replace(
696 self::tokenRegex($key),
697 'self::getHookTokenReplacement(\'\\1\', $contact, $key, $html, $escapeSmarty)',
698 $str
699 );
700 }
701 return $str;
702 }
703
704 public static function getHookTokenReplacement(
705 $token,
706 &$contact,
707 $category,
708 $html = FALSE,
709 $escapeSmarty = FALSE
710 ) {
711 $value = CRM_Utils_Array::value("{$category}.{$token}", $contact);
712
713 if ($value && !$html) {
714 $value = str_replace('&amp;', '&', $value);
715 }
716
717 if ($escapeSmarty) {
718 $value = self::tokenEscapeSmarty($value);
719 }
720
721 return $value;
722 }
723
724 /**
725 * unescapeTokens removes any characters that caused the replacement routines to skip token replacement
726 * for example {{token}} or \{token} will result in {token} in the final email
727 *
728 * this routine will remove the extra backslashes and braces
729 *
730 * @param $str ref to the string that will be scanned and modified
731 * @return void this function works directly on the string that is passed
732 * @access public
733 * @static
734 */
735 public static function unescapeTokens(&$str) {
736 $str = preg_replace('/\\\\|\{(\{\w+\.\w+\})\}/', '\\1', $str);
737 }
738
739 /**
740 * Replace unsubscribe tokens
741 *
742 * @param string $str the string with tokens to be replaced
743 * @param object $domain The domain BAO
744 * @param array $groups The groups (if any) being unsubscribed
745 * @param boolean $html Replace tokens with html or plain text
746 * @param int $contact_id The contact ID
747 * @param string hash The security hash of the unsub event
748 *
749 * @return string The processed string
750 * @access public
751 * @static
752 */
753 public static function &replaceUnsubscribeTokens(
754 $str,
755 &$domain,
756 &$groups,
757 $html,
758 $contact_id,
759 $hash
760 ) {
761 if (self::token_match('unsubscribe', 'group', $str)) {
762 if (!empty($groups)) {
763 $config = CRM_Core_Config::singleton();
764 $base = CRM_Utils_System::baseURL();
765
766 // FIXME: an ugly hack for CRM-2035, to be dropped once CRM-1799 is implemented
767 $dao = new CRM_Contact_DAO_Group();
768 $dao->find();
769 while ($dao->fetch()) {
770 if (substr($dao->visibility, 0, 6) == 'Public') {
771 $visibleGroups[] = $dao->id;
772 }
773 }
774 $value = implode(', ', $groups);
775 self::token_replace('unsubscribe', 'group', $value, $str);
776 }
777 }
778 return $str;
779 }
780
781 /**
782 * Replace resubscribe tokens
783 *
784 * @param string $str the string with tokens to be replaced
785 * @param object $domain The domain BAO
786 * @param array $groups The groups (if any) being resubscribed
787 * @param boolean $html Replace tokens with html or plain text
788 * @param int $contact_id The contact ID
789 * @param string hash The security hash of the resub event
790 *
791 * @return string The processed string
792 * @access public
793 * @static
794 */
795 public static function &replaceResubscribeTokens($str, &$domain, &$groups, $html,
796 $contact_id, $hash
797 ) {
798 if (self::token_match('resubscribe', 'group', $str)) {
799 if (!empty($groups)) {
800 $value = implode(', ', $groups);
801 self::token_replace('resubscribe', 'group', $value, $str);
802 }
803 }
804 return $str;
805 }
806
807 /**
808 * Replace subscription-confirmation-request tokens
809 *
810 * @param string $str The string with tokens to be replaced
811 * @param string $group The name of the group being subscribed
812 * @param boolean $html Replace tokens with html or plain text
813 *
814 * @return string The processed string
815 * @access public
816 * @static
817 */
818 public static function &replaceSubscribeTokens($str, $group, $url, $html) {
819 if (self::token_match('subscribe', 'group', $str)) {
820 self::token_replace('subscribe', 'group', $group, $str);
821 }
822 if (self::token_match('subscribe', 'url', $str)) {
823 self::token_replace('subscribe', 'url', $url, $str);
824 }
825 return $str;
826 }
827
828 /**
829 * Replace subscription-invitation tokens
830 *
831 * @param string $str The string with tokens to be replaced
832 *
833 * @return string The processed string
834 * @access public
835 * @static
836 */
837 public static function &replaceSubscribeInviteTokens($str) {
838 if (preg_match('/\{action\.subscribeUrl\}/', $str)) {
839 $url = CRM_Utils_System::url('civicrm/mailing/subscribe',
840 'reset=1',
841 TRUE, NULL, TRUE, TRUE
842 );
843 $str = preg_replace('/\{action\.subscribeUrl\}/', $url, $str);
844 }
845
846 if (preg_match('/\{action\.subscribeUrl.\d+\}/', $str, $matches)) {
847 foreach ($matches as $key => $value) {
848 $gid = substr($value, 21, -1);
849 $url = CRM_Utils_System::url('civicrm/mailing/subscribe',
850 "reset=1&gid={$gid}",
851 TRUE, NULL, TRUE, TRUE
852 );
853 $url = str_replace('&amp;', '&', $url);
854 $str = preg_replace('/' . preg_quote($value) . '/', $url, $str);
855 }
856 }
857
858 if (preg_match('/\{action\.subscribe.\d+\}/', $str, $matches)) {
859 foreach ($matches as $key => $value) {
860 $gid = substr($value, 18, -1);
861 $config = CRM_Core_Config::singleton();
862 $domain = CRM_Core_BAO_MailSettings::defaultDomain();
863 $localpart = CRM_Core_BAO_MailSettings::defaultLocalpart();
864 // we add the 0.0000000000000000 part to make this match the other email patterns (with action, two ids and a hash)
865 $str = preg_replace('/' . preg_quote($value) . '/', "mailto:{$localpart}s.{$gid}.0.0000000000000000@$domain", $str);
866 }
867 }
868 return $str;
869 }
870
871 /**
872 * Replace welcome/confirmation tokens
873 *
874 * @param string $str The string with tokens to be replaced
875 * @param string $group The name of the group being subscribed
876 * @param boolean $html Replace tokens with html or plain text
877 *
878 * @return string The processed string
879 * @access public
880 * @static
881 */
882 public static function &replaceWelcomeTokens($str, $group, $html) {
883 if (self::token_match('welcome', 'group', $str)) {
884 self::token_replace('welcome', 'group', $group, $str);
885 }
886 return $str;
887 }
888
889 /**
890 * Find unprocessed tokens (call this last)
891 *
892 * @param string $str The string to search
893 *
894 * @return array Array of tokens that weren't replaced
895 * @access public
896 * @static
897 */
898 public static function &unmatchedTokens(&$str) {
899 //preg_match_all('/[^\{\\\\]\{(\w+\.\w+)\}[^\}]/', $str, $match);
900 preg_match_all('/\{(\w+\.\w+)\}/', $str, $match);
901 return $match[1];
902 }
903
904 /**
905 * Find and replace tokens for each component
906 *
907 * @param string $str The string to search
908 * @param array $contact Associative array of contact properties
909 * @param array $components A list of tokens that are known to exist in the email body
910 *
911 * @return string The processed string
912 * @access public
913 * @static
914 */
915 public static function &replaceComponentTokens(&$str, $contact, $components, $escapeSmarty = FALSE, $returnEmptyToken = TRUE) {
916 if (!is_array($components) || empty($contact)) {
917 return $str;
918 }
919
920 foreach ($components as $name => $tokens) {
921 if (!is_array($tokens) || empty($tokens)) {
922 continue;
923 }
924
925 foreach ($tokens as $token) {
926 if (self::token_match($name, $token, $str) && isset($contact[$name . '.' . $token])) {
927 self::token_replace($name, $token, $contact[$name . '.' . $token], $str, $escapeSmarty);
928 }
929 elseif (!$returnEmptyToken) {
930 //replacing empty token
931 self::token_replace($name, $token, "", $str, $escapeSmarty);
932 }
933 }
934 }
935 return $str;
936 }
937
938 /**
939 * Get array of string tokens
940 *
941 * @param $string the input string to parse for tokens
942 *
943 * @return $tokens array of tokens mentioned in field
944 * @access public
945 * @static
946 */
947 static function getTokens($string) {
948 $matches = array();
949 $tokens = array();
950 preg_match_all('/(?<!\{|\\\\)\{(\w+\.\w+)\}(?!\})/',
951 $string,
952 $matches,
953 PREG_PATTERN_ORDER
954 );
955
956 if ($matches[1]) {
957 foreach ($matches[1] as $token) {
958 list($type, $name) = preg_split('/\./', $token, 2);
959 if ($name && $type) {
960 if (!isset($tokens[$type])) {
961 $tokens[$type] = array();
962 }
963 $tokens[$type][] = $name;
964 }
965 }
966 }
967 return $tokens;
968 }
969
970 /**
971 * gives required details of contacts in an indexed array format so we
972 * can iterate in a nice loop and do token evaluation
973 *
974 * @param array $contactIds of contacts
975 * @param array $returnProperties of required properties
976 * @param boolean $skipOnHold don't return on_hold contact info also.
977 * @param boolean $skipDeceased don't return deceased contact info.
978 * @param array $extraParams extra params
979 * @param array $tokens the list of tokens we've extracted from the content
980 * @param int $jobID the mailing list jobID - this is a legacy param
981 *
982 * @return array
983 * @access public
984 * @static
985 */
986 static function getTokenDetails($contactIDs,
987 $returnProperties = NULL,
988 $skipOnHold = TRUE,
989 $skipDeceased = TRUE,
990 $extraParams = NULL,
991 $tokens = array(),
992 $className = NULL,
993 $jobID = NULL
994 ) {
995 if (empty($contactIDs)) {
996 // putting a fatal here so we can track if/when this happens
997 CRM_Core_Error::fatal();
998 }
999
1000 $params = array();
1001 foreach ($contactIDs as $key => $contactID) {
1002 $params[] = array(
1003 CRM_Core_Form::CB_PREFIX . $contactID,
1004 '=', 1, 0, 0,
1005 );
1006 }
1007
1008 // fix for CRM-2613
1009 if ($skipDeceased) {
1010 $params[] = array('is_deceased', '=', 0, 0, 0);
1011 }
1012
1013 //fix for CRM-3798
1014 if ($skipOnHold) {
1015 $params[] = array('on_hold', '=', 0, 0, 0);
1016 }
1017
1018 if ($extraParams) {
1019 $params = array_merge($params, $extraParams);
1020 }
1021
1022 // if return properties are not passed then get all return properties
1023 if (empty($returnProperties)) {
1024 $fields = array_merge(array_keys(CRM_Contact_BAO_Contact::exportableFields()),
1025 array('display_name', 'checksum', 'contact_id')
1026 );
1027 foreach ($fields as $key => $val) {
1028 $returnProperties[$val] = 1;
1029 }
1030 }
1031
1032 $custom = array();
1033 foreach ($returnProperties as $name => $dontCare) {
1034 $cfID = CRM_Core_BAO_CustomField::getKeyID($name);
1035 if ($cfID) {
1036 $custom[] = $cfID;
1037 }
1038 }
1039
1040 //get the total number of contacts to fetch from database.
1041 $numberofContacts = count($contactIDs);
1042 $query = new CRM_Contact_BAO_Query($params, $returnProperties);
1043
1044 $details = $query->apiQuery($params, $returnProperties, NULL, NULL, 0, $numberofContacts);
1045
1046 $contactDetails = &$details[0];
1047
1048 foreach ($contactIDs as $key => $contactID) {
1049 if (array_key_exists($contactID, $contactDetails)) {
1050 if (CRM_Utils_Array::value('preferred_communication_method', $returnProperties) == 1
1051 && array_key_exists('preferred_communication_method', $contactDetails[$contactID])
1052 ) {
1053 $pcm = CRM_Core_PseudoConstant::pcm();
1054
1055 // communication Prefferance
1056 $contactPcm = explode(CRM_Core_DAO::VALUE_SEPARATOR,
1057 $contactDetails[$contactID]['preferred_communication_method']
1058 );
1059 $result = array();
1060 foreach ($contactPcm as $key => $val) {
1061 if ($val) {
1062 $result[$val] = $pcm[$val];
1063 }
1064 }
1065 $contactDetails[$contactID]['preferred_communication_method'] = implode(', ', $result);
1066 }
1067
1068 foreach ($custom as $cfID) {
1069 if (isset($contactDetails[$contactID]["custom_{$cfID}"])) {
1070 $contactDetails[$contactID]["custom_{$cfID}"] = CRM_Core_BAO_CustomField::getDisplayValue($contactDetails[$contactID]["custom_{$cfID}"],
1071 $cfID, $details[1]
1072 );
1073 }
1074 }
1075
1076 //special case for greeting replacement
1077 foreach (array(
1078 'email_greeting', 'postal_greeting', 'addressee') as $val) {
1079 if (CRM_Utils_Array::value($val, $contactDetails[$contactID])) {
1080 $contactDetails[$contactID][$val] = $contactDetails[$contactID]["{$val}_display"];
1081 }
1082 }
1083 }
1084 }
1085
1086 // also call a hook and get token details
1087 CRM_Utils_Hook::tokenValues($details[0],
1088 $contactIDs,
1089 $jobID,
1090 $tokens,
1091 $className
1092 );
1093 return $details;
1094 }
1095
1096 /**
1097 * gives required details of contribuion in an indexed array format so we
1098 * can iterate in a nice loop and do token evaluation
1099 *
1100 * @param array $contributionId one contribution id
1101 * @param array $returnProperties of required properties
1102 * @param boolean $skipOnHold don't return on_hold contact info.
1103 * @param boolean $skipDeceased don't return deceased contact info.
1104 * @param array $extraParams extra params
1105 * @param array $tokens the list of tokens we've extracted from the content
1106 *
1107 * @return array
1108 * @access public
1109 * @static
1110 */
1111 static function getContributionTokenDetails($contributionIDs,
1112 $returnProperties = NULL,
1113 $extraParams = NULL,
1114 $tokens = array(),
1115 $className = NULL
1116 ) {
1117 if (empty($contributionIDs)) {
1118 // putting a fatal here so we can track if/when this happens
1119 CRM_Core_Error::fatal();
1120 }
1121
1122 $details = array();
1123
1124 // no apiQuery helper yet, so do a loop and find contribution by id
1125 foreach ($contributionIDs as $contributionID) {
1126
1127 $dao = new CRM_Contribute_DAO_Contribution();
1128 $dao->id = $contributionID;
1129
1130 if ($dao->find(TRUE)) {
1131
1132 $details[$dao->id] = array();
1133 CRM_Core_DAO::storeValues($dao, $details[$dao->id]);
1134
1135 // do the necessary transformation
1136 if (CRM_Utils_Array::value('payment_instrument_id', $details[$dao->id])) {
1137 $piId = $details[$dao->id]['payment_instrument_id'];
1138 $pis = CRM_Contribute_PseudoConstant::paymentInstrument();
1139 $details[$dao->id]['payment_instrument'] = $pis[$piId];
1140 }
1141 if (CRM_Utils_Array::value('campaign_id', $details[$dao->id])) {
1142 $campaignId = $details[$dao->id]['campaign_id'];
1143 $campaigns = CRM_Campaign_BAO_Campaign::getCampaigns($campaignId);
1144 $details[$dao->id]['campaign'] = $campaigns[$campaignId];
1145 }
1146
1147 // TODO: call a hook to get token contribution details
1148 }
1149 }
1150
1151 return $details;
1152 }
1153
1154 /**
1155 * replace greeting tokens exists in message/subject
1156 *
1157 * @access public
1158 */
1159 static function replaceGreetingTokens(&$tokenString, $contactDetails = NULL, $contactId = NULL, $className = NULL) {
1160
1161 if (!$contactDetails && !$contactId) {
1162 return;
1163 }
1164
1165 // check if there are any tokens
1166 $greetingTokens = self::getTokens($tokenString);
1167
1168 if (!empty($greetingTokens)) {
1169 // first use the existing contact object for token replacement
1170 if (!empty($contactDetails)) {
1171 $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString, $contactDetails, TRUE, $greetingTokens, TRUE);
1172 }
1173
1174 // check if there are any unevaluated tokens
1175 $greetingTokens = self::getTokens($tokenString);
1176
1177 // $greetingTokens not empty, means there are few tokens which are not evaluated, like custom data etc
1178 // so retrieve it from database
1179 if (!empty($greetingTokens) && array_key_exists('contact', $greetingTokens)) {
1180 $greetingsReturnProperties = array_flip(CRM_Utils_Array::value('contact', $greetingTokens));
1181 $greetingsReturnProperties = array_fill_keys(array_keys($greetingsReturnProperties), 1);
1182 $contactParams = array('contact_id' => $contactId);
1183
1184 $greetingDetails = self::getTokenDetails($contactParams,
1185 $greetingsReturnProperties,
1186 FALSE, FALSE, NULL,
1187 $greetingTokens,
1188 $className
1189 );
1190
1191 // again replace tokens
1192 $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString,
1193 $greetingDetails,
1194 TRUE,
1195 $greetingTokens
1196 );
1197 }
1198 }
1199 }
1200
1201 static function flattenTokens(&$tokens) {
1202 $flattenTokens = array();
1203
1204 foreach (array(
1205 'html', 'text', 'subject') as $prop) {
1206 if (!isset($tokens[$prop])) {
1207 continue;
1208 }
1209 foreach ($tokens[$prop] as $type => $names) {
1210 if (!isset($flattenTokens[$type])) {
1211 $flattenTokens[$type] = array();
1212 }
1213 foreach ($names as $name) {
1214 $flattenTokens[$type][$name] = 1;
1215 }
1216 }
1217 }
1218
1219 return $flattenTokens;
1220 }
1221
1222 /**
1223 * Replace all user tokens in $str
1224 *
1225 * @param string $str The string with tokens to be replaced
1226 *
1227 * @return string The processed string
1228 * @access public
1229 * @static
1230 */
1231 public static function &replaceUserTokens($str, $knownTokens = NULL, $escapeSmarty = FALSE) {
1232 $key = 'user';
1233 if (!$knownTokens ||
1234 !isset($knownTokens[$key])
1235 ) {
1236 return $str;
1237 }
1238
1239 $str = preg_replace(self::tokenRegex($key),
1240 'self::getUserTokenReplacement(\'\\1\',$escapeSmarty)', $str
1241 );
1242 return $str;
1243 }
1244
1245 public static function getUserTokenReplacement($token, $escapeSmarty = FALSE) {
1246 $value = '';
1247
1248 list($objectName, $objectValue) = explode('-', $token, 2);
1249
1250 switch ($objectName) {
1251 case 'permission':
1252 $value = CRM_Core_Permission::permissionEmails($objectValue);
1253 break;
1254
1255 case 'role':
1256 $value = CRM_Core_Permission::roleEmails($objectValue);
1257 break;
1258 }
1259
1260 if ($escapeSmarty) {
1261 $value = self::tokenEscapeSmarty($value);
1262 }
1263
1264 return $value;
1265 }
1266
1267
1268 protected static function _buildContributionTokens() {
1269 $key = 'contribution';
1270 if (self::$_tokens[$key] == NULL) {
1271 self::$_tokens[$key] = array_keys(array_merge(CRM_Contribute_BAO_Contribution::exportableFields('All'),
1272 array('campaign', 'financial_type')
1273 ));
1274 }
1275 }
1276
1277 public static function &replaceContributionTokens($str, &$contribution, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE) {
1278 self::_buildContributionTokens();
1279
1280 // here we intersect with the list of pre-configured valid tokens
1281 // so that we remove anything we do not recognize
1282 // I hope to move this step out of here soon and
1283 // then we will just iterate on a list of tokens that are passed to us
1284 $key = 'contribution';
1285 if (!$knownTokens || !CRM_Utils_Array::value($key, $knownTokens)) {
1286 return $str;
1287 }
1288
1289 $str = preg_replace(self::tokenRegex($key),
1290 'self::getContributionTokenReplacement(\'\\1\', $contribution, $html, $escapeSmarty)',
1291 $str
1292 );
1293
1294 $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str);
1295 return $str;
1296 }
1297
1298 public static function getContributionTokenReplacement($token, &$contribution, $html = FALSE, $escapeSmarty = FALSE) {
1299 self::_buildContributionTokens();
1300
1301 switch ($token) {
1302 case 'total_amount':
1303 case 'net_amount':
1304 case 'fee_amount':
1305 case 'non_deductible_amount':
1306 $value = CRM_Utils_Money::format(CRM_Utils_Array::retrieveValueRecursive($contribution, $token));
1307 break;
1308
1309 case 'receive_date':
1310 $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
1311 $value = CRM_Utils_Date::customFormat($value, NULL, array('j', 'm', 'Y'));
1312 break;
1313
1314 default:
1315 if (!in_array($token, self::$_tokens['contribution'])) {
1316 $value = "{contribution.$token}";
1317 }
1318 else {
1319 $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token);
1320 }
1321 break;
1322 }
1323
1324
1325 if ($escapeSmarty) {
1326 $value = self::tokenEscapeSmarty($value);
1327 }
1328 return $value;
1329 }
1330
1331 function getPermissionEmails($permissionName) {}
1332
1333 function getRoleEmails($roleName) {}
1334 }
1335