array( 'forward', 'optOut', 'optOutUrl', 'reply', 'unsubscribe', 'unsubscribeUrl', 'resubscribe', 'resubscribeUrl', 'subscribeUrl', ), 'mailing' => array( 'id', 'name', 'group', 'subject', 'viewUrl', 'editUrl', 'scheduleUrl', 'approvalStatus', 'approvalNote', 'approveUrl', 'creator', 'creatorEmail', ), 'user' => array( // we extract the stuff after the role / permission and return the // civicrm email addresses of all users with that role / permission // useful with rules integration 'permission:', 'role:', ), // populate this dynamically 'contact' => NULL, // populate this dynamically 'contribution' => NULL, 'domain' => array( 'name', 'phone', 'address', 'email', 'id', 'description', ), 'subscribe' => array('group'), 'unsubscribe' => array('group'), 'resubscribe' => array('group'), 'welcome' => array('group'), ); /** * @deprecated * This is used by CiviMail but will be made redundant by FlexMailer. * @return array */ public static function getRequiredTokens() { if (self::$_requiredTokens == NULL) { self::$_requiredTokens = array( 'domain.address' => ts("Domain address - displays your organization's postal address."), 'action.optOutUrl or action.unsubscribeUrl' => array( 'action.optOut' => ts("'Opt out via email' - displays an email address for recipients to opt out of receiving emails from your organization."), '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."), 'action.unsubscribe' => ts("'Unsubscribe via email' - displays an email address for recipients to unsubscribe from the specific mailing list used to send this message."), '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."), ), ); } return self::$_requiredTokens; } /** * Check a string (mailing body) for required tokens. * * @param string $str * The message. * * @return bool|array * true if all required tokens are found, * else an array of the missing tokens */ public static function requiredTokens(&$str) { // FlexMailer is a refactoring of CiviMail which provides new hooks/APIs/docs. If the sysadmin has opted to enable it, then use that instead of CiviMail. $requiredTokens = defined('CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS') ? Civi\Core\Resolver::singleton()->call(CIVICRM_FLEXMAILER_HACK_REQUIRED_TOKENS, array()) : CRM_Utils_Token::getRequiredTokens(); $missing = array(); foreach ($requiredTokens as $token => $value) { if (!is_array($value)) { if (!preg_match('/(^|[^\{])' . preg_quote('{' . $token . '}') . '/', $str)) { $missing[$token] = $value; } } else { $present = FALSE; $desc = NULL; foreach ($value as $t => $d) { $desc = $d; if (preg_match('/(^|[^\{])' . preg_quote('{' . $t . '}') . '/', $str)) { $present = TRUE; } } if (!$present) { $missing[$token] = $desc; } } } if (empty($missing)) { return TRUE; } return $missing; } /** * Wrapper for token matching. * * @param string $type * The token type (domain,mailing,contact,action). * @param string $var * The token variable. * @param string $str * The string to search. * * @return bool * Was there a match */ public static function token_match($type, $var, &$str) { $token = preg_quote('{' . "$type.$var") . '(\|.+?)?' . preg_quote('}'); return preg_match("/(^|[^\{])$token/", $str); } /** * Wrapper for token replacing. * * @param string $type * The token type. * @param string $var * The token variable. * @param string $value * The value to substitute for the token. * @param string (reference) $str The string to replace in * * @param bool $escapeSmarty * * @return string * The processed string */ public static function &token_replace($type, $var, $value, &$str, $escapeSmarty = FALSE) { $token = preg_quote('{' . "$type.$var") . '(\|([^\}]+?))?' . preg_quote('}'); if (!$value) { $value = '$3'; } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } $str = preg_replace("/([^\{])?$token/", "\${1}$value", $str); return $str; } /** * Get< the regex for token replacement * * @param string $token_type * A string indicating the the type of token to be used in the expression. * * @return string * regular expression sutiable for using in preg_replace */ private static function tokenRegex($token_type) { return '/(?getLocationValues(); if (!in_array($token, self::$_tokens['domain'])) { $value = "{domain.$token}"; } elseif ($token == 'address') { static $addressCache = array(); $cache_key = $html ? 'address-html' : 'address-text'; if (array_key_exists($cache_key, $addressCache)) { return $addressCache[$cache_key]; } $value = NULL; // Construct the address token if (!empty($loc[$token])) { if ($html) { $value = $loc[$token][1]['display']; $value = str_replace("\n", '
', $value); } else { $value = $loc[$token][1]['display_text']; } $addressCache[$cache_key] = $value; } } elseif ($token == 'name' || $token == 'id' || $token == 'description') { $value = $domain->$token; } elseif ($token == 'phone' || $token == 'email') { // Construct the phone and email tokens $value = NULL; if (!empty($loc[$token])) { foreach ($loc[$token] as $index => $entity) { $value = $entity[$token]; break; } } } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } return $value; } /** * Replace all the org-level tokens in $str * * @fixme: This function appears to be broken, as it depends on * nonexistant method: CRM_Core_BAO_CustomValue::getContactValues() * Marking as deprecated until this is fixed * @deprecated * * @param string $str * The string with tokens to be replaced. * @param object $org * Associative array of org properties. * @param bool $html * Replace tokens with HTML or plain text. * * @param bool $escapeSmarty * * @return string * The processed string */ public static function &replaceOrgTokens($str, &$org, $html = FALSE, $escapeSmarty = FALSE) { self::$_tokens['org'] = array_merge( array_keys(CRM_Contact_BAO_Contact::importableFields('Organization')), array('address', 'display_name', 'checksum', 'contact_id') ); $cv = NULL; foreach (self::$_tokens['org'] as $token) { // print "Getting token value for $token

"; if ($token == '') { continue; } // If the string doesn't contain this token, skip it. if (!self::token_match('org', $token, $str)) { continue; } // Construct value from $token and $contact $value = NULL; if ($cfID = CRM_Core_BAO_CustomField::getKeyID($token)) { // only generate cv if we need it if ($cv === NULL) { $cv = CRM_Core_BAO_CustomValue::getContactValues($org['contact_id']); } foreach ($cv as $cvFieldID => $value) { if ($cvFieldID == $cfID) { $value = CRM_Core_BAO_CustomField::displayValue($value, $cfID); break; } } } elseif ($token == 'checksum') { $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($org['contact_id']); $value = "cs={$cs}"; } elseif ($token == 'address') { // Build the location values array $loc = array(); $loc['display_name'] = CRM_Utils_Array::retrieveValueRecursive($org, 'display_name'); $loc['street_address'] = CRM_Utils_Array::retrieveValueRecursive($org, 'street_address'); $loc['city'] = CRM_Utils_Array::retrieveValueRecursive($org, 'city'); $loc['state_province'] = CRM_Utils_Array::retrieveValueRecursive($org, 'state_province'); $loc['postal_code'] = CRM_Utils_Array::retrieveValueRecursive($org, 'postal_code'); // Construct the address token $value = CRM_Utils_Address::format($loc); if ($html) { $value = str_replace("\n", '
', $value); } } else { $value = CRM_Utils_Array::retrieveValueRecursive($org, $token); } self::token_replace('org', $token, $value, $str, $escapeSmarty); } return $str; } /** * Replace all mailing tokens in $str * * @param string $str * The string with tokens to be replaced. * @param object $mailing * The mailing BAO, or null for validation. * @param bool $html * Replace tokens with HTML or plain text. * * @param null $knownTokens * @param bool $escapeSmarty * * @return string * The processed string */ public static function &replaceMailingTokens( $str, &$mailing, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE ) { $key = 'mailing'; if (!$knownTokens || !isset($knownTokens[$key])) { return $str; } $str = preg_replace_callback( self::tokenRegex($key), function ($matches) use (&$mailing, $escapeSmarty) { return CRM_Utils_Token::getMailingTokenReplacement($matches[1], $mailing, $escapeSmarty); }, $str ); return $str; } /** * @param $token * @param $mailing * @param bool $escapeSmarty * * @return string */ public static function getMailingTokenReplacement($token, &$mailing, $escapeSmarty = FALSE) { $value = ''; switch ($token) { // CRM-7663 case 'id': $value = $mailing ? $mailing->id : 'undefined'; break; case 'name': $value = $mailing ? $mailing->name : 'Mailing Name'; break; case 'group': $groups = $mailing ? $mailing->getGroupNames() : array('Mailing Groups'); $value = implode(', ', $groups); break; case 'subject': $value = $mailing->subject; break; case 'viewUrl': $mailingKey = $mailing->id; if ($hash = CRM_Mailing_BAO_Mailing::getMailingHash($mailingKey)) { $mailingKey = $hash; } $value = CRM_Utils_System::url('civicrm/mailing/view', "reset=1&id={$mailingKey}", TRUE, NULL, FALSE, TRUE ); break; case 'editUrl': case 'scheduleUrl': // Note: editUrl and scheduleUrl used to be different, but now there's // one screen which can adapt based on permissions (in workflow mode). $value = CRM_Utils_System::url('civicrm/mailing/send', "reset=1&mid={$mailing->id}&continue=true", TRUE, NULL, FALSE, TRUE ); break; case 'html': $page = new CRM_Mailing_Page_View(); $value = $page->run($mailing->id, NULL, FALSE, TRUE); break; case 'approvalStatus': $value = CRM_Core_PseudoConstant::getLabel('CRM_Mailing_DAO_Mailing', 'approval_status_id', $mailing->approval_status_id); break; case 'approvalNote': $value = $mailing->approval_note; break; case 'approveUrl': $value = CRM_Utils_System::url('civicrm/mailing/approve', "reset=1&mid={$mailing->id}", TRUE, NULL, FALSE, TRUE ); break; case 'creator': $value = CRM_Contact_BAO_Contact::displayName($mailing->created_id); break; case 'creatorEmail': $value = CRM_Contact_BAO_Contact::getPrimaryEmail($mailing->created_id); break; default: $value = "{mailing.$token}"; break; } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } return $value; } /** * Replace all action tokens in $str * * @param string $str * The string with tokens to be replaced. * @param array $addresses * Assoc. array of VERP event addresses. * @param array $urls * Assoc. array of action URLs. * @param bool $html * Replace tokens with HTML or plain text. * @param array $knownTokens * A list of tokens that are known to exist in the email body. * * @param bool $escapeSmarty * * @return string * The processed string */ public static function &replaceActionTokens( $str, &$addresses, &$urls, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE ) { $key = 'action'; // here we intersect with the list of pre-configured valid tokens // so that we remove anything we do not recognize // I hope to move this step out of here soon and // then we will just iterate on a list of tokens that are passed to us if (!$knownTokens || empty($knownTokens[$key])) { return $str; } $str = preg_replace_callback( self::tokenRegex($key), function ($matches) use (&$addresses, &$urls, $html, $escapeSmarty) { return CRM_Utils_Token::getActionTokenReplacement($matches[1], $addresses, $urls, $html, $escapeSmarty); }, $str ); return $str; } /** * @param $token * @param $addresses * @param $urls * @param bool $html * @param bool $escapeSmarty * * @return mixed|string */ public static function getActionTokenReplacement( $token, &$addresses, &$urls, $html = FALSE, $escapeSmarty = FALSE ) { // If the token is an email action, use it. Otherwise, find the // appropriate URL. if (!in_array($token, self::$_tokens['action'])) { $value = "{action.$token}"; } else { $value = CRM_Utils_Array::value($token, $addresses); if ($value == NULL) { $value = CRM_Utils_Array::value($token, $urls); } if ($value && $html) { // fix for CRM-2318 if ((substr($token, -3) != 'Url') && ($token != 'forward')) { $value = "mailto:$value"; } } elseif ($value && !$html) { $value = str_replace('&', '&', $value); } } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } return $value; } /** * Replace all the contact-level tokens in $str with information from * $contact. * * @param string $str * The string with tokens to be replaced. * @param array $contact * Associative array of contact properties. * @param bool $html * Replace tokens with HTML or plain text. * @param array $knownTokens * A list of tokens that are known to exist in the email body. * @param bool $returnBlankToken * Return unevaluated token if value is null. * * @param bool $escapeSmarty * * @return string * The processed string */ public static function &replaceContactTokens( $str, &$contact, $html = FALSE, $knownTokens = NULL, $returnBlankToken = FALSE, $escapeSmarty = FALSE ) { // Refresh contact tokens in case they have changed. There is heavy caching // in exportable fields so there is no benefit in doing this conditionally. self::$_tokens['contact'] = array_merge( array_keys(CRM_Contact_BAO_Contact::exportableFields('All')), array('checksum', 'contact_id') ); $key = 'contact'; // here we intersect with the list of pre-configured valid tokens // so that we remove anything we do not recognize // I hope to move this step out of here soon and // then we will just iterate on a list of tokens that are passed to us if (!$knownTokens || empty($knownTokens[$key])) { return $str; } $str = preg_replace_callback( self::tokenRegex($key), function ($matches) use (&$contact, $html, $returnBlankToken, $escapeSmarty) { return CRM_Utils_Token::getContactTokenReplacement($matches[1], $contact, $html, $returnBlankToken, $escapeSmarty); }, $str ); $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str); return $str; } /** * @param $token * @param $contact * @param bool $html * @param bool $returnBlankToken * @param bool $escapeSmarty * * @return bool|mixed|null|string */ public static function getContactTokenReplacement( $token, &$contact, $html = FALSE, $returnBlankToken = FALSE, $escapeSmarty = FALSE ) { if (self::$_tokens['contact'] == NULL) { /* This should come from UF */ self::$_tokens['contact'] = array_merge( array_keys(CRM_Contact_BAO_Contact::exportableFields('All')), array('checksum', 'contact_id') ); } // Construct value from $token and $contact $value = NULL; $noReplace = FALSE; // Support legacy tokens $token = CRM_Utils_Array::value($token, self::legacyContactTokens(), $token); // check if the token we were passed is valid // we have to do this because this function is // called only when we find a token in the string if (!in_array($token, self::$_tokens['contact'])) { $noReplace = TRUE; } elseif ($token == 'checksum') { $hash = CRM_Utils_Array::value('hash', $contact); $contactID = CRM_Utils_Array::retrieveValueRecursive($contact, 'contact_id'); $cs = CRM_Contact_BAO_Contact_Utils::generateChecksum($contactID, NULL, NULL, $hash ); $value = "cs={$cs}"; } else { $value = CRM_Utils_Array::retrieveValueRecursive($contact, $token); // FIXME: for some pseudoconstants we get array ( 0 => id, 1 => label ) if (is_array($value)) { $value = $value[1]; } // Convert pseudoconstants using metadata elseif ($value && is_numeric($value)) { $allFields = CRM_Contact_BAO_Contact::exportableFields('All'); if (!empty($allFields[$token]['pseudoconstant'])) { $value = CRM_Core_PseudoConstant::getLabel('CRM_Contact_BAO_Contact', $token, $value); } } elseif ($value && CRM_Utils_String::endsWith($token, '_date')) { $value = CRM_Utils_Date::customFormat($value); } } if (!$html) { $value = str_replace('&', '&', $value); } // if null then return actual token if ($returnBlankToken && !$value) { $noReplace = TRUE; } if ($noReplace) { $value = "{contact.$token}"; } if ($escapeSmarty && !($returnBlankToken && $noReplace) ) { // $returnBlankToken means the caller wants to do further attempts at // processing unreplaced tokens -- so don't escape them yet in this case. $value = self::tokenEscapeSmarty($value); } return $value; } /** * Replace all the hook tokens in $str with information from * $contact. * * @param string $str * The string with tokens to be replaced. * @param array $contact * Associative array of contact properties (including hook token values). * @param $categories * @param bool $html * Replace tokens with HTML or plain text. * * @param bool $escapeSmarty * * @return string * The processed string */ public static function &replaceHookTokens( $str, &$contact, &$categories, $html = FALSE, $escapeSmarty = FALSE ) { foreach ($categories as $key) { $str = preg_replace_callback( self::tokenRegex($key), function ($matches) use (&$contact, $key, $html, $escapeSmarty) { return CRM_Utils_Token::getHookTokenReplacement($matches[1], $contact, $key, $html, $escapeSmarty); }, $str ); } return $str; } /** * Parse html through Smarty resolving any smarty functions. * @param string $tokenHtml * @param array $entity * @param string $entityType * @return string * html parsed through smarty */ public static function parseThroughSmarty($tokenHtml, $entity, $entityType = 'contact') { if (defined('CIVICRM_MAIL_SMARTY') && CIVICRM_MAIL_SMARTY) { $smarty = CRM_Core_Smarty::singleton(); // also add the tokens to the template $smarty->assign_by_ref($entityType, $entity); $tokenHtml = $smarty->fetch("string:$tokenHtml"); } return $tokenHtml; } /** * @param $token * @param $contact * @param $category * @param bool $html * @param bool $escapeSmarty * * @return mixed|string */ public static function getHookTokenReplacement( $token, &$contact, $category, $html = FALSE, $escapeSmarty = FALSE ) { $value = CRM_Utils_Array::value("{$category}.{$token}", $contact); if ($value && !$html) { $value = str_replace('&', '&', $value); } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } return $value; } /** * unescapeTokens removes any characters that caused the replacement routines to skip token replacement * for example {{token}} or \{token} will result in {token} in the final email * * this routine will remove the extra backslashes and braces * * @param $str ref to the string that will be scanned and modified */ public static function unescapeTokens(&$str) { $str = preg_replace('/\\\\|\{(\{\w+\.\w+\})\}/', '\\1', $str); } /** * Replace unsubscribe tokens. * * @param string $str * The string with tokens to be replaced. * @param object $domain * The domain BAO. * @param array $groups * The groups (if any) being unsubscribed. * @param bool $html * Replace tokens with html or plain text. * @param int $contact_id * The contact ID. * @param string $hash The security hash of the unsub event * * @return string * The processed string */ public static function &replaceUnsubscribeTokens( $str, &$domain, &$groups, $html, $contact_id, $hash ) { if (self::token_match('unsubscribe', 'group', $str)) { if (!empty($groups)) { $config = CRM_Core_Config::singleton(); $base = CRM_Utils_System::baseURL(); // FIXME: an ugly hack for CRM-2035, to be dropped once CRM-1799 is implemented $dao = new CRM_Contact_DAO_Group(); $dao->find(); while ($dao->fetch()) { if (substr($dao->visibility, 0, 6) == 'Public') { $visibleGroups[] = $dao->id; } } $value = implode(', ', $groups); self::token_replace('unsubscribe', 'group', $value, $str); } } return $str; } /** * Replace resubscribe tokens. * * @param string $str * The string with tokens to be replaced. * @param object $domain * The domain BAO. * @param array $groups * The groups (if any) being resubscribed. * @param bool $html * Replace tokens with html or plain text. * @param int $contact_id * The contact ID. * @param string $hash The security hash of the resub event * * @return string * The processed string */ public static function &replaceResubscribeTokens( $str, &$domain, &$groups, $html, $contact_id, $hash ) { if (self::token_match('resubscribe', 'group', $str)) { if (!empty($groups)) { $value = implode(', ', $groups); self::token_replace('resubscribe', 'group', $value, $str); } } return $str; } /** * Replace subscription-confirmation-request tokens * * @param string $str * The string with tokens to be replaced. * @param string $group * The name of the group being subscribed. * @param $url * @param bool $html * Replace tokens with html or plain text. * * @return string * The processed string */ public static function &replaceSubscribeTokens($str, $group, $url, $html) { if (self::token_match('subscribe', 'group', $str)) { self::token_replace('subscribe', 'group', $group, $str); } if (self::token_match('subscribe', 'url', $str)) { self::token_replace('subscribe', 'url', $url, $str); } return $str; } /** * Replace subscription-invitation tokens * * @param string $str * The string with tokens to be replaced. * * @return string * The processed string */ public static function &replaceSubscribeInviteTokens($str) { if (preg_match('/\{action\.subscribeUrl\}/', $str)) { $url = CRM_Utils_System::url('civicrm/mailing/subscribe', 'reset=1', TRUE, NULL, FALSE, TRUE ); $str = preg_replace('/\{action\.subscribeUrl\}/', $url, $str); } if (preg_match('/\{action\.subscribeUrl.\d+\}/', $str, $matches)) { foreach ($matches as $key => $value) { $gid = substr($value, 21, -1); $url = CRM_Utils_System::url('civicrm/mailing/subscribe', "reset=1&gid={$gid}", TRUE, NULL, FALSE, TRUE ); $str = preg_replace('/' . preg_quote($value) . '/', $url, $str); } } if (preg_match('/\{action\.subscribe.\d+\}/', $str, $matches)) { foreach ($matches as $key => $value) { $gid = substr($value, 18, -1); $config = CRM_Core_Config::singleton(); $domain = CRM_Core_BAO_MailSettings::defaultDomain(); $localpart = CRM_Core_BAO_MailSettings::defaultLocalpart(); // we add the 0.0000000000000000 part to make this match the other email patterns (with action, two ids and a hash) $str = preg_replace('/' . preg_quote($value) . '/', "mailto:{$localpart}s.{$gid}.0.0000000000000000@$domain", $str); } } return $str; } /** * Replace welcome/confirmation tokens * * @param string $str * The string with tokens to be replaced. * @param string $group * The name of the group being subscribed. * @param bool $html * Replace tokens with html or plain text. * * @return string * The processed string */ public static function &replaceWelcomeTokens($str, $group, $html) { if (self::token_match('welcome', 'group', $str)) { self::token_replace('welcome', 'group', $group, $str); } return $str; } /** * Find unprocessed tokens (call this last) * * @param string $str * The string to search. * * @return array * Array of tokens that weren't replaced */ public static function &unmatchedTokens(&$str) { //preg_match_all('/[^\{\\\\]\{(\w+\.\w+)\}[^\}]/', $str, $match); preg_match_all('/\{(\w+\.\w+)\}/', $str, $match); return $match[1]; } /** * Find and replace tokens for each component. * * @param string $str * The string to search. * @param array $contact * Associative array of contact properties. * @param array $components * A list of tokens that are known to exist in the email body. * * @param bool $escapeSmarty * @param bool $returnEmptyToken * * @return string * The processed string */ public static function &replaceComponentTokens(&$str, $contact, $components, $escapeSmarty = FALSE, $returnEmptyToken = TRUE) { if (!is_array($components) || empty($contact)) { return $str; } foreach ($components as $name => $tokens) { if (!is_array($tokens) || empty($tokens)) { continue; } foreach ($tokens as $token) { if (self::token_match($name, $token, $str) && isset($contact[$name . '.' . $token])) { self::token_replace($name, $token, $contact[$name . '.' . $token], $str, $escapeSmarty); } elseif (!$returnEmptyToken) { //replacing empty token self::token_replace($name, $token, "", $str, $escapeSmarty); } } } return $str; } /** * Get array of string tokens. * * @param string $string * The input string to parse for tokens. * * @return array * array of tokens mentioned in field */ public static function getTokens($string) { $matches = array(); $tokens = array(); preg_match_all('/(? $dontCare) { $cfID = CRM_Core_BAO_CustomField::getKeyID($name); if ($cfID) { $custom[] = $cfID; } } $details = CRM_Contact_BAO_Query::apiQuery($params, $returnProperties, NULL, NULL, 0, count($contactIDs), TRUE, FALSE, TRUE, CRM_Contact_BAO_Query::MODE_CONTACTS, NULL, TRUE); $contactDetails = &$details[0]; foreach ($contactIDs as $contactID) { if (array_key_exists($contactID, $contactDetails)) { if (!empty($contactDetails[$contactID]['preferred_communication_method']) ) { $communicationPreferences = array(); foreach ($contactDetails[$contactID]['preferred_communication_method'] as $val) { if ($val) { $communicationPreferences[$val] = CRM_Core_PseudoConstant::getLabel('CRM_Contact_DAO_Contact', 'preferred_communication_method', $val); } } $contactDetails[$contactID]['preferred_communication_method'] = implode(', ', $communicationPreferences); } foreach ($custom as $cfID) { if (isset($contactDetails[$contactID]["custom_{$cfID}"])) { $contactDetails[$contactID]["custom_{$cfID}"] = CRM_Core_BAO_CustomField::displayValue($contactDetails[$contactID]["custom_{$cfID}"], $cfID); } } // special case for greeting replacement foreach (array( 'email_greeting', 'postal_greeting', 'addressee', ) as $val) { if (!empty($contactDetails[$contactID][$val])) { $contactDetails[$contactID][$val] = $contactDetails[$contactID]["{$val}_display"]; } } } } // also call a hook and get token details CRM_Utils_Hook::tokenValues($details[0], $contactIDs, $jobID, $tokens, $className ); return $details; } /** * Call hooks on tokens for anonymous users - contact id is set to 0 - this allows non-contact * specific tokens to be rendered * * @param array $contactIDs * This should always be array(0) or its not anonymous - left to keep signature same. * as main fn * @param string $returnProperties * @param bool $skipOnHold * @param bool $skipDeceased * @param string $extraParams * @param array $tokens * @param string $className * Sent as context to the hook. * @param string $jobID * @return array * contactDetails with hooks swapped out */ public static function getAnonymousTokenDetails($contactIDs = array( 0, ), $returnProperties = NULL, $skipOnHold = TRUE, $skipDeceased = TRUE, $extraParams = NULL, $tokens = array(), $className = NULL, $jobID = NULL) { $details = array(0 => array()); // also call a hook and get token details CRM_Utils_Hook::tokenValues($details[0], $contactIDs, $jobID, $tokens, $className ); return $details; } /** * Get Membership Token Details. * @param array $membershipIDs * Array of membership IDS. */ public static function getMembershipTokenDetails($membershipIDs) { $memberships = civicrm_api3('membership', 'get', array( 'options' => array('limit' => 0), 'membership_id' => array('IN' => (array) $membershipIDs), )); return $memberships['values']; } /** * Replace existing greeting tokens in message/subject. * * This function operates by reference, modifying the first parameter. Other * methods for token replacement in this class return the modified string. * This leads to inconsistency in how these methods must be applied. * * @TODO Remove that inconsistency in usage. * * ::replaceContactTokens() may need to be called after this method, to * replace tokens supplied from this method. * * @param string $tokenString * @param array $contactDetails * @param int $contactId * @param string $className * @param bool $escapeSmarty */ public static function replaceGreetingTokens(&$tokenString, $contactDetails = NULL, $contactId = NULL, $className = NULL, $escapeSmarty = FALSE) { if (!$contactDetails && !$contactId) { return; } // check if there are any tokens $greetingTokens = self::getTokens($tokenString); if (!empty($greetingTokens)) { // first use the existing contact object for token replacement if (!empty($contactDetails)) { $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString, $contactDetails, TRUE, $greetingTokens, TRUE, $escapeSmarty); } self::removeNullContactTokens($tokenString, $contactDetails, $greetingTokens); // check if there are any unevaluated tokens $greetingTokens = self::getTokens($tokenString); // $greetingTokens not empty, means there are few tokens which are not // evaluated, like custom data etc // so retrieve it from database if (!empty($greetingTokens) && array_key_exists('contact', $greetingTokens)) { $greetingsReturnProperties = array_flip(CRM_Utils_Array::value('contact', $greetingTokens)); $greetingsReturnProperties = array_fill_keys(array_keys($greetingsReturnProperties), 1); $contactParams = array('contact_id' => $contactId); $greetingDetails = self::getTokenDetails($contactParams, $greetingsReturnProperties, FALSE, FALSE, NULL, $greetingTokens, $className ); // again replace tokens $tokenString = CRM_Utils_Token::replaceContactTokens($tokenString, $greetingDetails, TRUE, $greetingTokens, TRUE, $escapeSmarty ); } // check if there are still any unevaluated tokens $remainingTokens = self::getTokens($tokenString); // $greetingTokens not empty, there are customized or hook tokens to replace if (!empty($remainingTokens)) { // Fill the return properties array $greetingTokens = $remainingTokens; reset($greetingTokens); $greetingsReturnProperties = array(); foreach ($greetingTokens as $value) { $props = array_flip($value); $props = array_fill_keys(array_keys($props), 1); $greetingsReturnProperties = $greetingsReturnProperties + $props; } $contactParams = array('contact_id' => $contactId); $greetingDetails = self::getTokenDetails($contactParams, $greetingsReturnProperties, FALSE, FALSE, NULL, $greetingTokens, $className ); // Prepare variables for calling replaceHookTokens $categories = array_keys($greetingTokens); list($contact) = $greetingDetails; // Replace tokens defined in Hooks. $tokenString = CRM_Utils_Token::replaceHookTokens($tokenString, $contact[$contactId], $categories); } } } /** * At this point, $contactDetails has loaded the contact from the DAO. Any * (non-custom) missing fields are null. By removing them, we can avoid * expensive calls to CRM_Contact_BAO_Query. * * @param string $tokenString * @param array $contactDetails */ private static function removeNullContactTokens(&$tokenString, $contactDetails, &$greetingTokens) { $greetingTokensOriginal = $greetingTokens; $contactFieldList = CRM_Contact_DAO_Contact::fields(); // Sometimes contactDetails are in a multidemensional array, sometimes a // single-dimension array. if (array_key_exists(0, $contactDetails) && is_array($contactDetails[0])) { $contactDetails = current($contactDetails[0]); } $nullFields = array_keys(array_diff_key($contactFieldList, $contactDetails)); // Handle legacy tokens foreach (self::legacyContactTokens() as $oldToken => $newToken) { if (CRM_Utils_Array::key($newToken, $nullFields)) { $nullFields[] = $oldToken; } } // Remove null contact fields from $greetingTokens $greetingTokens['contact'] = array_diff($greetingTokens['contact'], $nullFields); // Also remove them from $tokenString $removedTokens = array_diff($greetingTokensOriginal['contact'], $greetingTokens['contact']); // Handle legacy tokens again, sigh if (!empty($removedTokens)) { foreach ($removedTokens as $token) { if (CRM_Utils_Array::value($token, self::legacyContactTokens()) !== NULL) { $removedTokens[] = CRM_Utils_Array::value($token, self::legacyContactTokens()); } } foreach ($removedTokens as $token) { $tokenString = str_replace("{contact.$token}", '', $tokenString); } } } /** * @param $tokens * * @return array */ public static function flattenTokens(&$tokens) { $flattenTokens = array(); foreach (array( 'html', 'text', 'subject', ) as $prop) { if (!isset($tokens[$prop])) { continue; } foreach ($tokens[$prop] as $type => $names) { if (!isset($flattenTokens[$type])) { $flattenTokens[$type] = array(); } foreach ($names as $name) { $flattenTokens[$type][$name] = 1; } } } return $flattenTokens; } /** * Replace all user tokens in $str * * @param string $str * The string with tokens to be replaced. * * @param null $knownTokens * @param bool $escapeSmarty * * @return string * The processed string */ public static function &replaceUserTokens($str, $knownTokens = NULL, $escapeSmarty = FALSE) { $key = 'user'; if (!$knownTokens || !isset($knownTokens[$key]) ) { return $str; } $str = preg_replace_callback( self::tokenRegex($key), function ($matches) use ($escapeSmarty) { return CRM_Utils_Token::getUserTokenReplacement($matches[1], $escapeSmarty); }, $str ); return $str; } /** * @param $token * @param bool $escapeSmarty * * @return string */ public static function getUserTokenReplacement($token, $escapeSmarty = FALSE) { $value = ''; list($objectName, $objectValue) = explode('-', $token, 2); switch ($objectName) { case 'permission': $value = CRM_Core_Permission::permissionEmails($objectValue); break; case 'role': $value = CRM_Core_Permission::roleEmails($objectValue); break; } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } return $value; } protected static function _buildContributionTokens() { $key = 'contribution'; if (self::$_tokens[$key] == NULL) { self::$_tokens[$key] = array_keys(array_merge(CRM_Contribute_BAO_Contribution::exportableFields('All'), array('campaign', 'financial_type'), self::getCustomFieldTokens('Contribution') )); } } /** * Store membership tokens on the static _tokens array. */ protected static function _buildMembershipTokens() { $key = 'membership'; if (!isset(self::$_tokens[$key]) || self::$_tokens[$key] == NULL) { $membershipTokens = array(); $tokens = CRM_Core_SelectValues::membershipTokens(); foreach ($tokens as $token => $dontCare) { $membershipTokens[] = substr($token, (strpos($token, '.') + 1), -1); } self::$_tokens[$key] = $membershipTokens; } } /** * Replace tokens for an entity. * @param string $entity * @param array $entityArray * (e.g. in format from api). * @param string $str * String to replace in. * @param array $knownTokens * Array of tokens present. * @param bool $escapeSmarty * @return string * string with replacements made */ public static function replaceEntityTokens($entity, $entityArray, $str, $knownTokens = array(), $escapeSmarty = FALSE) { if (!$knownTokens || empty($knownTokens[$entity])) { return $str; } $fn = 'get' . ucfirst($entity) . 'TokenReplacement'; $fn = is_callable(array('CRM_Utils_Token', $fn)) ? $fn : 'getApiTokenReplacement'; // since we already know the tokens lets just use them & do str_replace which is faster & simpler than preg_replace foreach ($knownTokens[$entity] as $token) { $replacement = self::$fn($entity, $token, $entityArray); if ($escapeSmarty) { $replacement = self::tokenEscapeSmarty($replacement); } $str = str_replace('{' . $entity . '.' . $token . '}', $replacement, $str); } return preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str); } /** * @param int $caseId * @param int $str * @param array $knownTokens * @param bool $escapeSmarty * @return string * @throws \CiviCRM_API3_Exception */ public static function replaceCaseTokens($caseId, $str, $knownTokens = array(), $escapeSmarty = FALSE) { if (!$knownTokens || empty($knownTokens['case'])) { return $str; } $case = civicrm_api3('case', 'getsingle', array('id' => $caseId)); return self::replaceEntityTokens('case', $case, $str, $knownTokens, $escapeSmarty); } /** * Generic function for formatting token replacement for an api field * * @param string $entity * @param string $token * @param array $entityArray * @return string * @throws \CiviCRM_API3_Exception */ public static function getApiTokenReplacement($entity, $token, $entityArray) { if (!isset($entityArray[$token])) { return ''; } $field = civicrm_api3($entity, 'getfield', array('action' => 'get', 'name' => $token, 'get_options' => 'get')); $field = $field['values']; $fieldType = CRM_Utils_Array::value('type', $field); // Boolean fields if ($fieldType == CRM_Utils_Type::T_BOOLEAN && empty($field['options'])) { $field['options'] = array(ts('No'), ts('Yes')); } // Match pseudoconstants if (!empty($field['options'])) { $ret = array(); foreach ((array) $entityArray[$token] as $val) { $ret[] = $field['options'][$val]; } return implode(', ', $ret); } // Format date fields elseif ($entityArray[$token] && $fieldType == CRM_Utils_Type::T_DATE) { return CRM_Utils_Date::customFormat($entityArray[$token]); } return implode(', ', (array) $entityArray[$token]); } /** * Replace Contribution tokens in html. * * @param string $str * @param array $contribution * @param bool|string $html * @param string $knownTokens * @param bool|string $escapeSmarty * * @return mixed */ public static function replaceContributionTokens($str, &$contribution, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE) { $key = 'contribution'; if (!$knownTokens || !CRM_Utils_Array::value($key, $knownTokens)) { return $str; //early return } self::_buildContributionTokens(); // here we intersect with the list of pre-configured valid tokens // so that we remove anything we do not recognize // I hope to move this step out of here soon and // then we will just iterate on a list of tokens that are passed to us $str = preg_replace_callback( self::tokenRegex($key), function ($matches) use (&$contribution, $html, $escapeSmarty) { return CRM_Utils_Token::getContributionTokenReplacement($matches[1], $contribution, $html, $escapeSmarty); }, $str ); $str = preg_replace('/\\\\|\{(\s*)?\}/', ' ', $str); return $str; } /** * We have a situation where we are rendering more than one token in each field because we are combining * tokens from more than one contribution when pdf thank you letters are grouped (CRM-14367) * * The replaceContributionToken doesn't handle receive_date correctly in this scenario because of the formatting * it applies (other tokens are OK including date fields) * * So we sort this out & then call the main function. Note that we are not escaping smarty on this fields like the main function * does - but the fields is already being formatted through a date function * * @param string $separator * @param string $str * @param array $contribution * @param bool|string $html * @param string $knownTokens * @param bool|string $escapeSmarty * * @return string */ public static function replaceMultipleContributionTokens($separator, $str, &$contribution, $html = FALSE, $knownTokens = NULL, $escapeSmarty = FALSE) { if (empty($knownTokens['contribution'])) { return $str; } if (in_array('receive_date', $knownTokens['contribution'])) { $formattedDates = array(); $dates = explode($separator, $contribution['receive_date']); foreach ($dates as $date) { $formattedDates[] = CRM_Utils_Date::customFormat($date, NULL, array('j', 'm', 'Y')); } $str = str_replace("{contribution.receive_date}", implode($separator, $formattedDates), $str); unset($knownTokens['contribution']['receive_date']); } return self::replaceContributionTokens($str, $contribution, $html, $knownTokens, $escapeSmarty); } /** * Get replacement strings for any membership tokens (only a small number of tokens are implemnted in the first instance * - this is used by the pdfLetter task from membership search * @param string $entity * should always be "membership" * @param string $token * field name * @param array $membership * An api result array for a single membership. * @return string token replacement */ public static function getMembershipTokenReplacement($entity, $token, $membership) { self::_buildMembershipTokens(); switch ($token) { case 'type': $value = $membership['membership_name']; break; case 'status': $statuses = CRM_Member_BAO_Membership::buildOptions('status_id'); $value = $statuses[$membership['status_id']]; break; case 'fee': try { $value = civicrm_api3('membership_type', 'getvalue', array( 'id' => $membership['membership_type_id'], 'return' => 'minimum_fee', )); $value = CRM_Utils_Money::format($value, NULL, NULL, TRUE); } catch (CiviCRM_API3_Exception $e) { // we can anticipate we will get an error if the minimum fee is set to 'NULL' because of the way the // api handles NULL (4.4) $value = 0; } break; default: if (in_array($token, self::$_tokens[$entity])) { $value = $membership[$token]; if (CRM_Utils_String::endsWith($token, '_date')) { $value = CRM_Utils_Date::customFormat($value); } } else { // ie unchanged $value = "{$entity}.{$token}"; } break; } return $value; } /** * @param $token * @param $contribution * @param bool $html * @param bool $escapeSmarty * * @return mixed|string */ public static function getContributionTokenReplacement($token, &$contribution, $html = FALSE, $escapeSmarty = FALSE) { self::_buildContributionTokens(); switch ($token) { case 'total_amount': case 'net_amount': case 'fee_amount': case 'non_deductible_amount': $value = CRM_Utils_Money::format(CRM_Utils_Array::retrieveValueRecursive($contribution, $token)); break; case 'receive_date': $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token); $value = CRM_Utils_Date::customFormat($value, NULL, array('j', 'm', 'Y')); break; default: if (!in_array($token, self::$_tokens['contribution'])) { $value = "{contribution.$token}"; } else { $value = CRM_Utils_Array::retrieveValueRecursive($contribution, $token); } break; } if ($escapeSmarty) { $value = self::tokenEscapeSmarty($value); } return $value; } /** * @return array * [legacy_token => new_token] */ public static function legacyContactTokens() { return array( 'individual_prefix' => 'prefix_id', 'individual_suffix' => 'suffix_id', 'gender' => 'gender_id', 'communication_style' => 'communication_style_id', ); } /** * Get all custom field tokens of $entity * * @param string $entity * @param bool $usedForTokenWidget * * @return array $customTokens * return custom field tokens in array('custom_N' => 'label') format */ public static function getCustomFieldTokens($entity, $usedForTokenWidget = FALSE) { $customTokens = array(); $tokenName = $usedForTokenWidget ? "{contribution.custom_%d}" : "custom_%d"; foreach (CRM_Core_BAO_CustomField::getFields($entity) as $id => $info) { $customTokens[sprintf($tokenName, $id)] = $info['label']; } return $customTokens; } /** * Formats a token list for the select2 widget * * @param $tokens * @return array */ public static function formatTokensForDisplay($tokens) { $sorted = $output = array(); // Sort in ascending order by ignoring word case natcasesort($tokens); // Attempt to place tokens into optgroups // @todo These groupings could be better and less hackish. Getting them pre-grouped from upstream would be nice. foreach ($tokens as $k => $v) { // Check to see if this token is already in a group e.g. for custom fields $split = explode(' :: ', $v); if (!empty($split[1])) { $sorted[$split[1]][] = array('id' => $k, 'text' => $split[0]); } // Group by entity else { $split = explode('.', trim($k, '{}')); if (isset($split[1])) { $entity = array_key_exists($split[1], CRM_Core_DAO_Address::export()) ? 'Address' : ucfirst($split[0]); } else { $entity = 'Contact'; } $sorted[ts($entity)][] = array('id' => $k, 'text' => $v); } } ksort($sorted); foreach ($sorted as $k => $v) { $output[] = array('text' => $k, 'children' => $v); } return $output; } }