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