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-2012 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',
113 function EmailTemplate() {
118 * Generates the extended field_defs for creating macros
119 * @param object $bean SugarBean
120 * @param string $prefix "contact_", "user_" etc.
123 function generateFieldDefsJS() {
124 global $current_user;
130 $contact = new Contact();
131 $account = new Account();
133 $prospect = new Prospect();
136 $loopControl = array(
138 'Contacts' => $contact,
140 'Prospects' => $prospect,
143 'Accounts' => $account,
146 'Users' => $current_user,
151 'Contacts' => 'contact_',
152 'Accounts' => 'account_',
153 'Users' => 'contact_user_',
156 $collection = array();
157 foreach($loopControl as $collectionKey => $beans) {
158 $collection[$collectionKey] = array();
159 foreach($beans as $beankey => $bean) {
161 foreach($bean->field_defs as $key => $field_def) {
162 if( ($field_def['type'] == 'relate' && empty($field_def['custom_type'])) ||
163 ($field_def['type'] == 'assigned_user_name' || $field_def['type'] =='link') ||
164 ($field_def['type'] == 'bool') ||
165 (in_array($field_def['name'], $this->badFields)) ) {
168 if(!isset($field_def['vname'])) {
171 // valid def found, process
172 $optionKey = strtolower("{$prefixes[$collectionKey]}{$key}");
173 $optionLabel = preg_replace('/:$/', "", translate($field_def['vname'], $beankey));
175 foreach ($collection[$collectionKey] as $value){
176 if($value['name']==$optionKey){
182 $collection[$collectionKey][] = array("name" => $optionKey, "value" => $optionLabel);
187 $json = getJSONobj();
188 $ret = "var field_defs = ";
189 $ret .= $json->encode($collection, false);
194 function get_summary_text() {
195 return "$this->name";
198 function create_export_query(&$order_by, &$where) {
199 return $this->create_new_list_query($order_by, $where);
202 function fill_in_additional_list_fields() {
203 $this->fill_in_additional_parent_fields();
206 function fill_in_additional_detail_fields() {
207 if (empty($this->body) && !empty($this->body_html))
209 global $sugar_config;
210 $this->body = strip_tags(html_entity_decode($this->body_html, ENT_COMPAT, $sugar_config['default_charset']));
212 $this->created_by_name = get_assigned_user_name($this->created_by);
213 $this->modified_by_name = get_assigned_user_name($this->modified_user_id);
214 $this->assigned_user_name = get_assigned_user_name($this->assigned_user_id);
215 $this->fill_in_additional_parent_fields();
218 function fill_in_additional_parent_fields() {
221 function get_list_view_data() {
222 global $app_list_strings, $focus, $action, $currentModule;
223 $fields = $this->get_list_view_array();
224 $fields["DATE_MODIFIED"] = substr($fields["DATE_MODIFIED"], 0 , 10);
228 //function all string that match the pattern {.} , also catches the list of found strings.
229 //the cache will get refreshed when the template bean instance changes.
230 //The found url key patterns are replaced with name value pairs provided as function parameter. $tracked_urls.
231 //$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
232 //$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.
233 //$removeme_url_template, if the url has is_optout property checked then use this template.
234 function parse_tracker_urls($template_text_array,$url_template,$tracked_urls,$removeme_url_template) {
235 global $beanFiles,$beanList, $app_list_strings,$sugar_config;
236 if (!isset($this->parsed_urls))
237 $this->parsed_urls=array();
239 $return_array = $template_text_array;
240 if(count($tracked_urls) > 0)
242 //parse the template and find all the dynamic strings that need replacement.
243 foreach ($template_text_array as $key=>$template_text) {
244 if (!empty($template_text)) {
246 if(!isset($this->parsed_urls[$key]) || $this->parsed_urls[$key]['text'] != $template_text) {
248 $template_text = urldecode($template_text);
249 $matches = $this->_preg_match_tracker_url($template_text);
250 $count = count($matches[0]);
251 $this->parsed_urls[$key]=array('matches' => $matches, 'text' => $template_text);
253 $matches=$this->parsed_urls[$key]['matches'];
254 if(!empty($matches[0])) {
255 $count=count($matches[0]);
261 //navigate thru all the matched keys and replace the keys with actual strings.
265 for ($i=($count -1); $i>=0; $i--) {
266 $url_key_name=$matches[0][$i][0];
267 if (!empty($tracked_urls[$url_key_name])) {
268 if ($tracked_urls[$url_key_name]['is_optout']==1){
269 $tracker_url = $removeme_url_template;
271 $tracker_url = sprintf($url_template,$tracked_urls[$url_key_name]['id']);
274 if(!empty($tracker_url) && !empty($template_text) && !empty($matches[0][$i][0]) && !empty($tracked_urls[$matches[0][$i][0]])){
275 $template_text=substr_replace($template_text,$tracker_url,$matches[0][$i][1], strlen($matches[0][$i][0]));
276 $template_text=str_replace($sugar_config['site_url'].'/'.$sugar_config['site_url'],$sugar_config['site_url'],$template_text);
281 $return_array[$key]=$template_text;
284 return $return_array;
289 * Method for replace "preg_match_all" in method "parse_tracker_urls"
290 * @param $text string String in which we need to search all string that match the pattern {.}
291 * @return array result of search
293 private function _preg_match_tracker_url($text)
298 for($i = 0; $i < strlen($text); $i++)
305 elseif($text[$i] == '}' && $switch === true)
308 array_push($result, array(substr($text, $ind, $i - $ind + 1), $ind));
311 return array($result);
314 function parse_email_template($template_text_array, $focus_name, $focus, &$macro_nv) {
317 global $beanFiles, $beanList, $app_list_strings;
319 // generate User instance that owns this "Contact" for contact_user_* macros
321 if(isset($focus->assigned_user_id) && !empty($focus->assigned_user_id)){
322 $user->retrieve($focus->assigned_user_id);
325 if(!isset($this->parsed_entities))
326 $this->parsed_entities=array();
328 //parse the template and find all the dynamic strings that need replacement.
329 // Bug #48111 It's strange why prefix for User module is contact_user (see self::generateFieldDefsJS method)
330 if ($beanList[$focus_name] == 'User')
332 $pattern_prefix = '$contact_user_';
336 $pattern_prefix = '$'.strtolower($beanList[$focus_name]).'_';
338 $pattern_prefix_length = strlen($pattern_prefix);
339 $pattern = '/\\'.$pattern_prefix.'[A-Za-z_0-9]*/';
341 foreach($template_text_array as $key=>$template_text) {
342 if(!isset($this->parsed_entities[$key])) {
344 $count = preg_match_all($pattern, $template_text, $matches, PREG_OFFSET_CAPTURE);
347 for($i=($count -1); $i>=0; $i--) {
348 if(!isset($matches[0][$i][2])) {
349 //find the field name in the bean.
350 $matches[0][$i][2]=substr($matches[0][$i][0],$pattern_prefix_length,strlen($matches[0][$i][0]) - $pattern_prefix_length);
352 //store the localized strings if the field is of type enum..
353 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'])) {
354 $matches[0][$i][3]=$focus->field_defs[$matches[0][$i][2]]['options'];
359 $this->parsed_entities[$key]=$matches;
361 $matches=$this->parsed_entities[$key];
362 if(!empty($matches[0])) {
363 $count=count($matches[0]);
369 for ($i=($count -1); $i>=0; $i--) {
370 $field_name=$matches[0][$i][2];
372 // cn: feel for User object attribute key and assign as found
373 if(strpos($field_name, "user_") === 0) {
374 $userFieldName = substr($field_name, 5);
375 $value = $user->$userFieldName;
376 //_pp($userFieldName."[{$value}]");
378 $value = $focus->{$field_name};
382 if(isset($matches[0][$i][3])) {
383 if(isset($app_list_strings[$matches[0][$i][3]][$value])) {
384 $value=$app_list_strings[$matches[0][$i][3]][$value];
388 //generate name value pair array of macros and corresponding values for the targed.
389 $macro_nv[$matches[0][$i][0]] =$value;
391 $template_text=substr_replace($template_text,$value,$matches[0][$i][1], strlen($matches[0][$i][0]));
394 //parse the template for tracker url strings. patter for these strings in {[a-zA-Z_0-9]+}
396 $return_array[$key]=$template_text;
399 return $return_array;
403 * Convenience method to convert raw value into appropriate type format
404 * @param string $type
405 * @param string $value
408 function _convertToType($type,$value) {
410 case 'currency' : return currency_format_number($value);
411 default: return $value;
416 * Convenience method to parse for user's values in a template
417 * @param array $repl_arr
418 * @param object $user
421 function _parseUserValues($repl_arr, &$user) {
422 foreach($user->field_defs as $field_def) {
423 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
427 if($field_def['type'] == 'enum') {
428 $translated = translate($field_def['options'], 'Users', $user->$field_def['name']);
430 if(isset($translated) && ! is_array($translated)) {
431 $repl_arr["contact_user_".$field_def['name']] = $translated;
432 } else { // unset enum field, make sure we have a match string to replace with ""
433 $repl_arr["contact_user_".$field_def['name']] = '';
436 if(isset($user->$field_def['name'])) {
437 // bug 47647 - allow for fields to translate before adding to template
438 $repl_arr["contact_user_".$field_def['name']] = self::_convertToType($field_def['type'],$user->$field_def['name']);
440 $repl_arr["contact_user_".$field_def['name']] = "";
449 function parse_template_bean($string, $bean_name, &$focus) {
450 global $current_user;
451 global $beanFiles, $beanList;
454 // cn: bug 9277 - create a replace array with empty strings to blank-out invalid vars
455 $acct = new Account();
456 $contact = new Contact();
458 $prospect = new Prospect();
460 foreach($lead->field_defs as $field_def) {
461 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
464 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
465 'contact_' . $field_def['name'] => '',
466 'contact_account_' . $field_def['name'] => '',
469 foreach($prospect->field_defs as $field_def) {
470 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
473 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
474 'contact_' . $field_def['name'] => '',
475 'contact_account_' . $field_def['name'] => '',
478 foreach($contact->field_defs as $field_def) {
479 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
482 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
483 'contact_' . $field_def['name'] => '',
484 'contact_account_' . $field_def['name'] => '',
487 foreach($acct->field_defs as $field_def) {
488 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
491 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
492 'account_' . $field_def['name'] => '',
493 'account_contact_' . $field_def['name'] => '',
496 // cn: end bug 9277 fix
499 // feel for Parent account, only for Contacts traditionally, but written for future expansion
500 if(isset($focus->account_id) && !empty($focus->account_id)) {
501 $acct->retrieve($focus->account_id);
504 if($bean_name == 'Contacts') {
505 // cn: bug 9277 - email templates not loading account/opp info for templates
506 if(!empty($acct->id)) {
507 foreach($acct->field_defs as $field_def) {
508 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
512 if($field_def['type'] == 'enum') {
513 $translated = translate($field_def['options'], 'Accounts' ,$acct->$field_def['name']);
515 if(isset($translated) && ! is_array($translated)) {
516 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
517 'account_' . $field_def['name'] => $translated,
518 'contact_account_' . $field_def['name'] => $translated,
520 } else { // unset enum field, make sure we have a match string to replace with ""
521 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
522 'account_' . $field_def['name'] => '',
523 'contact_account_' . $field_def['name'] => '',
527 // bug 47647 - allow for fields to translate before adding to template
528 $translated = self::_convertToType($field_def['type'],$acct->$field_def['name']);
529 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
530 'account_' . $field_def['name'] => $translated,
531 'contact_account_' . $field_def['name'] => $translated,
537 if(!empty($focus->assigned_user_id)) {
539 $user->retrieve($focus->assigned_user_id);
540 $repl_arr = EmailTemplate::_parseUserValues($repl_arr, $user);
542 } elseif($bean_name == 'Users') {
544 * This section of code will on do work when a blank Contact, Lead,
545 * etc. is passed in to parse the contact_* vars. At this point,
546 * $current_user will be used to fill in the blanks.
548 $repl_arr = EmailTemplate::_parseUserValues($repl_arr, $current_user);
550 // assumed we have an Account in focus
551 foreach($contact->field_defs as $field_def) {
552 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name' || $field_def['type'] == 'link') {
556 if($field_def['type'] == 'enum') {
557 $translated = translate($field_def['options'], 'Accounts' ,$contact->$field_def['name']);
559 if(isset($translated) && ! is_array($translated)) {
560 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
561 'contact_' . $field_def['name'] => $translated,
562 'contact_account_' . $field_def['name'] => $translated,
564 } else { // unset enum field, make sure we have a match string to replace with ""
565 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
566 'contact_' . $field_def['name'] => '',
567 'contact_account_' . $field_def['name'] => '',
571 if (isset($contact->$field_def['name'])) {
572 // bug 47647 - allow for fields to translate before adding to template
573 $translated = self::_convertToType($field_def['type'],$contact->$field_def['name']);
574 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
575 'contact_' . $field_def['name'] => $translated,
576 'contact_account_' . $field_def['name'] => $translated,
583 ///////////////////////////////////////////////////////////////////////
584 //// LOAD FOCUS DATA INTO REPL_ARR
585 foreach($focus->field_defs as $field_def) {
586 if(isset($focus->$field_def['name'])) {
587 if(($field_def['type'] == 'relate' && empty($field_def['custom_type'])) || $field_def['type'] == 'assigned_user_name') {
591 if($field_def['type'] == 'enum' && isset($field_def['options'])) {
592 $translated = translate($field_def['options'],$bean_name,$focus->$field_def['name']);
594 if(isset($translated) && ! is_array($translated)) {
595 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
596 strtolower($beanList[$bean_name])."_".$field_def['name'] => $translated,
598 } else { // unset enum field, make sure we have a match string to replace with ""
599 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
600 strtolower($beanList[$bean_name])."_".$field_def['name'] => '',
604 // bug 47647 - translate currencies to appropriate values
605 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
606 strtolower($beanList[$bean_name])."_".$field_def['name'] => self::_convertToType($field_def['type'],$focus->$field_def['name']),
610 if($field_def['name'] == 'full_name') {
611 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
612 strtolower($beanList[$bean_name]).'_full_name' => $focus->get_summary_text(),
615 $repl_arr = EmailTemplate::add_replacement($repl_arr, $field_def, array(
616 strtolower($beanList[$bean_name])."_".$field_def['name'] => '',
624 //20595 add nl2br() to respect the multi-lines formatting
625 if(isset($repl_arr['contact_primary_address_street'])){
626 $repl_arr['contact_primary_address_street'] = nl2br($repl_arr['contact_primary_address_street']);
628 if(isset($repl_arr['contact_alt_address_street'])){
629 $repl_arr['contact_alt_address_street'] = nl2br($repl_arr['contact_alt_address_street']);
632 foreach ($repl_arr as $name=>$value) {
633 if($value != '' && is_string($value)) {
634 $string = str_replace("\$$name", $value, $string);
636 $string = str_replace("\$$name", ' ', $string);
644 * Add replacement(s) to the collection based on field definition
647 * @param array $field_def
648 * @param array $replacement
651 protected static function add_replacement($data, $field_def, $replacement)
653 foreach ($replacement as $key => $value)
655 // @see defect #48641
656 if ('multienum' == $field_def['type']) {
657 $value = implode(', ', unencodeMultienum($value));
659 $data[$key] = $value;
664 function parse_template($string, &$bean_arr) {
665 global $beanFiles, $beanList;
667 foreach($bean_arr as $bean_name => $bean_id) {
668 require_once($beanFiles[$beanList[$bean_name]]);
670 $focus = new $beanList[$bean_name];
671 $result = $focus->retrieve($bean_id);
673 if($bean_name == 'Leads' || $bean_name == 'Prospects') {
674 $bean_name = 'Contacts';
677 if(isset($this) && isset($this->module_dir) && $this->module_dir == 'EmailTemplates') {
678 $string = $this->parse_template_bean($string, $bean_name, $focus);
680 $string = EmailTemplate::parse_template_bean($string, $bean_name, $focus);
686 function bean_implements($interface) {
688 case 'ACL':return true;
693 static function getTypeOptionsForSearch(){
694 $template = new EmailTemplate();
695 $optionKey = $template->field_defs['type']['options'];
696 $options = $GLOBALS['app_list_strings'][$optionKey];
697 if( ! is_admin($GLOBALS['current_user']) && isset($options['workflow']))
698 unset($options['workflow']);
703 function is_used_by_email_marketing() {
704 $query = "select id from email_marketing where template_id='$this->id' and deleted=0";
705 $result = $this->db->query($query);
706 if($this->db->fetchByAssoc($result)) {