]> CyberLeo.Net >> Repos - Github/sugarcrm.git/blob - ModuleInstall/ModuleScanner.php
Release 6.4.2
[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-2012 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         private $blackListExempt = array();
60
61         private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml');
62         private $blackList = array(
63     'popen',
64     'proc_open',
65     'escapeshellarg',
66     'escapeshellcmd',
67     'proc_close',
68     'proc_get_status',
69     'proc_nice',
70     'basename',
71         'passthru',
72     'clearstatcache',
73     'delete',
74     'dirname',
75     'disk_free_space',
76     'disk_total_space',
77     'diskfreespace',
78     'fclose',
79     'feof',
80     'fflush',
81     'fgetc',
82     'fgetcsv',
83     'fgets',
84     'fgetss',
85     'file_exists',
86     'file_get_contents',
87     'filesize',
88     'filetype',
89     'flock',
90     'fnmatch',
91     'fpassthru',
92     'fputcsv',
93     'fputs',
94     'fread',
95     'fscanf',
96     'fseek',
97     'fstat',
98     'ftell',
99     'ftruncate',
100     'fwrite',
101     'glob',
102     'is_dir',
103     'is_file',
104     'is_link',
105     'is_readable',
106     'is_uploaded_file',
107     'parse_ini_string',
108     'pathinfo',
109     'pclose',
110     'readfile',
111     'readlink',
112     'realpath_cache_get',
113     'realpath_cache_size',
114     'realpath',
115     'rewind',
116     'set_file_buffer',
117     'tmpfile',
118     'umask',
119         'eval',
120         'exec',
121         'system',
122         'shell_exec',
123         'passthru',
124         'chgrp',
125         'chmod',
126         'chwown',
127         'file_put_contents',
128         'file',
129         'fileatime',
130         'filectime',
131         'filegroup',
132         'fileinode',
133         'filemtime',
134         'fileowner',
135         'fileperms',
136         'fopen',
137         'is_executable',
138         'is_writable',
139         'is_writeable',
140         'lchgrp',
141         'lchown',
142         'linkinfo',
143         'lstat',
144         'mkdir',
145         'parse_ini_file',
146         'rmdir',
147         'stat',
148         'tempnam',
149         'touch',
150         'unlink',
151         'getimagesize',
152         'call_user_func',
153         'call_user_func_array',
154         'create_function',
155
156
157         //mutliple files per function call
158         'copy',
159         'link',
160         'rename',
161         'symlink',
162         'move_uploaded_file',
163         'chdir',
164         'chroot',
165         'create_cache_directory',
166         'mk_temp_dir',
167         'write_array_to_file',
168         'write_encoded_file',
169         'create_custom_directory',
170         'sugar_rename',
171         'sugar_chown',
172         'sugar_fopen',
173         'sugar_mkdir',
174         'sugar_file_put_contents',
175         'sugar_chgrp',
176         'sugar_chmod',
177         'sugar_touch',
178
179 );
180
181         public function printToWiki(){
182                 echo "'''Default Extensions'''<br>";
183                 foreach($this->validExt as $b){
184                         echo '#' . $b . '<br>';
185
186                 }
187                 echo "'''Default Black Listed Functions'''<br>";
188                 foreach($this->blackList as $b){
189                         echo '#' . $b . '<br>';
190
191                 }
192
193         }
194
195         public function __construct(){
196                 if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackListExempt'])){
197                         $this->blackListExempt = array_merge($this->blackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['blackListExempt']);
198                 }
199                 if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackList'])){
200                         $this->blackList = array_merge($this->blackList, $GLOBALS['sugar_config']['moduleInstaller']['blackList']);
201                 }
202           if(!empty($GLOBALS['sugar_config']['moduleInstaller']['validExt'])){
203                         $this->validExt = array_merge($this->validExt, $GLOBALS['sugar_config']['moduleInstaller']['validExt']);
204                 }
205
206         }
207
208         private $issues = array();
209         private $pathToModule = '';
210
211         /**
212          *returns a list of issues
213          */
214         public function getIssues(){
215                 return $this->issues;
216         }
217
218         /**
219          *returns true or false if any issues were found
220          */
221         public function hasIssues(){
222                 return !empty($this->issues);
223         }
224
225         /**
226          *Ensures that a file has a valid extension
227          */
228         private function isValidExtension($file){
229                 $file = strtolower($file);
230
231                 $extPos = strrpos($file, '.');
232                 //make sure they don't override the files.md5
233                 if($extPos === false || $file == 'files.md5')return false;
234                 $ext = substr($file, $extPos + 1);
235                 return in_array($ext, $this->validExt);
236
237         }
238
239         /**
240          *Scans a directory and calls on scan file for each file
241          **/
242         public function scanDir($path){
243                 static $startPath = '';
244                 if(empty($startPath))$startPath = $path;
245                 if(!is_dir($path))return false;
246                 $d = dir($path);
247                 while($e = $d->read()){
248                 $next = $path . '/' . $e;
249                 if(is_dir($next)){
250                         if(substr($e, 0, 1) == '.')continue;
251                         $this->scanDir($next);
252                 }else{
253                         $issues = $this->scanFile($next);
254
255
256                 }
257                 }
258         return true;
259         }
260
261         /**
262          * Check if the file contents looks like PHP
263          * @param string $contents File contents
264          * @return boolean
265          */
266         public function isPHPFile($contents)
267         {
268             if(stripos($contents, '<?php') !== false) return true;
269             for($tag=0;($tag = stripos($contents, '<?', $tag)) !== false;$tag++) {
270             if(strncasecmp(substr($contents, $tag, 13), '<?xml version', 13) == 0) {
271                 // <?xml version is OK, skip it
272                 $tag++;
273                 continue;
274             }
275             // found <?, it's PHP
276             return true;
277             }
278             return false;
279         }
280
281         /**
282          * 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
283          * $var()  and ` are always prevented then whatever is in the blacklist.
284          * It will also ensure that all files are of valid extension types
285          *
286          */
287         public function scanFile($file){
288                 $issues = array();
289                 if(!$this->isValidExtension($file)){
290                         $issues[] = translate('ML_INVALID_EXT');
291                         $this->issues['file'][$file] = $issues;
292                         return $issues;
293                 }
294                 $contents = file_get_contents($file);
295                 if(!$this->isPHPFile($contents)) return $issues;
296                 $tokens = @token_get_all($contents);
297                 $checkFunction = false;
298                 $possibleIssue = '';
299                 $lastToken = false;
300                 foreach($tokens as $index=>$token){
301                         if(is_string($token[0])){
302                                 switch($token[0]){
303                                         case '`':
304                                                 $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
305                                         case '(':
306                                                 if($checkFunction)$issues[] = $possibleIssue;
307                                                 break;
308                                 }
309                                 $checkFunction = false;
310                                 $possibleIssue = '';
311                         }else{
312                                 $token['_msi'] = token_name($token[0]);
313                                 switch($token[0]){
314                                         case T_WHITESPACE: continue;
315                                         case T_EVAL:
316                                                 if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
317                                                 $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
318                                                 break;
319                                         case T_STRING:
320                                                 $token[1] = strtolower($token[1]);
321                                                 if(!in_array($token[1], $this->blackList))break;
322                                                 if(in_array($token[1], $this->blackListExempt))break;
323                                                 if ($lastToken !== false &&
324                                                 ($lastToken[0] == T_NEW || $lastToken[0] == T_OBJECT_OPERATOR ||  $lastToken[0] == T_DOUBLE_COLON))
325                                                 {
326                                                         break;
327                                                 }
328                                         case T_VARIABLE:
329                                                 $checkFunction = true;
330                                                 $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' .  $token[1] . '()';
331                                                 break;
332
333                                         default:
334                                                 $checkFunction = false;
335                                                 $possibleIssue = '';
336
337                                 }
338                                 if ($token[0] != T_WHITESPACE)
339                                 {
340                                         $lastToken = $token;
341                                 }
342                         }
343
344                 }
345                 if(!empty($issues)){
346                         $this->issues['file'][$file] = $issues;
347                 }
348
349                 return $issues;
350         }
351
352
353         /*
354          * checks files.md5 file to see if the file is from sugar
355          * ONLY WORKS ON FILES
356          */
357         public function sugarFileExists($path){
358                 static $md5 = array();
359                 if(empty($md5) && file_exists('files.md5'))
360                 {
361                         include('files.md5');
362                         $md5 = $md5_string;
363                 }
364                 if(isset($md5['./' . $path]))return true;
365
366
367         }
368
369
370         /**
371          *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
372          *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
373          */
374         public function scanManifest($manifestPath){
375                 $issues = array();
376                 if(!file_exists($manifestPath)){
377                         $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
378                         return $issues;
379                 }
380                 $fileIssues = $this->scanFile($manifestPath);
381                 //if the manifest contains malicious code do not open it
382                 if(!empty($fileIssues)){
383                         return $fileIssues;
384                 }
385                 include($manifestPath);
386
387
388                 //scan for disabled actions
389                 if(isset($GLOBALS['sugar_config']['moduleInstaller']['disableActions'])){
390                         foreach($GLOBALS['sugar_config']['moduleInstaller']['disableActions'] as $action){
391                                 if(isset($installdefs[$this->manifestMap[$action]])){
392                                         $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
393                                 }
394                         }
395                 }
396
397                 //now lets scan for files that will override our files
398                 if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy']) && isset($installdefs['copy'])){
399                         foreach($installdefs['copy'] as $copy){
400                                 $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
401                                 $to = $copy['to'];
402                                 if(substr_count($from, '..')){
403                                         $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
404                                 }
405                                 if(substr_count($to, '..')){
406                                         $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
407                                 }
408                                 while(substr_count($from, '//')){
409                                         $from = str_replace('//', '/', $from);
410                                 }
411                                 while(substr_count($to, '//')){
412                                         $to = str_replace('//', '/', $to);
413                                 }
414                                 $this->scanCopy($from, $to);
415                         }
416                 }
417                 if(!empty($issues)){
418                         $this->issues['manifest'][$manifestPath] = $issues;
419                 }
420
421
422
423         }
424
425
426
427         /**
428          * Takes in where the file will is specified to be copied from and to
429          * and ensures that there is no official sugar file there. If the file exists it will check
430          * against the MD5 file list to see if Sugar Created the file
431          *
432          */
433         function scanCopy($from, $to){
434                                 //if the file doesn't exist for the $to then it is not overriding anything
435                                 if(!file_exists($to))return;
436                                 //if $to is a dir and $from is a file then make $to a full file path as well
437                                 if(is_dir($to) && is_file($from)){
438                                         if(substr($to,-1) === '/'){
439                                                 $to = substr($to, 0 , strlen($to) - 1);
440                                         }
441                                         $to .= '/'. basename($from);
442                                 }
443                                 //if the $to is a file and it is found in sugarFileExists then don't allow overriding it
444                                 if(is_file($to) && $this->sugarFileExists($to)){
445                                         $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
446                                 }
447
448                                 if(is_dir($from)){
449                                         $d = dir($from);
450                                         while($e = $d->read()){
451                                                 if($e == '.' || $e == '..')continue;
452                                                 $this->scanCopy($from .'/'. $e, $to .'/' . $e);
453                                         }
454                                 }
455
456
457
458
459
460                         }
461
462
463         /**
464          *Main external function that takes in a path to a package and then scans
465          *that package's manifest for disabled actions and then it scans the PHP files
466          *for restricted function calls
467          *
468          */
469         public function scanPackage($path){
470                 $this->pathToModule = $path;
471                 $this->scanManifest($path . '/manifest.php');
472                 if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableFileScan'])){
473                         $this->scanDir($path);
474                 }
475         }
476
477         /**
478          *This function will take all issues of the current instance and print them to the screen
479          **/
480         public function displayIssues($package='Package'){
481                 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').
482 '</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>.'.
483 '<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>';
484
485
486                 foreach($this->issues as $type=>$issues){
487                         echo '<div class="error"><h2>'. ucfirst($type) .' ' .  translate('ML_ISSUES') . '</h2> </div>';
488                         echo '<div id="details' . $type . '" >';
489                         foreach($issues as $file=>$issue){
490                                 $file = str_replace($this->pathToModule . '/', '', $file);
491                                 echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">';
492                                 if(is_array($issue)){
493                                         foreach($issue as $i){
494                                                 echo "$i<br>";
495                                         }
496                                 }else{
497                                         echo "$issue<br>";
498                                 }
499                                 echo "</div>";
500                         }
501                         echo '</div>';
502
503                 }
504                 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') . "\" />";
505
506         }
507
508
509 }
510
511
512 ?>