PageRenderTime 50ms CodeModel.GetById 26ms RepoModel.GetById 0ms app.codeStats 0ms

/ModuleInstall/ModuleScanner.php

https://bitbucket.org/cviolette/sugarcrm
PHP | 725 lines | 513 code | 89 blank | 123 comment | 68 complexity | f9cffaf03d000e24d37e2c77b695d35e MD5 | raw file
Possible License(s): LGPL-2.1, MPL-2.0-no-copyleft-exception, BSD-3-Clause
  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. 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 $classBlackListExempt = array();
  58. private $validExt = array('png', 'gif', 'jpg', 'css', 'js', 'php', 'txt', 'html', 'htm', 'tpl', 'pdf', 'md5', 'xml');
  59. private $classBlackList = array(
  60. // Class names specified here must be in lowercase as the implementation
  61. // of the tokenizer converts all tokens to lowercase.
  62. 'reflection',
  63. 'reflectionclass',
  64. 'reflectionzendextension',
  65. 'reflectionextension',
  66. 'reflectionfunction',
  67. 'reflectionfunctionabstract',
  68. 'reflectionmethod',
  69. 'reflectionobject',
  70. 'reflectionparameter',
  71. 'reflectionproperty',
  72. 'reflector',
  73. 'reflectionexception',
  74. 'lua',
  75. );
  76. private $blackList = array(
  77. 'popen',
  78. 'proc_open',
  79. 'escapeshellarg',
  80. 'escapeshellcmd',
  81. 'proc_close',
  82. 'proc_get_status',
  83. 'proc_nice',
  84. 'basename',
  85. 'passthru',
  86. 'clearstatcache',
  87. 'delete',
  88. 'dirname',
  89. 'disk_free_space',
  90. 'disk_total_space',
  91. 'diskfreespace',
  92. 'fclose',
  93. 'feof',
  94. 'fflush',
  95. 'fgetc',
  96. 'fgetcsv',
  97. 'fgets',
  98. 'fgetss',
  99. 'file_exists',
  100. 'file_get_contents',
  101. 'filesize',
  102. 'filetype',
  103. 'flock',
  104. 'fnmatch',
  105. 'fpassthru',
  106. 'fputcsv',
  107. 'fputs',
  108. 'fread',
  109. 'fscanf',
  110. 'fseek',
  111. 'fstat',
  112. 'ftell',
  113. 'ftruncate',
  114. 'fwrite',
  115. 'glob',
  116. 'is_dir',
  117. 'is_file',
  118. 'is_link',
  119. 'is_readable',
  120. 'is_uploaded_file',
  121. 'parse_ini_string',
  122. 'pathinfo',
  123. 'pclose',
  124. 'readfile',
  125. 'readlink',
  126. 'realpath_cache_get',
  127. 'realpath_cache_size',
  128. 'realpath',
  129. 'rewind',
  130. 'set_file_buffer',
  131. 'tmpfile',
  132. 'umask',
  133. 'eval',
  134. 'exec',
  135. 'system',
  136. 'shell_exec',
  137. 'passthru',
  138. 'chgrp',
  139. 'chmod',
  140. 'chwown',
  141. 'file_put_contents',
  142. 'file',
  143. 'fileatime',
  144. 'filectime',
  145. 'filegroup',
  146. 'fileinode',
  147. 'filemtime',
  148. 'fileowner',
  149. 'fileperms',
  150. 'fopen',
  151. 'is_executable',
  152. 'is_writable',
  153. 'is_writeable',
  154. 'lchgrp',
  155. 'lchown',
  156. 'linkinfo',
  157. 'lstat',
  158. 'mkdir',
  159. 'parse_ini_file',
  160. 'rmdir',
  161. 'stat',
  162. 'tempnam',
  163. 'touch',
  164. 'unlink',
  165. 'getimagesize',
  166. 'call_user_func',
  167. 'call_user_func_array',
  168. 'create_function',
  169. //mutliple files per function call
  170. 'copy',
  171. 'link',
  172. 'rename',
  173. 'symlink',
  174. 'move_uploaded_file',
  175. 'chdir',
  176. 'chroot',
  177. 'create_cache_directory',
  178. 'mk_temp_dir',
  179. 'write_array_to_file',
  180. 'write_encoded_file',
  181. 'create_custom_directory',
  182. 'sugar_rename',
  183. 'sugar_chown',
  184. 'sugar_fopen',
  185. 'sugar_mkdir',
  186. 'sugar_file_put_contents',
  187. 'sugar_chgrp',
  188. 'sugar_chmod',
  189. 'sugar_touch',
  190. // Functions that have callbacks can circumvent our security measures.
  191. // List retrieved through PHP's XML documentation, and running the
  192. // following script in the reference directory:
  193. // grep -R callable . | grep -v \.svn | grep methodparam | cut -d: -f1 | sort -u | cut -d"." -f2 | sed 's/\-/\_/g' | cut -d"/" -f4
  194. // AMQPQueue
  195. 'consume',
  196. // PHP internal - arrays
  197. 'array_diff_uassoc',
  198. 'array_diff_ukey',
  199. 'array_filter',
  200. 'array_intersect_uassoc',
  201. 'array_intersect_ukey',
  202. 'array_map',
  203. 'array_reduce',
  204. 'array_udiff_assoc',
  205. 'array_udiff_uassoc',
  206. 'array_udiff',
  207. 'array_uintersect_assoc',
  208. 'array_uintersect_uassoc',
  209. 'array_uintersect',
  210. 'array_walk_recursive',
  211. 'array_walk',
  212. 'uasort',
  213. 'uksort',
  214. 'usort',
  215. // EIO functions that accept callbacks.
  216. 'eio_busy',
  217. 'eio_chmod',
  218. 'eio_chown',
  219. 'eio_close',
  220. 'eio_custom',
  221. 'eio_dup2',
  222. 'eio_fallocate',
  223. 'eio_fchmod',
  224. 'eio_fchown',
  225. 'eio_fdatasync',
  226. 'eio_fstat',
  227. 'eio_fstatvfs',
  228. 'eio_fsync',
  229. 'eio_ftruncate',
  230. 'eio_futime',
  231. 'eio_grp',
  232. 'eio_link',
  233. 'eio_lstat',
  234. 'eio_mkdir',
  235. 'eio_mknod',
  236. 'eio_nop',
  237. 'eio_open',
  238. 'eio_read',
  239. 'eio_readahead',
  240. 'eio_readdir',
  241. 'eio_readlink',
  242. 'eio_realpath',
  243. 'eio_rename',
  244. 'eio_rmdir',
  245. 'eio_sendfile',
  246. 'eio_stat',
  247. 'eio_statvfs',
  248. 'eio_symlink',
  249. 'eio_sync_file_range',
  250. 'eio_sync',
  251. 'eio_syncfs',
  252. 'eio_truncate',
  253. 'eio_unlink',
  254. 'eio_utime',
  255. 'eio_write',
  256. // PHP internal - error functions
  257. 'set_error_handler',
  258. 'set_exception_handler',
  259. // Forms Data Format functions
  260. 'fdf_enum_values',
  261. // PHP internal - function handling
  262. 'call_user_func_array',
  263. 'call_user_func',
  264. 'forward_static_call_array',
  265. 'forward_static_call',
  266. 'register_shutdown_function',
  267. 'register_tick_function',
  268. // Gearman
  269. 'setclientcallback',
  270. 'setcompletecallback',
  271. 'setdatacallback',
  272. 'setexceptioncallback',
  273. 'setfailcallback',
  274. 'setstatuscallback',
  275. 'setwarningcallback',
  276. 'setworkloadcallback',
  277. 'addfunction',
  278. // Firebird/InterBase
  279. 'ibase_set_event_handler',
  280. // LDAP
  281. 'ldap_set_rebind_proc',
  282. // LibXML
  283. 'libxml_set_external_entity_loader',
  284. // Mailparse functions
  285. 'mailparse_msg_extract_part_file',
  286. 'mailparse_msg_extract_part',
  287. 'mailparse_msg_extract_whole_part_file',
  288. // Memcache(d) functions
  289. 'addserver',
  290. 'setserverparams',
  291. 'get',
  292. 'getbykey',
  293. 'getdelayed',
  294. 'getdelayedbykey',
  295. // MySQLi
  296. 'set_local_infile_handler',
  297. // PHP internal - network functions
  298. 'header_register_callback',
  299. // Newt
  300. 'newt_entry_set_filter',
  301. 'newt_set_suspend_callback',
  302. // OAuth
  303. 'consumerhandler',
  304. 'timestampnoncehandler',
  305. 'tokenhandler',
  306. // PHP internal - output control
  307. 'ob_start',
  308. // PHP internal - PCNTL
  309. 'pcntl_signal',
  310. // PHP internal - PCRE
  311. 'preg_replace_callback',
  312. // SQLite
  313. 'sqlitecreateaggregate',
  314. 'sqlitecreatefunction',
  315. 'sqlite_create_aggregate',
  316. 'sqlite_create_function',
  317. // RarArchive
  318. 'open',
  319. // Readline
  320. 'readline_callback_handler_install',
  321. 'readline_completion_function',
  322. // PHP internal - session handling
  323. 'session_set_save_handler',
  324. // PHP internal - SPL
  325. 'construct',
  326. 'iterator_apply',
  327. 'spl_autoload_register',
  328. // Sybase
  329. 'sybase_set_message_handler',
  330. // PHP internal - variable handling
  331. 'is_callable',
  332. // XML Parser
  333. 'xml_set_character_data_handler',
  334. 'xml_set_default_handler',
  335. 'xml_set_element_handler',
  336. 'xml_set_end_namespace_decl_handler',
  337. 'xml_set_external_entity_ref_handler',
  338. 'xml_set_notation_decl_handler',
  339. 'xml_set_processing_instruction_handler',
  340. 'xml_set_start_namespace_decl_handler',
  341. 'xml_set_unparsed_entity_decl_handler',
  342. );
  343. public function printToWiki(){
  344. echo "'''Default Extensions'''<br>";
  345. foreach($this->validExt as $b){
  346. echo '#' . $b . '<br>';
  347. }
  348. echo "'''Default Black Listed Functions'''<br>";
  349. foreach($this->blackList as $b){
  350. echo '#' . $b . '<br>';
  351. }
  352. }
  353. public function __construct(){
  354. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackListExempt'])){
  355. $this->blackListExempt = array_merge($this->blackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['blackListExempt']);
  356. }
  357. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['blackList'])){
  358. $this->blackList = array_merge($this->blackList, $GLOBALS['sugar_config']['moduleInstaller']['blackList']);
  359. }
  360. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['classBlackListExempt'])){
  361. $this->classBlackListExempt = array_merge($this->classBlackListExempt, $GLOBALS['sugar_config']['moduleInstaller']['classBlackListExempt']);
  362. }
  363. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['classBlackList'])){
  364. $this->classBlackList = array_merge($this->classBlackList, $GLOBALS['sugar_config']['moduleInstaller']['classBlackList']);
  365. }
  366. if(!empty($GLOBALS['sugar_config']['moduleInstaller']['validExt'])){
  367. $this->validExt = array_merge($this->validExt, $GLOBALS['sugar_config']['moduleInstaller']['validExt']);
  368. }
  369. }
  370. private $issues = array();
  371. private $pathToModule = '';
  372. /**
  373. *returns a list of issues
  374. */
  375. public function getIssues(){
  376. return $this->issues;
  377. }
  378. /**
  379. *returns true or false if any issues were found
  380. */
  381. public function hasIssues(){
  382. return !empty($this->issues);
  383. }
  384. /**
  385. *Ensures that a file has a valid extension
  386. */
  387. private function isValidExtension($file){
  388. $file = strtolower($file);
  389. $extPos = strrpos($file, '.');
  390. //make sure they don't override the files.md5
  391. if($extPos === false || $file == 'files.md5')return false;
  392. $ext = substr($file, $extPos + 1);
  393. return in_array($ext, $this->validExt);
  394. }
  395. /**
  396. *Scans a directory and calls on scan file for each file
  397. **/
  398. public function scanDir($path){
  399. static $startPath = '';
  400. if(empty($startPath))$startPath = $path;
  401. if(!is_dir($path))return false;
  402. $d = dir($path);
  403. while($e = $d->read()){
  404. $next = $path . '/' . $e;
  405. if(is_dir($next)){
  406. if(substr($e, 0, 1) == '.')continue;
  407. $this->scanDir($next);
  408. }else{
  409. $issues = $this->scanFile($next);
  410. }
  411. }
  412. return true;
  413. }
  414. /**
  415. * Check if the file contents looks like PHP
  416. * @param string $contents File contents
  417. * @return boolean
  418. */
  419. public function isPHPFile($contents)
  420. {
  421. if(stripos($contents, '<?php') !== false) return true;
  422. for($tag=0;($tag = stripos($contents, '<?', $tag)) !== false;$tag++) {
  423. if(strncasecmp(substr($contents, $tag, 13), '<?xml version', 13) == 0) {
  424. // <?xml version is OK, skip it
  425. $tag++;
  426. continue;
  427. }
  428. // found <?, it's PHP
  429. return true;
  430. }
  431. return false;
  432. }
  433. /**
  434. * 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
  435. * $var() and ` are always prevented then whatever is in the blacklist.
  436. * It will also ensure that all files are of valid extension types
  437. *
  438. */
  439. public function scanFile($file){
  440. $issues = array();
  441. if(!$this->isValidExtension($file)){
  442. $issues[] = translate('ML_INVALID_EXT');
  443. $this->issues['file'][$file] = $issues;
  444. return $issues;
  445. }
  446. $contents = file_get_contents($file);
  447. if(!$this->isPHPFile($contents)) return $issues;
  448. $tokens = @token_get_all($contents);
  449. $checkFunction = false;
  450. $possibleIssue = '';
  451. $lastToken = false;
  452. foreach($tokens as $index=>$token){
  453. if(is_string($token[0])){
  454. switch($token[0]){
  455. case '`':
  456. $issues['backtick'] = translate('ML_INVALID_FUNCTION') . " '`'";
  457. case '(':
  458. if($checkFunction)$issues[] = $possibleIssue;
  459. break;
  460. }
  461. $checkFunction = false;
  462. $possibleIssue = '';
  463. }else{
  464. $token['_msi'] = token_name($token[0]);
  465. switch($token[0]){
  466. case T_WHITESPACE: continue;
  467. case T_EVAL:
  468. if(in_array('eval', $this->blackList) && !in_array('eval', $this->blackListExempt))
  469. $issues[]= translate('ML_INVALID_FUNCTION') . ' eval()';
  470. break;
  471. case T_STRING:
  472. $token[1] = strtolower($token[1]);
  473. if($lastToken !== false && $lastToken[0] == T_NEW) {
  474. if(!in_array($token[1], $this->classBlackList))break;
  475. if(in_array($token[1], $this->classBlackListExempt))break;
  476. } elseif ($token[0] == T_DOUBLE_COLON) {
  477. if(!in_array($lastToken[1], $this->classBlackList))break;
  478. if(in_array($lastToken[1], $this->classBlackListExempt))break;
  479. } else {
  480. if(!in_array($token[1], $this->blackList))break;
  481. if(in_array($token[1], $this->blackListExempt))break;
  482. if ($lastToken !== false &&
  483. ($lastToken[0] == T_OBJECT_OPERATOR || $lastToken[0] == T_DOUBLE_COLON))
  484. {
  485. break;
  486. }
  487. }
  488. case T_VARIABLE:
  489. $checkFunction = true;
  490. $possibleIssue = translate('ML_INVALID_FUNCTION') . ' ' . $token[1] . '()';
  491. break;
  492. default:
  493. $checkFunction = false;
  494. $possibleIssue = '';
  495. }
  496. if ($token[0] != T_WHITESPACE)
  497. {
  498. $lastToken = $token;
  499. }
  500. }
  501. }
  502. if(!empty($issues)){
  503. $this->issues['file'][$file] = $issues;
  504. }
  505. return $issues;
  506. }
  507. /*
  508. * checks files.md5 file to see if the file is from sugar
  509. * ONLY WORKS ON FILES
  510. */
  511. public function sugarFileExists($path){
  512. static $md5 = array();
  513. if(empty($md5) && file_exists('files.md5'))
  514. {
  515. include('files.md5');
  516. $md5 = $md5_string;
  517. }
  518. if(isset($md5['./' . $path]))return true;
  519. }
  520. /**
  521. *This function will scan the Manifest for disabled actions specified in $GLOBALS['sugar_config']['moduleInstaller']['disableActions']
  522. *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
  523. */
  524. public function scanManifest($manifestPath){
  525. $issues = array();
  526. if(!file_exists($manifestPath)){
  527. $this->issues['manifest'][$manifestPath] = translate('ML_NO_MANIFEST');
  528. return $issues;
  529. }
  530. $fileIssues = $this->scanFile($manifestPath);
  531. //if the manifest contains malicious code do not open it
  532. if(!empty($fileIssues)){
  533. return $fileIssues;
  534. }
  535. include($manifestPath);
  536. //scan for disabled actions
  537. if(isset($GLOBALS['sugar_config']['moduleInstaller']['disableActions'])){
  538. foreach($GLOBALS['sugar_config']['moduleInstaller']['disableActions'] as $action){
  539. if(isset($installdefs[$this->manifestMap[$action]])){
  540. $issues[] = translate('ML_INVALID_ACTION_IN_MANIFEST') . $this->manifestMap[$action];
  541. }
  542. }
  543. }
  544. //now lets scan for files that will override our files
  545. if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableRestrictedCopy']) && isset($installdefs['copy'])){
  546. foreach($installdefs['copy'] as $copy){
  547. $from = str_replace('<basepath>', $this->pathToModule, $copy['from']);
  548. $to = $copy['to'];
  549. if(substr_count($from, '..')){
  550. $this->issues['copy'][$from] = translate('ML_PATH_MAY_NOT_CONTAIN').' ".." -' . $from;
  551. }
  552. if(substr_count($to, '..')){
  553. $this->issues['copy'][$to] = translate('ML_PATH_MAY_NOT_CONTAIN'). ' ".." -' . $to;
  554. }
  555. while(substr_count($from, '//')){
  556. $from = str_replace('//', '/', $from);
  557. }
  558. while(substr_count($to, '//')){
  559. $to = str_replace('//', '/', $to);
  560. }
  561. $this->scanCopy($from, $to);
  562. }
  563. }
  564. if(!empty($issues)){
  565. $this->issues['manifest'][$manifestPath] = $issues;
  566. }
  567. }
  568. /**
  569. * Takes in where the file will is specified to be copied from and to
  570. * and ensures that there is no official sugar file there. If the file exists it will check
  571. * against the MD5 file list to see if Sugar Created the file
  572. *
  573. */
  574. function scanCopy($from, $to){
  575. //if the file doesn't exist for the $to then it is not overriding anything
  576. if(!file_exists($to))return;
  577. //if $to is a dir and $from is a file then make $to a full file path as well
  578. if(is_dir($to) && is_file($from)){
  579. if(substr($to,-1) === '/'){
  580. $to = substr($to, 0 , strlen($to) - 1);
  581. }
  582. $to .= '/'. basename($from);
  583. }
  584. //if the $to is a file and it is found in sugarFileExists then don't allow overriding it
  585. if(is_file($to) && $this->sugarFileExists($to)){
  586. $this->issues['copy'][$from] = translate('ML_OVERRIDE_CORE_FILES') . '(' . $to . ')';
  587. }
  588. if(is_dir($from)){
  589. $d = dir($from);
  590. while($e = $d->read()){
  591. if($e == '.' || $e == '..')continue;
  592. $this->scanCopy($from .'/'. $e, $to .'/' . $e);
  593. }
  594. }
  595. }
  596. /**
  597. *Main external function that takes in a path to a package and then scans
  598. *that package's manifest for disabled actions and then it scans the PHP files
  599. *for restricted function calls
  600. *
  601. */
  602. public function scanPackage($path){
  603. $this->pathToModule = $path;
  604. $this->scanManifest($path . '/manifest.php');
  605. if(empty($GLOBALS['sugar_config']['moduleInstaller']['disableFileScan'])){
  606. $this->scanDir($path);
  607. }
  608. }
  609. /**
  610. *This function will take all issues of the current instance and print them to the screen
  611. **/
  612. public function displayIssues($package='Package'){
  613. 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').
  614. '</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>.'.
  615. '<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>';
  616. foreach($this->issues as $type=>$issues){
  617. echo '<div class="error"><h2>'. ucfirst($type) .' ' . translate('ML_ISSUES') . '</h2> </div>';
  618. echo '<div id="details' . $type . '" >';
  619. foreach($issues as $file=>$issue){
  620. $file = str_replace($this->pathToModule . '/', '', $file);
  621. echo '<div style="position:relative;left:10px"><b>' . $file . '</b></div><div style="position:relative;left:20px">';
  622. if(is_array($issue)){
  623. foreach($issue as $i){
  624. echo "$i<br>";
  625. }
  626. }else{
  627. echo "$issue<br>";
  628. }
  629. echo "</div>";
  630. }
  631. echo '</div>';
  632. }
  633. 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') . "\" />";
  634. }
  635. }
  636. ?>