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: Defines the English language pack for the base application.
41 * Portions created by SugarCRM are Copyright (C) SugarCRM, Inc.
42 * All Rights Reserved.
43 * Contributor(s): ______________________________________..
44 ********************************************************************************/
47 * This is the base class that all other SugarMerge objects extend
52 * The variable name that is used with the file for example in editviewdefs and detailviewdefs it is $viewdefs
56 protected $varName = 'viewdefs';
58 * Enter the name of the parameter used in the $varName for example in editviewdefs and detailviewdefs it is 'EditView' and 'DetailView' respectively - $viewdefs['EditView']
62 protected $viewDefs = 'EditView';
64 * this will store the meta data for the original file
68 protected $originalData = array();
70 * this will store the meta data for the new file
74 protected $newData = array();
76 * this will store the meta data for the custom file
80 protected $customData = array();
82 * this will store an associative array contianing all the fields that are used in the original meta data file
86 protected $originalFields = array();
88 * this will store an associative array contianing all the fields that are used in the new meta data file
92 protected $newFields = array();
94 * this will store an associative array contianing all the fields that are used in the custom meta data file
98 protected $customFields = array();
100 * this will store an associative array contianing all the merged fields
104 protected $mergedFields = array();
106 * the name of the module to be merged
110 protected $module = 'module';
112 * the max number of columns for this view
116 protected $maxCols = 2;
118 * If we should use the best match algorithim
122 protected $bestMatch = true;
124 * The default panel we place the fields in if we aren't using the best match algorithim
128 protected $defaultPanel = 'default';
130 * The name of the panels section in the meta data
134 protected $panelName = 'panels';
136 * The name of the templateMeta data secion in the meta data
138 protected $templateMetaName = 'templateMeta';
140 * The file pointer to log to if set to NULL it will use the GLOBALS['log'] if available and log to debug
144 protected $fp = NULL;
148 * Determines if getFields should analyze panels to determine if it is a MultiPanel
152 protected $scanForMultiPanel = true;
155 * If true then it works as though it's a multipanel
159 protected $isMultiPanel = true;
163 * The ids of the panels found in custom metadata fuke
166 protected $customPanelIds = array();
170 * The ids of the panels found in original metadata fuke
173 protected $originalPanelIds = array();
177 * The ids of the panels found in original metadata fuke
180 protected $newPanelIds = array();
184 * Special case conversion
187 protected $fieldConversionMapping = array(
188 'Campaigns' => array('created_by_name'=>'date_entered', 'modified_by_name'=>'date_modified'),
189 'Cases' => array('created_by_name'=>'date_entered', 'modified_by_name'=>'date_modified'),
190 'Contracts' => array('created_by_name'=>'date_entered', 'modified_by_name'=>'date_modified'),
191 'Leads' => array('created_by'=>'date_entered'),
192 'Meetings' => array('created_by_name'=>'date_entered', 'modified_by_name'=>'date_modified'),
193 'ProspectLists' => array('created_by_name'=>'date_entered', 'modified_by_name'=>'date_modified'),
194 'Prospects' => array('created_by_name'=>'date_entered', 'modified_by_name'=>'date_modified'),
200 public $sugarMerge = null;
203 * Clears out the values of the arrays so that the same object can be utilized
206 protected function clear(){
207 unset($this->newData);
208 $this->newData = array();
209 unset($this->customData);
210 $this->customData = array();
211 unset($this->originalData);
212 $this->originalData = array();
213 unset($this->newFields);
214 $this->newFields = array();
215 unset($this->customFields);
216 $this->customFields = array();
217 unset($this->originalFields);
218 $this->originalFields = array();
219 unset($this->mergedFields);
220 $this->mergedFields = array();
221 unset($this->mergeData);
222 $this->mergeData = array();
223 $this->defaultPanel = 'default';
227 * Allows the user to choose to use the best match algorithim or not
231 public function setBestMatch($on=true){
232 $this->bestMatch = $on;
237 * Allows users to set the name to use as the default panel in the meta data
239 * @param STRING $name - name of the default panel
241 public function setDefaultPanel($name = 'default'){
242 $this->defaultPanel = $name;
246 * Allows the user to set a filepointer that is already open to log to
248 * @param FILEPOINTER $fp
250 public function setLogFilePointer($fp){
255 * opens the file with the 'a' parameter and use it to log messages to
257 * @param STRING $file - path to file we wish to log to
259 public function setLogFile($file){
260 $this->fp = fopen($file, 'a');
267 * returns true if $val1 and $val2 match otherwise it returns false
269 * @param MULTI $val1 - a value to compare to val2
270 * @param MULTI $val2 - a value to compare to val1
271 * @return BOOLEAN - if $val1 and $val2 match
273 protected function areMatchingValues($val1, $val2){
274 if(!is_array($val1)){
275 //if val2 is an array and val1 isn't then it isn't a match
279 //otherwise both are not arrays so we can return a comparison between them
280 return $val1 == $val2;
282 //if val1 is an array and val2 isn't then it isn't a match
283 if(!is_array($val2)){
287 foreach($val1 as $k=>$v){
288 if(!isset($val2[$k]))return false;
289 if(!$this->areMatchingValues($val1[$k], $val2[$k])){
295 //this implies that there are still values left so the two must not match since we unset any matching values
303 * Recursiveley merges two arrays
305 * @param ARRAY $gimp - if keys match this arrays values are overriden
306 * @param ARRAY $dom - if keys match this arrays values will override the others
307 * @return ARRAY $merged - the merges array
309 function arrayMerge($gimp, $dom) {
310 if(is_array($gimp) && is_array($dom)) {
311 foreach($dom as $domKey => $domVal) {
312 if(isset($gimp[$domKey])) {
313 if(is_array($domVal)) {
314 $gimp[$domKey] = $this->arrayMerge($gimp[$domKey], $dom[$domKey]);
316 $gimp[$domKey] = $domVal;
319 $gimp[$domKey] = $domVal;
327 * Merges the meta data of a single field
329 * @param ARRAY $orig - the original meta-data for this field
330 * @param ARRAY $new - the new meta-data for this field
331 * @param ARRAY $custom - the custom meta-data for this field
332 * @return ARRAY $merged - the merged meta-data
334 protected function mergeField($orig, $new, $custom){
335 $orig_custom = $this->areMatchingValues($orig, $custom);
336 $new_custom = $this->areMatchingValues($new, $custom);
337 // if both are true then there is nothing to merge since all three fields match
338 if(!($orig_custom && $new_custom)){
339 $this->log('merging field');
340 $this->log('original meta-data');
342 $this->log('new meta-data');
344 $this->log('custom meta-data');
346 $this->log('merged meta-data');
351 //if orignal and custom match always take the new value or if new and custom match
352 if($orig_custom || $new_custom){
356 //if original and new match always take the custom
357 if($this->areMatchingValues($orig, $new)){
362 if(is_array($custom)) {
363 //if both new and custom are arrays then at this point new != custom and orig != custom and orig != new so let's merge the custom and the new and return that
365 $new = $this->arrayMerge($custom, $new);
369 //otherwise we know that new is not an array and custom has been 'customized' so let's keep those customizations.
374 //default to returning the New version of the field
380 * Merges the fields together and stores them in $this->mergedFields
383 protected function mergeFields() {
384 foreach($this->customFields as $field=>$data) {
385 //if we have this field in both the new fields and the original fields - it has existed since the last install/upgrade
386 if(isset($this->newFields[$field]) && isset($this->originalFields[$field])){
387 //if both the custom field and the original match then we take the location of the custom field since it hasn't moved
388 $loc = $this->customFields[$field]['loc'];
389 $loc['source'] = 'custom';
393 //Address fields present a special problem...
394 if(preg_match('/(alt_|primary_|billing_|shipping_)address_street/i', $field, $matches)) {
395 $prefix = $matches[1];
396 $city = $prefix . 'address_city';
397 $postal_code = $prefix . 'address_postalcode';
398 $state = $prefix . 'address_state';
399 $country = $prefix . 'address_country';
401 if(isset($this->customFields[$city]) ||
402 isset($this->customFields[$postal_code]) ||
403 isset($this->customFields[$state]) ||
404 isset($this->customFields[$country])) {
406 $this->mergedFields[$field] = array(
407 'data'=>$this->customFields[$field]['data'],
413 //but we still merge the meta data of the three
414 $this->mergedFields[$field] = array(
415 'data'=>$this->mergeField($this->originalFields[$field]['data'], $this->newFields[$field]['data'], $this->customFields[$field]['data']),
418 //if it's not set in the new fields then it was a custom field or an original field so we take the custom fields data and set the location source to custom
419 } else if(!isset($this->newFields[$field])){
420 $this->mergedFields[$field] = $data;
421 $this->mergedFields[$field]['loc']['source'] = 'custom';
423 //otherwise the field is in both new and custom but not in the orignal so we merge the new and custom data together and take the location from the custom
424 $this->mergedFields[$field] = array(
425 'data'=>$this->mergeField('', $this->newFields[$field]['data'], $this->customFields[$field]['data']),
426 'loc'=>$this->customFields[$field]['loc']);
428 $this->mergedFields[$field]['loc']['source'] = 'custom';
429 //echo var_export($this->mergedFields[$field], true);
432 //then we clear out the field from
433 unset($this->originalFields[$field]);
434 unset($this->customFields[$field]);
435 unset($this->newFields[$field]);
440 * These are fields that were removed by the customer
442 foreach($this->originalFields as $field=>$data){
443 unset($this->originalFields[$field]);
444 unset($this->newFields[$field]);
448 * These are fields that were added by sugar
450 $new_field_panel = $this->defaultPanel;
451 foreach($this->customPanelIds as $custom_panel_ids=>$panels) {
452 $new_field_panel = $custom_panel_ids;
455 foreach($this->newFields as $field=>$data){
456 $data['loc']['source']= 'new';
457 $data['loc']['panel'] = $new_field_panel;
458 $this->mergedFields[$field] = array(
459 'data'=>$data['data'],
460 'loc'=>$data['loc']);
461 unset($this->newFields[$field]);
466 * Walks through the merged fields and places them in the appropriate place based on their location parameter as well as the choosen algorithim
468 * @return ARRAY $panels - the new panels section for the merged file
470 protected function buildPanels(){
473 $panel_keys = array_keys($this->customPanelIds);
474 $this->defaultPanel = end($panel_keys);
476 foreach($this->mergedFields as $field_id=>$field){
477 //If this field is in a panel not defined in the custom layout, set it to default panel
478 if(!isset($this->customPanelIds[$field['loc']['panel']])) {
479 $field['loc']['panel'] = $this->defaultPanel;
482 if($field['loc']['source'] == 'new') {
483 if($this->bestMatch){
484 //for best match as long as the column is filled let's keep walking down till we can fill it
485 $row = end(array_keys($this->customData[$this->module][$this->viewDefs][$this->panelName][$field['loc']['panel']]));
487 while(!empty($panels[$field['loc']['panel']][$row][$col])){
494 //row should be at a point that there is no field in this location
495 $panels[$field['loc']['panel']][$row][$col] = $field['data'];
497 //so for not best match we place it in the default panel at the first available column for the row
499 while(!empty($panels[$this->defaultPanel][$row][$field['loc']['col']])){
502 $panels[$field['loc']['panel']][$row][$field['loc']['col']] = $field['data'];
505 $panels[$field['loc']['panel']][$field['loc']['row']][$field['loc']['col']] = $field['data'];
511 foreach($panels as $k=>$panel){
512 foreach($panel as $r=>$row){
513 ksort($panels[$k][$r]);
522 * Merge the templateMeta entry for the view defs. Also assume that any changes made in the custom files should
523 * have precedence since they must be changed manually, even over new files that may be provided in the upgarde
527 protected function mergeTemplateMeta()
529 //this is to handle the situation in Calls/Meetings where we updated the templateMeta and will fail if we don't update this.
530 //long term we should not do this and should provide a way for calls/meetings to update themselves.
531 if( isset($this->customData[$this->module][$this->viewDefs][$this->templateMetaName]) && strcmp(strtolower($this->module), 'calls') != 0 && strcmp(strtolower($this->module), 'meetings') != 0 )
533 $this->newData[$this->module][$this->viewDefs][$this->templateMetaName] = $this->customData[$this->module][$this->viewDefs][$this->templateMetaName];
539 * Sets the panel section for the meta-data after it has been merged
542 protected function setPanels(){
543 $this->newData[$this->module][$this->viewDefs][$this->panelName] = $this->buildPanels();
545 if(!$this->isMultiPanel) {
546 $this->newData[$this->module][$this->viewDefs][$this->panelName] = $this->newData[$this->module][$this->viewDefs][$this->panelName][$this->defaultPanel];
552 * Parses out the fields for each files meta data and then calls on mergeFields and setPanels
555 protected function mergeMetaData(){
556 $this->originalFields = $this->getFields($this->originalData[$this->module][$this->viewDefs][$this->panelName]);
557 $this->originalPanelIds = $this->getPanelIds($this->originalData[$this->module][$this->viewDefs][$this->panelName]);
558 $this->customFields = $this->getFields($this->customData[$this->module][$this->viewDefs][$this->panelName]);
559 $this->customPanelIds = $this->getPanelIds($this->customData[$this->module][$this->viewDefs][$this->panelName]);
560 $this->newFields = $this->getFields($this->newData[$this->module][$this->viewDefs][$this->panelName]);
561 //echo var_export($this->newFields, true);
562 $this->newPanelIds = $this->getPanelIds($this->newData[$this->module][$this->viewDefs][$this->panelName]);
563 $this->mergeFields();
564 $this->mergeTemplateMeta();
568 * This takes in a list of panels and returns an associative array of field names to the meta-data of the field as well as the locations of that field
570 * @param ARRAY $panels - this is the 'panel' section of the meta-data
571 * @return ARRAY $fields - an associate array of fields and their meta-data as well as their location
573 protected function getFields(&$panels){
577 $setDefaultPanel = false;
579 if(count($panels) == 1) {
580 $arrayKeys = array_keys($panels);
581 if(!empty($arrayKeys[0])) {
582 $this->defaultPanel = $arrayKeys[0];
583 $panels = $panels[$arrayKeys[0]];
585 $panels = $panels[''];
587 $setDefaultPanel = true;
590 if($this->scanForMultiPanel){
591 require_once('include/SugarFields/Parsers/MetaParser.php');
592 if($setDefaultPanel || !MetaParser::hasMultiplePanels($panels)) {
593 $panels = array($this->defaultPanel=>$panels);
594 $this->isMultiPanel = false;
598 //echo "---------------------------------------------------------\n";
599 //echo var_export($panels, true);
601 foreach($panels as $panel_id=>$panel){
602 foreach($panel as $row_id=>$rows){
603 foreach($rows as $col_id=>$col){
605 $field_name = 'BLANK_' . $blanks;
608 $field_name = is_array($col) && isset($col['name']) ? $col['name'] : $col;
610 if(!empty($col['name'])) {
611 $field_name = $col['name'];
618 if(is_string($field_name)) {
619 // We need to replace all instances of the fake uploadfile and filename field that has custom code with the real filename field
620 if(!empty($col['customCode']))
622 if($field_name == 'uploadfile')
624 $replaceField = false;
625 if ( !empty($col['customCode']) ) {
626 $replaceField = true;
627 unset($col['customCode']);
630 if( !empty($col['displayParams']) && !empty($col['displayParams']['link']) ) {
631 $replaceField = true;
634 if ( $replaceField ) {
635 $field_name = 'filename';
636 $col['name'] = 'filename';
638 } else if ($field_name == 'filename') {
643 $fields[$field_name] = array('data'=>$col, 'loc'=>array('panel'=>"{$panel_id}", 'row'=>"{$row_id}", 'col'=>"{$col_id}"));
649 //echo "---------------------------------------------------------\n";
650 //echo var_export($fields, true);
660 protected function getPanelIds($panels){
662 $panel_ids = array();
663 $setDefaultPanel = false;
665 if(count($panels) == 1) {
666 $arrayKeys = array_keys($panels);
667 if(!empty($arrayKeys[0])) {
668 $this->defaulPanel = $arrayKeys[0];
669 $panels = $panels[$arrayKeys[0]];
671 $panels = $panels[''];
673 $setDefaultPanel = true;
676 if($this->scanForMultiPanel){
677 require_once('include/SugarFields/Parsers/MetaParser.php');
678 if($setDefaultPanel || !MetaParser::hasMultiplePanels($panels)) {
679 $panels = array($this->defaultPanel=>$panels);
680 $this->isMultiPanel = false;
684 foreach($panels as $panel_id=>$panel){
685 $panel_ids[$panel_id] = $panel_id;
692 * Loads the meta data of the original, new, and custom file into the variables originalData, newData, and customData respectively
694 * @param STRING $module - name of the module's files that are to be merged
695 * @param STRING $original_file - path to the file that originally shipped with sugar
696 * @param STRING $new_file - path to the new file that is shipping with the patch
697 * @param STRING $custom_file - path to the custom file
699 protected function loadData($module, $original_file, $new_file, $custom_file){
700 $this->module = $module;
701 $varnmame = $this->varName;
702 require($original_file);
703 $this->originalData = $$varnmame;
705 $this->newData = $$varnmame;
706 if(file_exists($custom_file)){
707 require($custom_file);
708 $this->customData = $$varnmame;
710 $this->customData = $this->originalData;
715 * This will save the merged data to a file
717 * @param STRING $to - path of the file to save it to
718 * @return BOOLEAN - success or failure of the save
720 public function save($to){
721 return write_array_to_file("viewdefs['$this->module']['$this->viewDefs']", $this->newData[$this->module][$this->viewDefs], $to);
725 * This will return the meta data of the merged file
727 * @return ARRAY - the meta data of the merged file
729 public function getData(){
730 return $this->newData;
734 * public function that will merge meta data from an original sugar file that shipped with the product, a customized file, and a new file shipped with an upgrade
736 * @param STRING $module - name of the module's files that are to be merged
737 * @param STRING $original_file - path to the file that originally shipped with sugar
738 * @param STRING $new_file - path to the new file that is shipping with the patch
739 * @param STRING $custom_file - path to the custom file
740 * @param BOOLEAN $save - boolean on if it should save the results to the custom file or not
741 * @return BOOLEAN - if the merged file was saved if false is passed in for the save parameter it always returns true
743 public function merge($module, $original_file, $new_file, $custom_file=false, $save=true){
745 $this->log("\n\n". 'Starting a merge in ' . get_class($this));
746 $this->log('merging the following files');
747 $this->log('original file:' . $original_file);
748 $this->log('new file:' . $new_file);
749 $this->log('custom file:' . $custom_file);
750 if(empty($custom_file) && $save){
753 $this->loadData($module, $original_file, $new_file, $custom_file);
754 $this->mergeMetaData();
755 if($save && !empty($this->newData) && !empty($custom_file)){
757 copy($custom_file, $custom_file . '.suback.php');
758 return $this->save($custom_file);
761 if(!$save)return true;
765 * Logs the given message if the message is not a string it will export it first. If $this->fp is NULL then it will try to log to the $GLOBALS['log'] if it is available
767 * @param MULTI $message
769 protected function log($message){
770 if(!is_string($message)){
771 $message = var_export($message, true);
773 if(!empty($this->fp)){
774 fwrite($this->fp, $message. "\n");
776 if(!empty($GLOBALS['log'])){
777 $GLOBALS['log']->debug($message . "\n");