2 if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
3 /*********************************************************************************
4 * SugarCRM Community Edition is a customer relationship management program developed by
5 * SugarCRM, Inc. Copyright (C) 2004-2013 SugarCRM Inc.
7 * This program is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU Affero General Public License version 3 as published by the
9 * Free Software Foundation with the addition of the following permission added
10 * to Section 15 as permitted in Section 7(a): FOR ANY PART OF THE COVERED WORK
11 * IN WHICH THE COPYRIGHT IS OWNED BY SUGARCRM, SUGARCRM DISCLAIMS THE WARRANTY
12 * OF NON INFRINGEMENT OF THIRD PARTY RIGHTS.
14 * This program is distributed in the hope that it will be useful, but WITHOUT
15 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
16 * FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more
19 * You should have received a copy of the GNU Affero General Public License along with
20 * this program; if not, see http://www.gnu.org/licenses or write to the Free
21 * Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
24 * You can contact SugarCRM, Inc. headquarters at 10050 North Wolfe Road,
25 * SW2-130, Cupertino, CA 95014, USA. or at email address contact@sugarcrm.com.
27 * The interactive user interfaces in modified source and object code versions
28 * of this program must display Appropriate Legal Notices, as required under
29 * Section 5 of the GNU Affero General Public License version 3.
31 * In accordance with Section 7(b) of the GNU Affero General Public License version 3,
32 * these Appropriate Legal Notices must retain the display of the "Powered by
33 * SugarCRM" logo. If the display of the logo is not reasonably feasible for
34 * technical reasons, the Appropriate Legal Notices must display the words
35 * "Powered by SugarCRM".
36 ********************************************************************************/
38 /*********************************************************************************
40 * Description: TODO: To be written.
41 * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
42 * All Rights Reserved.
43 * Contributor(s): ______________________________________..
44 ********************************************************************************/
51 // EmailTemplate is used to store email email_template information.
52 class EmailTemplate extends SugarBean {
53 var $field_name_map = array();
58 var $modified_user_id;
61 var $modified_by_name;
62 var $assigned_user_id;
63 var $assigned_user_name;
73 var $table_name = "email_templates";
74 var $object_name = "EmailTemplate";
75 var $module_dir = "EmailTemplates";
76 var $new_schema = true;
77 // This is used to retrieve related fields from form posts.
78 var $additional_column_fields = array(
80 // add fields here that would not make sense in an email template
81 var $badFields = array(
82 'account_description',
88 'opportunity_role_id',
89 'opportunity_role_fields',
104 'receive_notifications',
110 'accept_status_name',
114 * @var array temp storage for template variables while cleanBean
116 protected $storedVariables = array();
118 function EmailTemplate() {
123 * Generates the extended field_defs for creating macros
124 * @param object $bean SugarBean
125 * @param string $prefix "contact_", "user_" etc.
128 function generateFieldDefsJS() {
129 global $current_user;
135 $contact = new Contact();
136 $account = new Account();
138 $prospect = new Prospect();
141 $loopControl = array(
143 'Contacts' => $contact,
145 'Prospects' => $prospect,
148 'Accounts' => $account,
151 'Users' => $current_user,
156 'Contacts' => 'contact_',
157 'Accounts' => 'account_',
158 'Users' => 'contact_user_',
161 $collection = array();
162 foreach($loopControl as $collectionKey => $beans) {
163 $collection[$collectionKey] = array();
164 foreach($beans as $beankey => $bean) {
166 foreach($bean->field_defs as $key => $field_def) {
167 if( ($field_def['type'] == 'relate' && empty($field_def['custom_type'])) ||
168 ($field_def['type'] == 'assigned_user_name' || $field_def['type'] =='link') ||
169 ($field_def['type'] == 'bool') ||
170 (in_array($field_def['name'], $this->badFields)) ) {
173 if(!isset($field_def['vname'])) {
176 // valid def found, process
177 $optionKey = strtolower("{$prefixes[$collectionKey]}{$key}");
178 $optionLabel = preg_replace('/:$/', "", translate($field_def['vname'], $beankey));
180 foreach ($collection[$collectionKey] as $value){
181 if($value['name']==$optionKey){
187 $collection[$collectionKey][] = array("name" => $optionKey, "value" => $optionLabel);
192 $json = getJSONobj();
193 $ret = "var field_defs = ";
194 $ret .= $json->encode($collection, false);
199 function get_summary_text() {
200 return "$this->name";
203 function create_export_query(&$order_by, &$where) {
204 return $this->create_new_list_query($order_by, $where);
207 function fill_in_additional_list_fields() {
208 $this->fill_in_additional_parent_fields();
211 function fill_in_additional_detail_fields() {
212 if (empty($this->body) && !empty($this->body_html))
214 global $sugar_config;
215 $this->body = strip_tags(html_entity_decode($this->body_html, ENT_COMPAT, $sugar_config['default_charset']));
217 $this->created_by_name = get_assigned_user_name($this->created_by);
218 $this->modified_by_name = get_assigned_user_name($this->modified_user_id);
219 $this->assigned_user_name = get_assigned_user_name($this->assigned_user_id);
220 $this->fill_in_additional_parent_fields();
223 function fill_in_additional_parent_fields() {
226 function get_list_view_data() {
227 global $app_list_strings, $focus, $action, $currentModule;
228 $fields = $this->get_list_view_array();
229 $fields["DATE_MODIFIED"] = substr($fields["DATE_MODIFIED"], 0 , 10);
233 //function all string that match the pattern {.} , also catches the list of found strings.
234 //the cache will get refreshed when the template bean instance changes.
235 //The found url key patterns are replaced with name value pairs provided as function parameter. $tracked_urls.
236 //$url_template is used to construct the url for the email message. the template should have place holder for 1 variable parameter, represented by %1
237 //$template_text_array is a list of text strings that need to be searched. usually the subject, html body and text body of the email message.
238 //$removeme_url_template, if the url has is_optout property checked then use this template.
239 function parse_tracker_urls($template_text_array,$url_template,$tracked_urls,$removeme_url_template) {
240 global $beanFiles,$beanList, $app_list_strings,$sugar_config;
241 if (!isset($this->parsed_urls))
242 $this->parsed_urls=array();
244 $return_array = $template_text_array;
245 if(count($tracked_urls) > 0)
247 //parse the template and find all the dynamic strings that need replacement.
248 foreach ($template_text_array as $key=>$template_text) {
249 if (!empty($template_text)) {
251 if(!isset($this->parsed_urls[$key]) || $this->parsed_urls[$key]['text'] != $template_text) {
253 $template_text = urldecode($template_text);
254 $matches = $this->_preg_match_tracker_url($template_text);
255 $count = count($matches[0]);
256 $this->parsed_urls[$key]=array('matches' => $matches, 'text' => $template_text);
258 $matches=$this->parsed_urls[$key]['matches'];
259 if(!empty($matches[0])) {
260 $count=count($matches[0]);
266 //navigate thru all the matched keys and replace the keys with actual strings.
270 for ($i=($count -1); $i>=0; $i--) {
271 $url_key_name=$matches[0][$i][0];
272 if (!empty($tracked_urls[$url_key_name])) {
273 if ($tracked_urls[$url_key_name]['is_optout']==1){
274 $tracker_url = $removeme_url_template;
276 $tracker_url = sprintf($url_template,$tracked_urls[$url_key_name]['id']);
279 if(!empty($tracker_url) && !empty($template_text) && !empty($matches[0][$i][0]) && !empty($tracked_urls[$matches[0][$i][0]])){
280 $template_text=substr_replace($template_text,$tracker_url,$matches[0][$i][1], strlen($matches[0][$i][0]));
281 $template_text=str_replace($sugar_config['site_url'].'/'.$sugar_config['site_url'],$sugar_config['site_url'],$template_text);
286 $return_array[$key]=$template_text;
289 return $return_array;
294 * Method for replace "preg_match_all" in method "parse_tracker_urls"
295 * @param $text string String in which we need to search all string that match the pattern {.}
296 * @return array result of search
298 private function _preg_match_tracker_url($text)
303 for($i = 0; $i < strlen($text); $i++)
310 elseif($text[$i] == '}' && $switch === true)
313 array_push($result, array(substr($text, $ind, $i - $ind + 1), $ind));
316 return array($result);
319 function parse_email_template($template_text_array, $focus_name, $focus, &$macro_nv) {
322 global $beanFiles, $beanList, $app_list_strings;
324 // generate User instance that owns this "Contact" for contact_user_* macros
326 if(isset($focus->assigned_user_id) && !empty($focus->assigned_user_id)){
327 $user->retrieve($focus->assigned_user_id);
330 if(!isset($this->parsed_entities))
331 $this->parsed_entities=array();
333 //parse the template and find all the dynamic strings that need replacement.
334 // Bug #48111 It's strange why prefix for User module is contact_user (see self::generateFieldDefsJS method)
335 if ($beanList[$focus_name] == 'User')
337 $pattern_prefix = '$contact_user_';
341 $pattern_prefix = '$'.strtolower($beanList[$focus_name]).'_';
343 $pattern_prefix_length = strlen($pattern_prefix);
344 $pattern = '/\\'.$pattern_prefix.'[A-Za-z_0-9]*/';
346 foreach($template_text_array as $key=>$template_text) {
347 if(!isset($this->parsed_entities[$key])) {
349 $count = preg_match_all($pattern, $template_text, $matches, PREG_OFFSET_CAPTURE);
352 for($i=($count -1); $i>=0; $i--) {
353 if(!isset($matches[0][$i][2])) {
354 //find the field name in the bean.
355 $matches[0][$i][2]=substr($matches[0][$i][0],$pattern_prefix_length,strlen($matches[0][$i][0]) - $pattern_prefix_length);
357 //store the localized strings if the field is of type enum..
358 if(isset($focus->field_defs[$matches[0][$i][2]]) && $focus->field_defs[$matches[0][$i][2]]['type']=='enum' && isset($focus->field_defs[$matches[0][$i][2]]['options'])) {
359 $matches[0][$i][3]=$focus->field_defs[$matches[0][$i][2]]['options'];
364 $this->parsed_entities[$key]=$matches;
366 $matches=$this->parsed_entities[$key];
367 if(!empty($matches[0])) {
368 $count=count($matches[0]);
374 for ($i=($count -1); $i>=0; $i--) {
375 $field_name=$matches[0][$i][2];
377 // cn: feel for User object attribute key and assign as found
378 if(strpos($field_name, "user_") === 0) {
379 $userFieldName = substr($field_name, 5);
380 $value = $user->$userFieldName;
381 //_pp($userFieldName."[{$value}]");
383 $value = $focus->{$field_name};
387 if(isset($matches[0][$i][3])) {
388 if(isset($app_list_strings[$matches[0][$i][3]][$value])) {
389 $value=$app_list_strings[$matches[0][$i][3]][$value];
393 //generate name value pair array of macros and corresponding values for the targed.
394 $macro_nv[$matches[0][$i][0]] =$value;
396 $template_text=substr_replace($template_text,$value,$matches[0][$i][1], strlen($matches[0][$i][0]));
399 //parse the template for tracker url strings. patter for these strings in {[a-zA-Z_0-9]+}
401 $return_array[$key]=$template_text;
404 return $return_array;
408 * Convenience method to convert raw value into appropriate type format
409 * @param string $type
410 * @param string $value
413 function _convertToType($type,$value) {
415 case 'currency' : return currency_format_number($value);
416 default: return $value;
421 * Convenience method to parse for user's values in a template
422 * @param array $repl_arr
423 * @param object $user
426 function _parseUserValues($repl_arr, &$user) {
427 foreach($user->field_defs as $field_def) {
428 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
432 if($field_def['type'] == 'enum') {
433 $translated = translate($field_def['options'], 'Users', $user->$field_def['name']);
435 if(isset($translated) && ! is_array($translated)) {
436 $repl_arr["contact_user_".$field_def['name']] = $translated;
437 } else { // unset enum field, make sure we have a match string to replace with ""
438 $repl_arr["contact_user_".$field_def['name']] = '';
441 if(isset($user->$field_def['name'])) {
442 // bug 47647 - allow for fields to translate before adding to template
443 $repl_arr["contact_user_".$field_def['name']] = self::_convertToType($field_def['type'],$user->$field_def['name']);
445 $repl_arr["contact_user_".$field_def['name']] = "";
454 function parse_template_bean($string, $bean_name, &$focus) {
455 global $current_user;
456 global $beanFiles, $beanList;
459 // cn: bug 9277 - create a replace array with empty strings to blank-out invalid vars
460 $acct = new Account();
461 $contact = new Contact();
463 $prospect = new Prospect();
465 foreach($lead->field_defs as $field_def) {
466 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
469 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
470 'contact_' . $field_def['name'] => '',
471 'contact_account_' . $field_def['name'] => '',
474 foreach($prospect->field_defs as $field_def) {
475 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
478 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
479 'contact_' . $field_def['name'] => '',
480 'contact_account_' . $field_def['name'] => '',
483 foreach($contact->field_defs as $field_def) {
484 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
487 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
488 'contact_' . $field_def['name'] => '',
489 'contact_account_' . $field_def['name'] => '',
492 foreach($acct->field_defs as $field_def) {
493 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
496 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
497 'account_' . $field_def['name'] => '',
498 'account_contact_' . $field_def['name'] => '',
501 // cn: end bug 9277 fix
504 // feel for Parent account, only for Contacts traditionally, but written for future expansion
505 if(isset($focus->account_id) && !empty($focus->account_id)) {
506 $acct->retrieve($focus->account_id);
509 if($bean_name == 'Contacts') {
510 // cn: bug 9277 - email templates not loading account/opp info for templates
511 if(!empty($acct->id)) {
512 foreach($acct->field_defs as $field_def) {
513 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
517 if($field_def['type'] == 'enum') {
518 $translated = translate($field_def['options'], 'Accounts' ,$acct->$field_def['name']);
520 if(isset($translated) && ! is_array($translated)) {
521 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
522 'account_' . $field_def['name'] => $translated,
523 'contact_account_' . $field_def['name'] => $translated,
525 } else { // unset enum field, make sure we have a match string to replace with ""
526 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
527 'account_' . $field_def['name'] => '',
528 'contact_account_' . $field_def['name'] => '',
532 // bug 47647 - allow for fields to translate before adding to template
533 $translated = self::_convertToType($field_def['type'],$acct->$field_def['name']);
534 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
535 'account_' . $field_def['name'] => $translated,
536 'contact_account_' . $field_def['name'] => $translated,
542 if(!empty($focus->assigned_user_id)) {
544 $user->retrieve($focus->assigned_user_id);
545 $repl_arr = EmailTemplate::_parseUserValues($repl_arr, $user);
547 } elseif($bean_name == 'Users') {
549 * This section of code will on do work when a blank Contact, Lead,
550 * etc. is passed in to parse the contact_* vars. At this point,
551 * $current_user will be used to fill in the blanks.
553 $repl_arr = EmailTemplate::_parseUserValues($repl_arr, $current_user);
555 // assumed we have an Account in focus
556 foreach($contact->field_defs as $field_def) {
557 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name' || $field_def['type'] == 'link') {
561 if($field_def['type'] == 'enum') {
562 $translated = translate($field_def['options'], 'Accounts' ,$contact->$field_def['name']);
564 if(isset($translated) && ! is_array($translated)) {
565 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
566 'contact_' . $field_def['name'] => $translated,
567 'contact_account_' . $field_def['name'] => $translated,
569 } else { // unset enum field, make sure we have a match string to replace with ""
570 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
571 'contact_' . $field_def['name'] => '',
572 'contact_account_' . $field_def['name'] => '',
576 if (isset($contact->$field_def['name'])) {
577 // bug 47647 - allow for fields to translate before adding to template
578 $translated = self::_convertToType($field_def['type'],$contact->$field_def['name']);
579 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
580 'contact_' . $field_def['name'] => $translated,
581 'contact_account_' . $field_def['name'] => $translated,
588 ///////////////////////////////////////////////////////////////////////
589 //// LOAD FOCUS DATA INTO REPL_ARR
590 foreach($focus->field_defs as $field_def) {
591 if(isset($focus->$field_def['name'])) {
592 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
596 if($field_def['type'] == 'enum' && isset($field_def['options'])) {
597 $translated = translate($field_def['options'],$bean_name,$focus->$field_def['name']);
599 if(isset($translated) && ! is_array($translated)) {
600 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
601 strtolower($beanList[$bean_name])."_".$field_def['name'] => $translated,
603 } else { // unset enum field, make sure we have a match string to replace with ""
604 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
605 strtolower($beanList[$bean_name])."_".$field_def['name'] => '',
609 // bug 47647 - translate currencies to appropriate values
610 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
611 strtolower($beanList[$bean_name])."_".$field_def['name'] => self::_convertToType($field_def['type'],$focus->$field_def['name']),
615 if($field_def['name'] == 'full_name') {
616 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
617 strtolower($beanList[$bean_name]).'_full_name' => $focus->get_summary_text(),
620 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
621 strtolower($beanList[$bean_name])."_".$field_def['name'] => '',
629 //20595 add nl2br() to respect the multi-lines formatting
630 if(isset($repl_arr['contact_primary_address_street'])){
631 $repl_arr['contact_primary_address_street'] = nl2br($repl_arr['contact_primary_address_street']);
633 if(isset($repl_arr['contact_alt_address_street'])){
634 $repl_arr['contact_alt_address_street'] = nl2br($repl_arr['contact_alt_address_street']);
637 foreach ($repl_arr as $name=>$value) {
638 if($value != '' && is_string($value)) {
639 $string = str_replace("\$$name", $value, $string);
641 $string = str_replace("\$$name", ' ', $string);
649 * Add replacement(s) to the collection based on field definition
652 * @param array $field_def
653 * @param array $replacement
656 protected static function add_replacement($data, $field_def, $replacement)
658 foreach ($replacement as $key => $value)
660 // @see defect #48641
661 if ('multienum' == $field_def['type']) {
662 $value = implode(', ', unencodeMultienum($value));
664 $data[$key] = $value;
669 function parse_template($string, &$bean_arr) {
670 global $beanFiles, $beanList;
672 foreach($bean_arr as $bean_name => $bean_id) {
673 require_once($beanFiles[$beanList[$bean_name]]);
675 $focus = new $beanList[$bean_name];
676 $result = $focus->retrieve($bean_id);
678 if($bean_name == 'Leads' || $bean_name == 'Prospects') {
679 $bean_name = 'Contacts';
682 if(isset($this) && isset($this->module_dir) && $this->module_dir == 'EmailTemplates') {
683 $string = $this->parse_template_bean($string, $bean_name, $focus);
685 $string = EmailTemplate::parse_template_bean($string, $bean_name, $focus);
691 function bean_implements($interface) {
693 case 'ACL':return true;
698 static function getTypeOptionsForSearch(){
699 $template = new EmailTemplate();
700 $optionKey = $template->field_defs['type']['options'];
701 $options = $GLOBALS['app_list_strings'][$optionKey];
702 if( ! is_admin($GLOBALS['current_user']) && isset($options['workflow']))
703 unset($options['workflow']);
708 function is_used_by_email_marketing() {
709 $query = "select id from email_marketing where template_id='$this->id' and deleted=0";
710 $result = $this->db->query($query);
711 if($this->db->fetchByAssoc($result)) {
718 * Allows us to save variables of template as they are
720 public function cleanBean()
722 $this->storedVariables = array();
723 $this->body_html = preg_replace_callback('/\{::[^}]+::\}/', array($this, 'storeVariables'), $this->body_html);
725 $this->body_html = str_replace(array_values($this->storedVariables), array_keys($this->storedVariables), $this->body_html);
729 * Replacing variables of templates by their md5 hash
731 * @param array $text result of preg_replace_callback
732 * @return string md5 hash of result
734 protected function storeVariables($text)
736 if (isset($this->storedVariables[$text[0]]) == false) {
737 $this->storedVariables[$text[0]] = md5($text[0]);
739 return $this->storedVariables[$text[0]];