]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - ModuleInstall/ModuleScanner.php
Release 6.5.14
[Github/sugarcrm.git] / ModuleInstall / ModuleScanner.php
1 <?php
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.
6  * 
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.
13  * 
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
17  * details.
18  * 
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
22  * 02110-1301 USA.
23  * 
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.
26  * 
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.
30  * 
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  ********************************************************************************/
37
38 class ModuleScanner{
39         private $manifestMap = array(
40                         'pre_execute'=>'pre_execute',
41                         'install_mkdirs'=>'mkdir',
42                         'install_copy'=>'copy',
43                         'install_images'=>'image_dir',
44                         'install_menus'=>'menu',
45                         'install_userpage'=>'user_page',
46                         'install_dashlets'=>'dashlets',
47                         'install_administration'=>'administration',
48                         'install_connectors'=>'connectors',
49                         'install_vardefs'=>'vardefs',
50                         'install_layoutdefs'=>'layoutdefs',
51                         'install_layoutfields'=>'layoutfields',
52                         'install_relationships'=>'relationships',
53                         'install_languages'=>'language',
54             'install_logichooks'=>'logic_hooks',
55                         'post_execute'=>'post_execute',
56
57         );
58
59         /**
60          * config settings
61          * @var array
62          */
63         private $config = array();
64         private $config_hash;
65
66         private $blackListExempt = array();
67         private $classBlackListExempt = array();
68
69         private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml');
70         private $classBlackList = array(
71         // Class names specified here must be in lowercase as the implementation
72         // of the tokenizer converts all tokens to lowercase.
73         'reflection',
74         'reflectionclass',
75         'reflectionzendextension',
76         'reflectionextension',
77         'reflectionfunction',
78         'reflectionfunctionabstract',
79         'reflectionmethod',
80         'reflectionobject',
81         'reflectionparameter',
82         'reflectionproperty',
83         'reflector',
84         'reflectionexception',
85         'lua',
86     );
87         private $blackList = array(
88     'popen',
89     'proc_open',
90     'escapeshellarg',
91     'escapeshellcmd',
92     'proc_close',
93     'proc_get_status',
94     'proc_nice',
95         'passthru',
96     'clearstatcache',
97     'disk_free_space',
98     'disk_total_space',
99     'diskfreespace',
100     'fclose',
101     'feof',
102     'fflush',
103     'fgetc',
104     'fgetcsv',
105     'fgets',
106     'fgetss',
107     'file_exists',
108     'file_get_contents',
109     'filesize',
110     'filetype',
111     'flock',
112     'fnmatch',
113     'fpassthru',
114     'fputcsv',
115     'fputs',
116     'fread',
117     'fscanf',
118     'fseek',
119     'fstat',
120     'ftell',
121     'ftruncate',
122     'fwrite',
123     'glob',
124     'is_dir',
125     'is_file',
126     'is_link',
127     'is_readable',
128     'is_uploaded_file',
129     'parse_ini_string',
130     'pathinfo',
131     'pclose',
132     'readfile',
133     'readlink',
134     'realpath_cache_get',
135     'realpath_cache_size',
136     'realpath',
137     'rewind',
138     'set_file_buffer',
139     'tmpfile',
140     'umask',
141     'ini_set',
142         'eval',
143         'exec',
144         'system',
145         'shell_exec',
146         'passthru',
147         'chgrp',
148         'chmod',
149         'chwown',
150         'file_put_contents',
151         'file',
152         'fileatime',
153         'filectime',
154         'filegroup',
155         'fileinode',
156         'filemtime',
157         'fileowner',
158         'fileperms',
159         'fopen',
160         'is_executable',
161         'is_writable',
162         'is_writeable',
163         'lchgrp',
164         'lchown',
165         'linkinfo',
166         'lstat',
167         'mkdir',
168     'mkdir_recursive',
169         'parse_ini_file',
170         'rmdir',
171     'rmdir_recursive',
172         'stat',
173         'tempnam',
174         'touch',
175         'unlink',
176         'getimagesize',
177         'call_user_func',
178         'call_user_func_array',
179         'create_function',
180
181
182         //mutliple files per function call
183         'copy',
184     'copy_recursive',
185         'link',
186         'rename',
187         'symlink',
188         'move_uploaded_file',
189         'chdir',
190         'chroot',
191         'create_cache_directory',
192         'mk_temp_dir',
193         'write_array_to_file',
194         'write_encoded_file',
195         'create_custom_directory',
196         'sugar_rename',
197         'sugar_chown',
198         'sugar_fopen',
199         'sugar_mkdir',
200         'sugar_file_put_contents',
201         'sugar_chgrp',
202         'sugar_chmod',
203         'sugar_touch',
204
205         // Functions that have callbacks can circumvent our security measures.
206         // List retrieved through PHP's XML documentation, and running the
207         // following script in the reference directory:
208
209         // grep -R callable . | grep -v \.svn | grep methodparam | cut -d: -f1 | sort -u | cut -d"." -f2 | sed 's/\-/\_/g' | cut -d"/" -f4
210
211         // AMQPQueue
212         'consume',
213
214         // PHP internal - arrays
215         'array_diff_uassoc',
216         'array_diff_ukey',
217         'array_filter',
218         'array_intersect_uassoc',
219         'array_intersect_ukey',
220         'array_map',
221         'array_reduce',
222         'array_udiff_assoc',
223         'array_udiff_uassoc',
224         'array_udiff',
225         'array_uintersect_assoc',
226         'array_uintersect_uassoc',
227         'array_uintersect',
228         'array_walk_recursive',
229         'array_walk',
230         'uasort',
231         'uksort',
232         'usort',
233
234         // EIO functions that accept callbacks.
235         'eio_busy',
236         'eio_chmod',
237         'eio_chown',
238         'eio_close',
239         'eio_custom',
240         'eio_dup2',
241         'eio_fallocate',
242         'eio_fchmod',
243         'eio_fchown',
244         'eio_fdatasync',
245         'eio_fstat',
246         'eio_fstatvfs',
247         'eio_fsync',
248         'eio_ftruncate',
249         'eio_futime',
250         'eio_grp',
251         'eio_link',
252         'eio_lstat',
253         'eio_mkdir',
254         'eio_mknod',
255         'eio_nop',
256         'eio_open',
257         'eio_read',
258         'eio_readahead',
259         'eio_readdir',
260         'eio_readlink',
261         'eio_realpath',
262         'eio_rename',
263         'eio_rmdir',
264         'eio_sendfile',
265         'eio_stat',
266         'eio_statvfs',
267         'eio_symlink',
268         'eio_sync_file_range',
269         'eio_sync',
270         'eio_syncfs',
271         'eio_truncate',
272         'eio_unlink',
273         'eio_utime',
274         'eio_write',
275
276         // PHP internal - error functions
277         'set_error_handler',
278         'set_exception_handler',
279
280         // Forms Data Format functions
281         'fdf_enum_values',
282
283         // PHP internal - function handling
284         'call_user_func_array',
285         'call_user_func',
286         'forward_static_call_array',
287         'forward_static_call',
288         'register_shutdown_function',
289         'register_tick_function',
290
291         // Gearman
292         'setclientcallback',
293         'setcompletecallback',
294         'setdatacallback',
295         'setexceptioncallback',
296         'setfailcallback',
297         'setstatuscallback',
298         'setwarningcallback',
299         'setworkloadcallback',
300         'addfunction',
301
302         // Firebird/InterBase
303         'ibase_set_event_handler',
304
305         // LDAP
306         'ldap_set_rebind_proc',
307
308         // LibXML
309         'libxml_set_external_entity_loader',
310
311         // Mailparse functions
312         'mailparse_msg_extract_part_file',
313         'mailparse_msg_extract_part',
314         'mailparse_msg_extract_whole_part_file',
315
316         // Memcache(d) functions
317         'addserver',
318         'setserverparams',
319         'get',
320         'getbykey',
321         'getdelayed',
322         'getdelayedbykey',
323
324         // MySQLi
325         'set_local_infile_handler',
326
327         // PHP internal - network functions
328         'header_register_callback',
329
330         // Newt
331         'newt_entry_set_filter',
332         'newt_set_suspend_callback',
333
334         // OAuth
335         'consumerhandler',
336         'timestampnoncehandler',
337         'tokenhandler',
338
339         // PHP internal - output control
340         'ob_start',
341
342         // PHP internal - PCNTL
343         'pcntl_signal',
344
345         // PHP internal - PCRE
346         'preg_replace_callback',
347
348         // SQLite
349         'sqlitecreateaggregate',
350         'sqlitecreatefunction',
351         'sqlite_create_aggregate',
352         'sqlite_create_function',
353
354         // RarArchive
355         'open',
356
357         // Readline
358         'readline_callback_handler_install',
359         'readline_completion_function',
360
361         // PHP internal - session handling
362         'session_set_save_handler',
363
364         // PHP internal - SPL
365         'construct',
366         'iterator_apply',
367         'spl_autoload_register',
368
369         // Sybase
370         'sybase_set_message_handler',
371
372         // PHP internal - variable handling
373         'is_callable',
374
375         // XML Parser
376         'xml_set_character_data_handler',
377         'xml_set_default_handler',
378         'xml_set_element_handler',
379         'xml_set_end_namespace_decl_handler',
380         'xml_set_external_entity_ref_handler',
381         'xml_set_notation_decl_handler',
382         'xml_set_processing_instruction_handler',
383         'xml_set_start_namespace_decl_handler',
384         'xml_set_unparsed_entity_decl_handler',
385 );
386     private $methodsBlackList = array('setlevel');
387
388         public function printToWiki(){
389                 echo "'''Default Extensions'''<br>";
390                 foreach($this->validExt as $b){
391                         echo '#' . $b . '<br>';
392
393                 }
394                 echo "'''Default Black Listed Functions'''<br>";
395                 foreach($this->blackList as $b){
396                         echo '#' . $b . '<br>';
397
398                 }
399
400         }
401
402     public function __construct()
403     {
404         $params = array(
405             'blackListExempt'      => 'MODULE_INSTALLER_PACKAGE_SCAN_BLACK_LIST_EXEMPT',
406             'blackList'            => 'MODULE_INSTALLER_PACKAGE_SCAN_BLACK_LIST',
407             'classBlackListExempt' => 'MODULE_INSTALLER_PACKAGE_SCAN_CLASS_BLACK_LIST_EXEMPT',
408             'classBlackList'       => 'MODULE_INSTALLER_PACKAGE_SCAN_CLASS_BLACK_LIST',
409             'validExt'             => 'MODULE_INSTALLER_PACKAGE_SCAN_VALID_EXT',
410         );
411
412         $disableConfigOverride = defined('MODULE_INSTALLER_DISABLE_CONFIG_OVERRIDE')
413             && MODULE_INSTALLER_DISABLE_CONFIG_OVERRIDE;
414
415         $disableDefineOverride = defined('MODULE_INSTALLER_DISABLE_DEFINE_OVERRIDE')
416             && MODULE_INSTALLER_DISABLE_DEFINE_OVERRIDE;
417
418         if (!$disableConfigOverride && !empty($GLOBALS['sugar_config']['moduleInstaller'])) {
419             $this->config = $GLOBALS['sugar_config']['moduleInstaller'];
420         }
421
422         foreach ($params as $param => $constName) {
423
424             if (!$disableConfigOverride && isset($this->config[$param]) && is_array($this->config[$param])) {
425                 $this->{$param} = array_merge($this->{$param}, $this->config[$param]);
426             }
427
428             if (!$disableDefineOverride && defined($constName)) {
429                 $value = constant($constName);
430                 $value = explode(',', $value);
431                 $value = array_map('trim', $value);
432                 $value = array_filter($value, 'strlen');
433                 $this->{$param} = array_merge($this->{$param}, $value);
434             }
435         }
436         }
437
438         private $issues = array();
439         private $pathToModule = '';
440
441         /**
442          *returns a list of issues
443          */
444         public function getIssues(){
445                 return $this->issues;
446         }
447
448         /**
449          *returns true or false if any issues were found
450          */
451         public function hasIssues(){
452                 return !empty($this->issues);
453         }
454
455         /**
456          *Ensures that a file has a valid extension
457          */
458         private function isValidExtension($file){
459                 $file = strtolower($file);
460
461                 $extPos = strrpos($file, '.');
462                 //make sure they don't override the files.md5
463                 if($extPos === false || $file == 'files.md5')return false;
464                 $ext = substr($file, $extPos + 1);
465                 return in_array($ext, $this->validExt);
466
467         }
468
469         /**
470          *Scans a directory and calls on scan file for each file
471          **/
472         public function scanDir($path){
473                 static $startPath = '';
474                 if(empty($startPath))$startPath = $path;
475                 if(!is_dir($path))return false;
476                 $d = dir($path);
477                 while($e = $d->read()){
478                 $next = $path . '/' . $e;
479                 if(is_dir($next)){
480                         if(substr($e, 0, 1) == '.')continue;
481                         $this->scanDir($next);
482                 }else{
483                         $issues = $this->scanFile($next);
484
485
486                 }
487                 }
488         return true;
489         }
490
491         /**
492          * Check if the file contents looks like PHP
493          * @param string $contents File contents
494          * @return boolean
495          */
496         public function isPHPFile($contents)
497         {
498             if(stripos($contents, '<?php') !== false) return true;
499             for($tag=0;($tag = stripos($contents, '<?', $tag)) !== false;$tag++) {
500             if(strncasecmp(substr($contents, $tag, 13), '<?xml version', 13) == 0) {
501                 // <?xml version is OK, skip it
502                 $tag++;
503                 continue;
504             }
505             // found <?, it's PHP
506             return true;
507             }
508             return false;
509         }
510
511         /**
512          * Given a file it will open it's contents and check if it is a PHP file (not safe to just rely on extensions) if it finds <?php tags it will use the tokenizer to scan the file
513          * $var()  and ` are always prevented then whatever is in the blacklist.
514          * It will also ensure that all files are of valid extension types
515          *
516          */
517         public function scanFile($file){
518                 $issues = array();
519                 if(!$this->isValidExtension($file)){
520                         $issues[] = translate('ML_INVALID_EXT');
521                         $this->issues['file'][$file] = $issues;
522                         return $issues;
523                 }
524                 $contents = file_get_contents($file);
525                 if(!$this->isPHPFile($contents)) return $issues;
526                 $tokens = @token_get_all($contents);
527                 $checkFunction = false;
528                 $possibleIssue = '';
529                 $lastToken = false;
530                 foreach($tokens as $index=>$token){
531                         if(is_string($token[0])){
532                                 switch($token[0]){
533                                         case '`':
534                                                 $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
535                                         case '(':
536                                                 if($checkFunction)$issues[] = $possibleIssue;
537                                                 break;
538                                 }
539                                 $checkFunction = false;
540                                 $possibleIssue = '';
541                         }else{
542                                 $token['_msi'] = token_name($token[0]);
543                                 switch($token[0]){
544                                         case T_WHITESPACE: continue;
545                                         case T_EVAL:
546                                                 if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
547                                                 $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
548                                                 break;
549                                         case T_STRING:
550                                                 $token[1] = strtolower($token[1]);
551                                                 if($lastToken !== false && $lastToken[0] == T_NEW) {
552                             if(!in_array($token[1], $this->classBlackList))break;
553                             if(in_array($token[1], $this->classBlackListExempt))break;
554                         } elseif ($token[0] == T_DOUBLE_COLON) {
555                             if(!in_array($lastToken[1], $this->classBlackList))break;
556                             if(in_array($lastToken[1], $this->classBlackListExempt))break;
557                         } else {
558                             //if nothing else fit, lets check the last token to see if this is a possible method call
559                             if ($lastToken !== false &&
560                             ($lastToken[0] == T_OBJECT_OPERATOR ||  $lastToken[0] == T_DOUBLE_COLON))
561                             {
562                                 //this is a method call, check the black list
563                                 if(in_array($token[1], $this->methodsBlackList)){
564                                     $issues[]= translate('ML_INVALID_METHOD') . ' ' .$token[1].  '()';
565                                 }
566                                 break;
567                             }
568
569
570                             if(!in_array($token[1], $this->blackList))break;
571                             if(in_array($token[1], $this->blackListExempt))break;
572
573                         }
574                                         case T_VARIABLE:
575                                                 $checkFunction = true;
576                                                 $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' .  $token[1] . '()';
577                                                 break;
578
579                                         default:
580                                                 $checkFunction = false;
581                                                 $possibleIssue = '';
582
583                                 }
584                                 if ($token[0] != T_WHITESPACE)
585                                 {
586                                         $lastToken = $token;
587                                 }
588                         }
589
590                 }
591                 if(!empty($issues)){
592                         $this->issues['file'][$file] = $issues;
593                 }
594
595                 return $issues;
596         }
597
598
599         /*
600          * checks files.md5 file to see if the file is from sugar
601          * ONLY WORKS ON FILES
602          */
603         public function sugarFileExists($path){
604                 static $md5 = array();
605                 if(empty($md5) && file_exists('files.md5'))
606                 {
607                         include('files.md5');
608                         $md5 = $md5_string;
609                 }
610                 if(isset($md5['./' . $path]))return true;
611
612
613         }
614
615
616         /**
617          *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
618          *if $GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy'] is set to false or not set it will call on scanCopy to ensure that it is not overriding files
619          */
620         public function scanManifest($manifestPath){
621                 $issues = array();
622                 if(!file_exists($manifestPath)){
623                         $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
624                         return $issues;
625                 }
626                 $fileIssues = $this->scanFile($manifestPath);
627                 //if the manifest contains malicious code do not open it
628                 if(!empty($fileIssues)){
629                         return $fileIssues;
630                 }
631                 $this->lockConfig();
632                 list($manifest, $installdefs) = MSLoadManifest($manifestPath);
633                 $fileIssues = $this->checkConfig($manifestPath);
634                 if(!empty($fileIssues)){
635                         return $fileIssues;
636                 }
637
638                 //scan for disabled actions
639                 if(isset($this->config['disableActions'])){
640                         foreach($this->config['disableActions'] as $action){
641                                 if(isset($installdefs[$this->manifestMap[$action]])){
642                                         $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
643                                 }
644                         }
645                 }
646
647                 //now lets scan for files that will override our files
648                 if(empty($this->config['disableRestrictedCopy']) && isset($installdefs['copy'])){
649                         foreach($installdefs['copy'] as $copy){
650                                 $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
651                                 $to = $copy['to'];
652                                 if(substr_count($from, '..')){
653                                         $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
654                                 }
655                                 if(substr_count($to, '..')){
656                                         $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
657                                 }
658                                 while(substr_count($from, '//')){
659                                         $from = str_replace('//', '/', $from);
660                                 }
661                                 while(substr_count($to, '//')){
662                                         $to = str_replace('//', '/', $to);
663                                 }
664                                 $this->scanCopy($from, $to);
665                         }
666                 }
667                 if(!empty($issues)){
668                         $this->issues['manifest'][$manifestPath] = $issues;
669                 }
670
671
672
673         }
674
675
676
677         /**
678          * Takes in where the file will is specified to be copied from and to
679          * and ensures that there is no official sugar file there. If the file exists it will check
680          * against the MD5 file list to see if Sugar Created the file
681          *
682          */
683         function scanCopy($from, $to){
684                                 //if the file doesn't exist for the $to then it is not overriding anything
685                                 if(!file_exists($to))return;
686                                 //if $to is a dir and $from is a file then make $to a full file path as well
687                                 if(is_dir($to) && is_file($from)){
688                                         if(substr($to,-1) === '/'){
689                                                 $to = substr($to, 0 , strlen($to) - 1);
690                                         }
691                                         $to .= '/'. basename($from);
692                                 }
693                                 //if the $to is a file and it is found in sugarFileExists then don't allow overriding it
694                                 if(is_file($to) && $this->sugarFileExists($to)){
695                                         $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
696                                 }
697
698                                 if(is_dir($from)){
699                                         $d = dir($from);
700                                         while($e = $d->read()){
701                                                 if($e == '.' || $e == '..')continue;
702                                                 $this->scanCopy($from .'/'. $e, $to .'/' . $e);
703                                         }
704                                 }
705
706
707
708
709
710                         }
711
712
713         /**
714          *Main external function that takes in a path to a package and then scans
715          *that package's manifest for disabled actions and then it scans the PHP files
716          *for restricted function calls
717          *
718          */
719         public function scanPackage($path){
720                 $this->pathToModule = $path;
721                 $this->scanManifest($path . '/manifest.php');
722                 if(empty($this->config['disableFileScan'])){
723                         $this->scanDir($path);
724                 }
725         }
726
727         /**
728          *This function will take all issues of the current instance and print them to the screen
729          **/
730         public function displayIssues($package='Package'){
731                 echo '<h2>'.str_replace('{PACKAGE}' , $package ,translate('ML_PACKAGE_SCANNING')). '</h2><BR><h2 class="error">' . translate('ML_INSTALLATION_FAILED') . '</h2><br><p>' .str_replace('{PACKAGE}' , $package ,translate('ML_PACKAGE_NOT_CONFIRM')). '</p><ul><li>'. translate('ML_OBTAIN_NEW_PACKAGE') . '<li>' . translate('ML_RELAX_LOCAL').
732 '</ul></p><br>' . translate('ML_SUGAR_LOADING_POLICY') .  ' <a href=" http://kb.sugarcrm.com/custom/module-loader-restrictions-for-sugar-open-cloud/">' . translate('ML_SUGAR_KB') . '</a>.'.
733 '<br>' . translate('ML_AVAIL_RESTRICTION'). ' <a href=" http://developers.sugarcrm.com/wordpress/2009/08/14/module-loader-restrictions/">' . translate('ML_SUGAR_DZ') .  '</a>.<br><br>';
734
735
736                 foreach($this->issues as $type=>$issues){
737                         echo '<div class="error"><h2>'. ucfirst($type) .' ' .  translate('ML_ISSUES') . '</h2> </div>';
738                         echo '<div id="details' . $type . '" >';
739                         foreach($issues as $file=>$issue){
740                                 $file = str_replace($this->pathToModule . '/', '', $file);
741                                 echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">';
742                                 if(is_array($issue)){
743                                         foreach($issue as $i){
744                                                 echo "$i<br>";
745                                         }
746                                 }else{
747                                         echo "$issue<br>";
748                                 }
749                                 echo "</div>";
750                         }
751                         echo '</div>';
752
753                 }
754                 echo "<br><input class='button' onclick='document.location.href=\"index.php?module=Administration&action=UpgradeWizard&view=module\"' type='button' value=\"" . translate('LBL_UW_BTN_BACK_TO_MOD_LOADER') . "\" />";
755
756         }
757
758         /**
759          * Lock config settings
760          */
761         public function lockConfig()
762         {
763             if(empty($this->config_hash)) {
764                 $this->config_hash = md5(serialize($GLOBALS['sugar_config']));
765             }
766         }
767
768         /**
769          * Check if config was modified. Return
770          * @param string $file
771          * @return array Errors if something wrong, false if no problems
772          */
773         public function checkConfig($file)
774         {
775             $config_hash_after = md5(serialize($GLOBALS['sugar_config']));
776             if($config_hash_after != $this->config_hash) {
777                 $this->issues['file'][$file] = array(translate('ML_CONFIG_OVERRIDE'));
778                 return $this->issues;
779             }
780             return false;
781         }
782
783 }
784
785 /**
786  * Load manifest file
787  * Outside of the class to isolate the context
788  * @param string $manifest_file
789  * @return array
790  */
791 function MSLoadManifest($manifest_file)
792 {
793         include( $manifest_file );
794         return array($manifest, $installdefs);
795 }
796
797 ?>