PageRenderTime 49ms CodeModel.GetById 19ms RepoModel.GetById 0ms app.codeStats 1ms

/ModuleInstall/ModuleScanner.php

https://github.com/vincentamari/SuperSweetAdmin
PHP | 436 lines | 293 code | 60 blank | 83 comment | 55 complexity | 533ad4511f2d9d521cb30ac7c10c48f1 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, AGPL-3.0, LGPL-2.1
  1. <?php
  2. if(!defined('sugarEntry') || !sugarEntry) die('Not A Valid Entry Point');
  3. /*********************************************************************************
  4. * SugarCRM 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. class ModuleScanner{
  38. private $manifestMap = array(
  39. 'pre_execute'=>'pre_execute',
  40. 'install_mkdirs'=>'mkdir',
  41. 'install_copy'=>'copy',
  42. 'install_images'=>'image_dir',
  43. 'install_menus'=>'menu',
  44. 'install_userpage'=>'user_page',
  45. 'install_dashlets'=>'dashlets',
  46. 'install_administration'=>'administration',
  47. 'install_connectors'=>'connectors',
  48. 'install_vardefs'=>'vardefs',
  49. 'install_layoutdefs'=>'layoutdefs',
  50. 'install_layoutfields'=>'layoutfields',
  51. 'install_relationships'=>'relationships',
  52. 'install_languages'=>'language',
  53. 'install_logichooks'=>'logic_hooks',
  54. 'post_execute'=>'post_execute',
  55. );
  56. private $blackListExempt = array();
  57. private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml');
  58. private $blackList = array(
  59. 'eval',
  60. 'exec',
  61. 'system',
  62. 'shell_exec',
  63. 'passthru',
  64. 'chgrp',
  65. 'chmod',
  66. 'chwown',
  67. 'file_put_contents',
  68. 'file',
  69. 'fileatime',
  70. 'filectime',
  71. 'filegroup',
  72. 'fileinode',
  73. 'filemtime',
  74. 'fileowner',
  75. 'fileperms',
  76. 'fopen',
  77. 'is_executable',
  78. 'is_writable',
  79. 'is_writeable',
  80. 'lchgrp',
  81. 'lchown',
  82. 'linkinfo',
  83. 'lstat',
  84. 'mkdir',
  85. 'parse_ini_file',
  86. 'rmdir',
  87. 'stat',
  88. 'tempnam',
  89. 'touch',
  90. 'unlink',
  91. 'getimagesize',
  92. 'call_user_func',
  93. 'call_user_func_array',
  94. 'create_function',
  95. //mutliple files per function call
  96. 'copy',
  97. 'link',
  98. 'rename',
  99. 'symlink',
  100. 'move_uploaded_file',
  101. 'chdir',
  102. 'chroot',
  103. 'create_cache_directory',
  104. 'mk_temp_dir',
  105. 'write_array_to_file',
  106. 'write_encoded_file',
  107. 'create_custom_directory',
  108. 'sugar_rename',
  109. 'sugar_chown',
  110. 'sugar_fopen',
  111. 'sugar_mkdir',
  112. 'sugar_file_put_contents',
  113. 'sugar_chgrp',
  114. 'sugar_chmod',
  115. 'sugar_touch',
  116. );
  117. public function printToWiki(){
  118. echo "'''Default Extensions'''<br>";
  119. foreach($this->validExt as $b){
  120. echo '#' . $b . '<br>';
  121. }
  122. echo "'''Default Black Listed Functions'''<br>";
  123. foreach($this->blackList as $b){
  124. echo '#' . $b . '<br>';
  125. }
  126. }
  127. public function __construct(){
  128. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackListExempt'])){
  129. $this->blackListExempt = array_merge($this->blackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['blackListExempt']);
  130. }
  131. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackList'])){
  132. $this->blackList = array_merge($this->blackList, $GLOBALS['sugar_config']['moduleInstaller']['blackList']);
  133. }
  134. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['validExt'])){
  135. $this->validExt = array_merge($this->validExt, $GLOBALS['sugar_config']['moduleInstaller']['validExt']);
  136. }
  137. }
  138. private $issues = array();
  139. private $pathToModule = '';
  140. /**
  141. *returns a list of issues
  142. */
  143. public function getIssues(){
  144. return $this->issues;
  145. }
  146. /**
  147. *returns true or false if any issues were found
  148. */
  149. public function hasIssues(){
  150. return !empty($this->issues);
  151. }
  152. /**
  153. *Ensures that a file has a valid extension
  154. */
  155. private function isValidExtension($file){
  156. $file = strtolower($file);
  157. $extPos = strrpos($file, '.');
  158. //make sure they don't override the files.md5
  159. if($extPos === false || $file == 'files.md5')return false;
  160. $ext = substr($file, $extPos + 1);
  161. return in_array($ext, $this->validExt);
  162. }
  163. /**
  164. *Scans a directory and calls on scan file for each file
  165. **/
  166. public function scanDir($path){
  167. static $startPath = '';
  168. if(empty($startPath))$startPath = $path;
  169. if(!is_dir($path))return false;
  170. $d = dir($path);
  171. while($e = $d->read()){
  172. $next = $path . '/' . $e;
  173. if(is_dir($next)){
  174. if(substr($e, 0, 1) == '.')continue;
  175. $this->scanDir($next);
  176. }else{
  177. $issues = $this->scanFile($next);
  178. }
  179. }
  180. return true;
  181. }
  182. /**
  183. * 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
  184. * $var() and ` are always prevented then whatever is in the blacklist.
  185. * It will also ensure that all files are of valid extension types
  186. *
  187. */
  188. public function scanFile($file){
  189. $issues = array();
  190. if(!$this->isValidExtension($file)){
  191. $issues[] = translate('ML_INVALID_EXT');
  192. $this->issues['file'][$file] = $issues;
  193. return $issues;
  194. }
  195. $contents = file_get_contents($file);
  196. if(stripos($contents,'<?php') === false )return $issues;
  197. $tokens = token_get_all($contents);
  198. $checkFunction = false;
  199. $possibleIssue = '';
  200. $lastToken = false;
  201. foreach($tokens as $index=>$token){
  202. if(is_string($token[0])){
  203. switch($token[0]){
  204. case '`':
  205. $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
  206. case '(':
  207. if($checkFunction)$issues[] = $possibleIssue;
  208. break;
  209. }
  210. $checkFunction = false;
  211. $possibleIssue = '';
  212. }else{
  213. $token['_msi'] = token_name($token[0]);
  214. switch($token[0]){
  215. case T_WHITESPACE: continue;
  216. case T_EVAL:
  217. if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
  218. $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
  219. break;
  220. case T_STRING:
  221. $token[1] = strtolower($token[1]);
  222. if(!in_array($token[1], $this->blackList))break;
  223. if(in_array($token[1], $this->blackListExempt))break;
  224. if ($lastToken !== false &&
  225. ($lastToken[0] == T_NEW || $lastToken[0] == T_OBJECT_OPERATOR || $lastToken[0] == T_DOUBLE_COLON))
  226. {
  227. break;
  228. }
  229. case T_VARIABLE:
  230. $checkFunction = true;
  231. $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' . $token[1] . '()';
  232. break;
  233. default:
  234. $checkFunction = false;
  235. $possibleIssue = '';
  236. }
  237. if ($token[0] != T_WHITESPACE)
  238. {
  239. $lastToken = $token;
  240. }
  241. }
  242. }
  243. if(!empty($issues)){
  244. $this->issues['file'][$file] = $issues;
  245. }
  246. return $issues;
  247. }
  248. /*
  249. * checks files.md5 file to see if the file is from sugar
  250. * ONLY WORKS ON FILES
  251. */
  252. public function sugarFileExists($path){
  253. static $md5 = array();
  254. if(empty($md5)){
  255. include('files.md5');
  256. $md5 = $md5_string;
  257. }
  258. if(isset($md5['./' . $path]))return true;
  259. }
  260. /**
  261. *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
  262. *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
  263. */
  264. public function scanManifest($manifestPath){
  265. $issues = array();
  266. if(!file_exists($manifestPath)){
  267. $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
  268. return $issues;
  269. }
  270. $fileIssues = $this->scanFile($manifestPath);
  271. //if the manifest contains malicious code do not open it
  272. if(!empty($fileIssues)){
  273. return $fileIssues;
  274. }
  275. include($manifestPath);
  276. //scan for disabled actions
  277. if(isset($GLOBALS['sugar_config']['moduleInstaller']['disableActions'])){
  278. foreach($GLOBALS['sugar_config']['moduleInstaller']['disableActions'] as $action){
  279. if(isset($installdefs[$this->manifestMap[$action]])){
  280. $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
  281. }
  282. }
  283. }
  284. //now lets scan for files that will override our files
  285. if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy']) && isset($installdefs['copy'])){
  286. foreach($installdefs['copy'] as $copy){
  287. $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
  288. $to = $copy['to'];
  289. if(substr_count($from, '..')){
  290. $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
  291. }
  292. if(substr_count($to, '..')){
  293. $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
  294. }
  295. while(substr_count($from, '//')){
  296. $from = str_replace('//', '/', $from);
  297. }
  298. while(substr_count($to, '//')){
  299. $to = str_replace('//', '/', $to);
  300. }
  301. $this->scanCopy($from, $to);
  302. }
  303. }
  304. if(!empty($issues)){
  305. $this->issues['manifest'][$manifestPath] = $issues;
  306. }
  307. }
  308. /**
  309. * Takes in where the file will is specified to be copied from and to
  310. * and ensures that there is no official sugar file there. If the file exists it will check
  311. * against the MD5 file list to see if Sugar Created the file
  312. *
  313. */
  314. function scanCopy($from, $to){
  315. //if the file doesn't exist for the $to then it is not overriding anything
  316. if(!file_exists($to))return;
  317. //if $to is a dir and $from is a file then make $to a full file path as well
  318. if(is_dir($to) && is_file($from)){
  319. if(substr($to,-1) === '/'){
  320. $to = substr($to, 0 , strlen($to) - 1);
  321. }
  322. $to .= '/'. basename($from);
  323. }
  324. //if the $to is a file and it is found in sugarFileExists then don't allow overriding it
  325. if(is_file($to) && $this->sugarFileExists($to)){
  326. $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
  327. }
  328. if(is_dir($from)){
  329. $d = dir($from);
  330. while($e = $d->read()){
  331. if($e == '.' || $e == '..')continue;
  332. $this->scanCopy($from .'/'. $e, $to .'/' . $e);
  333. }
  334. }
  335. }
  336. /**
  337. *Main external function that takes in a path to a package and then scans
  338. *that package's manifest for disabled actions and then it scans the PHP files
  339. *for restricted function calls
  340. *
  341. */
  342. public function scanPackage($path){
  343. $this->pathToModule = $path;
  344. $this->scanManifest($path . '/manifest.php');
  345. if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableFileScan'])){
  346. $this->scanDir($path);
  347. }
  348. }
  349. /**
  350. *This function will take all issues of the current instance and print them to the screen
  351. **/
  352. public function displayIssues($package='Package'){
  353. 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').
  354. '</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>.'.
  355. '<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>';
  356. foreach($this->issues as $type=>$issues){
  357. echo '<div class="error"><h2>'. ucfirst($type) .' ' . translate('ML_ISSUES') . '</h2> </div>';
  358. echo '<div id="details' . $type . '" >';
  359. foreach($issues as $file=>$issue){
  360. $file = str_replace($this->pathToModule . '/', '', $file);
  361. echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">';
  362. if(is_array($issue)){
  363. foreach($issue as $i){
  364. echo "$i<br>";
  365. }
  366. }else{
  367. echo "$issue<br>";
  368. }
  369. echo "</div>";
  370. }
  371. echo '</div>';
  372. }
  373. 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') . "\" />";
  374. }
  375. }
  376. ?>