From 9220a638b509ca0bdd5398ab80eab3222d50fec4 Mon Sep 17 00:00:00 2001
From: Laurent Destailleur <eldy@destailleur.fr>
Date: Mon, 6 Feb 2017 13:37:23 +0100
Subject: [PATCH] NEW Filechecker can include custom dir and report added
 files.

---
 build/generate_filelist_xml.php   |  43 +++++--
 htdocs/admin/system/filecheck.php | 184 +++++++++++++++++++++---------
 htdocs/core/lib/files.lib.php     |   8 +-
 htdocs/langs/en_US/admin.lang     |   5 +-
 4 files changed, 173 insertions(+), 67 deletions(-)

diff --git a/build/generate_filelist_xml.php b/build/generate_filelist_xml.php
index 07e87dd86bb..01809439bf1 100755
--- a/build/generate_filelist_xml.php
+++ b/build/generate_filelist_xml.php
@@ -42,35 +42,55 @@ require_once(DOL_DOCUMENT_ROOT."/core/lib/files.lib.php");
 
 if (empty($argv[1])) 
 {
-    print "Usage: ".$script_file." release=x.y.z\n";
+    print "Usage: ".$script_file." release=x.y.z[-...] [includecustom=1]\n";
     exit -1;
 }
 parse_str($argv[1]);
+if (! empty($argv[2])) parse_str($argv[2]);
 
-if ($release != DOL_VERSION)
+if (empty($includecustom)) 
 {
-    print 'Error: release is not version declared into filefunc.in.php.'."\n";
-    exit -1;
+    $includecustom=0;
+    
+    if (DOL_VERSION != $release)
+    {
+        print 'Error: When parameter "includecustom" is not set, version declared into filefunc.in.php ('.DOL_VERSION.') must be exact same value than "release" parmater.'."\n";
+        print "Usage: ".$script_file." release=x.y.z[-...] [includecustom=1]\n";
+        exit -1;
+    }
+}
+else
+{
+    if (! preg_match('/'.preg_quote(DOL_VERSION,'/').'-/',$release))
+    {
+        print 'Error: When parameter "includecustom" is not set, version declared into ('.DOL_VERSION.') must be used with a suffix into "release" parmater (ex: '.DOL_VERSION.'-mydistrib).'."\n";
+        print "Usage: ".$script_file." release=x.y.z[-...] [includecustom=1]\n";
+        exit -1;
+    }
 }
 
+print "Version: ".$release."\n";
+print "Include custom: ".$includecustom."\n";
+
 //$outputfile=dirname(__FILE__).'/../htdocs/install/filelist-'.$release.'.xml';
 $outputdir=dirname(__FILE__).'/../htdocs/install';
-print 'Delete current files '.$outputdir.'/filelist*.xml'."\n";
-dol_delete_file($outputdir.'/filelist*.xml',0,1,1);
+print 'Delete current files '.$outputdir.'/filelist-'.$release.'.xml'."\n";
+dol_delete_file($outputdir.'/filelist-'.$release.'.xml',0,1,1);
 
 $outputfile=$outputdir.'/filelist-'.$release.'.xml';
 $fp = fopen($outputfile,'w');
 fputs($fp, '<?xml version="1.0" encoding="UTF-8" ?>'."\n");
 fputs($fp, '<checksum_list version="'.$release.'">'."\n");
 
-fputs($fp, '<dolibarr_htdocs_dir>'."\n");
+fputs($fp, '<dolibarr_htdocs_dir includecustom="'.$includecustom.'">'."\n");
 
 $checksumconcat=array();
 
+// TODO Replace RecursiveDirectoryIterator with dol_dir_list
 $dir_iterator1 = new RecursiveDirectoryIterator(dirname(__FILE__).'/../htdocs/');
 $iterator1 = new RecursiveIteratorIterator($dir_iterator1);
-// need to ignore document custom etc
-$files = new RegexIterator($iterator1, '#^(?:[A-Z]:)?(?:/(?!(?:custom|documents|conf|install|nltechno))[^/]+)+/[^/]+\.(?:php|css|html|js|json|tpl|jpg|png|gif|sql|lang)$#i');
+// Need to ignore document custom etc. Note: this also ignore natively symbolic links.
+$files = new RegexIterator($iterator1, '#^(?:[A-Z]:)?(?:/(?!(?:'.($includecustom?'':'custom\/|').'documents\/|conf\/|install\/))[^/]+)+/[^/]+\.(?:php|css|html|js|json|tpl|jpg|png|gif|sql|lang)$#i');
 $dir='';
 $needtoclose=0;
 foreach ($files as $file) {
@@ -102,10 +122,11 @@ $checksumconcat=array();
 
 fputs($fp, '<dolibarr_script_dir version="'.$release.'">'."\n");
 
+// TODO Replace RecursiveDirectoryIterator with dol_dir_list
 $dir_iterator2 = new RecursiveDirectoryIterator(dirname(__FILE__).'/../scripts/');
 $iterator2 = new RecursiveIteratorIterator($dir_iterator2);
-// need to ignore document custom etc
-$files = new RegexIterator($iterator2, '#^(?:[A-Z]:)?(?:/(?!(?:custom|documents|conf|install|nltechno))[^/]+)+/[^/]+\.(?:php|css|html|js|json|tpl|jpg|png|gif|sql|lang)$#i');
+// Need to ignore document custom etc. Note: this also ignore natively symbolic links.
+$files = new RegexIterator($iterator2, '#^(?:[A-Z]:)?(?:/(?!(?:custom|documents|conf|install))[^/]+)+/[^/]+\.(?:php|css|html|js|json|tpl|jpg|png|gif|sql|lang)$#i');
 $dir='';
 $needtoclose=0;
 foreach ($files as $file) {
diff --git a/htdocs/admin/system/filecheck.php b/htdocs/admin/system/filecheck.php
index c0ff5d75819..7536c6df793 100644
--- a/htdocs/admin/system/filecheck.php
+++ b/htdocs/admin/system/filecheck.php
@@ -90,19 +90,22 @@ print $langs->trans("MakeIntegrityAnalysisFrom").':<br>';
 print '<!-- for a local check target=local&xmlshortfile=... -->'."\n";
 if (dol_is_file($xmlfile))
 {
-    print '<input type="radio" name="target" value="local"'.((! GETPOST('target') || GETPOST('target') == 'local') ? 'checked="checked"':'').'"> '.$langs->trans("LocalSignature").' = '.$xmlshortfile.'<br>';
+    print '<input type="radio" name="target" value="local"'.((! GETPOST('target') || GETPOST('target') == 'local') ? 'checked="checked"':'').'"> '.$langs->trans("LocalSignature").' = ';
+    print '<input name="xmlshortfile" class="flat minwidth200" value="'.dol_escape_htmltag($xmlshortfile).'">';
+    print '<br>';
 }
 else
 {
-    print '<input type="radio" name="target" value="local"> '.$langs->trans("LocalSignature").' = '.$xmlshortfile;
-    if (! GETPOST('xmlshortfile')) print ' <span class="warning">('.$langs->trans("AvailableOnlyOnPackagedVersions").')</span>';
+    print '<input type="radio" name="target" value="local"> '.$langs->trans("LocalSignature").' = ';
+    print '<input name="xmlshortfile" class="flat minwidth200" value="'.dol_escape_htmltag($xmlshortfile).'">';
+    print ' <span class="warning">('.$langs->trans("AvailableOnlyOnPackagedVersions").')</span>';
     print '<br>';
 }
 print '<!-- for a remote target=remote&xmlremote=... -->'."\n";
 if ($enableremotecheck)
 {
     print '<input type="radio" name="target" value="remote"'.(GETPOST('target') == 'remote' ? 'checked="checked"':'').'> '.$langs->trans("RemoteSignature").' = ';
-    print '<input name="xmlremote" class="flat quatrevingtpercent" value="'.$xmlremote.'"><br>';
+    print '<input name="xmlremote" class="flat quatrevingtpercent" value="'.dol_escape_htmltag($xmlremote).'"><br>';
 }
 else
 {
@@ -150,21 +153,41 @@ if (GETPOST('target') == 'remote')
 if ($xml)
 {
     $checksumconcat = array();
-
+    $file_list = array();
+    $out = '';
+    
     // Scan htdocs
     if (is_object($xml->dolibarr_htdocs_dir[0]))
     {
-        $file_list = array();
-        $ret = getFilesUpdated($file_list, $xml->dolibarr_htdocs_dir[0], '', DOL_DOCUMENT_ROOT, $checksumconcat);		// Fill array $file_list
-    
-        print_fiche_titre($langs->trans("FilesMissing"));
+        //var_dump($xml->dolibarr_htdocs_dir[0]['includecustom']);exit;
+        $includecustom=(empty($xml->dolibarr_htdocs_dir[0]['includecustom'])?0:$xml->dolibarr_htdocs_dir[0]['includecustom']);
+
+        // Defined qualified files (must be same than into generate_filelist_xml.php)
+        $regextoinclude='\.(php|css|html|js|json|tpl|jpg|png|gif|sql|lang)$';
+        $regextoexclude='('.($includecustom?'':'custom|').'documents|conf|install)$';  // Exclude dirs
+        $scanfiles = dol_dir_list(DOL_DOCUMENT_ROOT, 'files', 1, $regextoinclude, $regextoexclude);
+
+        // Fill file_list with files in signature, new files, modified files
+        $ret = getFilesUpdated($file_list, $xml->dolibarr_htdocs_dir[0], '', DOL_DOCUMENT_ROOT, $checksumconcat, $scanfiles);		// Fill array $file_list
+        // Complete with list of new files
+        foreach ($scanfiles as $keyfile => $valfile)
+        {
+            $tmprelativefilename=preg_replace('/^'.preg_quote(DOL_DOCUMENT_ROOT,'/').'/','', $valfile['fullname']);
+            if (! in_array($tmprelativefilename, $file_list['insignature']))
+            {
+                $md5newfile=@md5_file($valfile['fullname']);    // Can fails if we don't have permission to open/read file
+                $file_list['added'][]=array('filename'=>$tmprelativefilename, 'md5'=>$md5newfile);
+            }
+        }
         
-        print '<table class="noborder">';
-        print '<tr class="liste_titre">';
-        print '<td>#</td>';
-        print '<td>' . $langs->trans("Filename") . '</td>';
-        print '<td align="center">' . $langs->trans("ExpectedChecksum") . '</td>';
-        print '</tr>'."\n";
+        $out.=load_fiche_titre($langs->trans("FilesMissing"));
+        
+        $out.='<table class="noborder">';
+        $out.='<tr class="liste_titre">';
+        $out.='<td>#</td>';
+        $out.='<td>' . $langs->trans("Filename") . '</td>';
+        $out.='<td align="center">' . $langs->trans("ExpectedChecksum") . '</td>';
+        $out.='</tr>'."\n";
         $var = true;
         $tmpfilelist = dol_sort_array($file_list['missing'], 'filename');
         if (is_array($tmpfilelist) && count($tmpfilelist))
@@ -174,32 +197,32 @@ if ($xml)
 	        {
 	            $i++;
 	            $var = !$var;
-	            print '<tr ' . $bc[$var] . '>';
-	            print '<td>'.$i.'</td>' . "\n";
-	            print '<td>'.$file['filename'].'</td>' . "\n";
-	            print '<td align="center">'.$file['expectedmd5'].'</td>' . "\n";
-	            print "</tr>\n";
+	            $out.='<tr ' . $bc[$var] . '>';
+	            $out.='<td>'.$i.'</td>' . "\n";
+	            $out.='<td>'.$file['filename'].'</td>' . "\n";
+	            $out.='<td align="center">'.$file['expectedmd5'].'</td>' . "\n";
+	            $out.="</tr>\n";
 	        }
         }
         else 
         {
-            print '<tr ' . $bc[false] . '><td colspan="3" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
+            $out.='<tr ' . $bc[false] . '><td colspan="3" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
         }            
-        print '</table>';
+        $out.='</table>';
 
-        print '<br>';
+        $out.='<br>';
 
-        print_fiche_titre($langs->trans("FilesUpdated"));
+        $out.=load_fiche_titre($langs->trans("FilesModified"));
         
-        print '<table class="noborder">';
-        print '<tr class="liste_titre">';
-        print '<td>#</td>';
-        print '<td>' . $langs->trans("Filename") . '</td>';
-        print '<td align="center">' . $langs->trans("ExpectedChecksum") . '</td>';
-        print '<td align="center">' . $langs->trans("CurrentChecksum") . '</td>';
-        print '<td align="right">' . $langs->trans("Size") . '</td>';
-        print '<td align="right">' . $langs->trans("DateModification") . '</td>';
-        print '</tr>'."\n";
+        $out.='<table class="noborder">';
+        $out.='<tr class="liste_titre">';
+        $out.='<td>#</td>';
+        $out.='<td>' . $langs->trans("Filename") . '</td>';
+        $out.='<td align="center">' . $langs->trans("ExpectedChecksum") . '</td>';
+        $out.='<td align="center">' . $langs->trans("CurrentChecksum") . '</td>';
+        $out.='<td align="right">' . $langs->trans("Size") . '</td>';
+        $out.='<td align="right">' . $langs->trans("DateModification") . '</td>';
+        $out.='</tr>'."\n";
         $var = true;
         $tmpfilelist2 = dol_sort_array($file_list['updated'], 'filename');
         if (is_array($tmpfilelist2) && count($tmpfilelist2))
@@ -209,30 +232,70 @@ if ($xml)
 	        {
 	            $i++;
 	            $var = !$var;
-	            print '<tr ' . $bc[$var] . '>';
-	            print '<td>'.$i.'</td>' . "\n";
-	            print '<td>'.$file['filename'].'</td>' . "\n";
-	            print '<td align="center">'.$file['expectedmd5'].'</td>' . "\n";
-	            print '<td align="center">'.$file['md5'].'</td>' . "\n";
-	            print '<td align="right">'.dol_print_size(dol_filesize(DOL_DOCUMENT_ROOT.'/'.$file['filename'])).'</td>' . "\n";
-	            print '<td align="right">'.dol_print_date(dol_filemtime(DOL_DOCUMENT_ROOT.'/'.$file['filename']),'dayhour').'</td>' . "\n";
-	            print "</tr>\n";
+	            $out.='<tr ' . $bc[$var] . '>';
+	            $out.='<td>'.$i.'</td>' . "\n";
+	            $out.='<td>'.$file['filename'].'</td>' . "\n";
+	            $out.='<td align="center">'.$file['expectedmd5'].'</td>' . "\n";
+	            $out.='<td align="center">'.$file['md5'].'</td>' . "\n";
+	            $out.='<td align="right">'.dol_print_size(dol_filesize(DOL_DOCUMENT_ROOT.'/'.$file['filename'])).'</td>' . "\n";
+	            $out.='<td align="right">'.dol_print_date(dol_filemtime(DOL_DOCUMENT_ROOT.'/'.$file['filename']),'dayhour').'</td>' . "\n";
+	            $out.="</tr>\n";
 	        }
         }
         else 
         {
-            print '<tr ' . $bc[false] . '><td colspan="5" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
+            $out.='<tr ' . $bc[false] . '><td colspan="5" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
         }
-        print '</table>';
+        $out.='</table>';
+        
+        $out.='<br>';
         
-        if (empty($tmpfilelist) && empty($tmpfilelist2))
+        $out.=load_fiche_titre($langs->trans("FilesAdded"));
+        
+        $out.='<table class="noborder">';
+        $out.='<tr class="liste_titre">';
+        $out.='<td>#</td>';
+        $out.='<td>' . $langs->trans("Filename") . '</td>';
+        $out.='<td align="center">' . $langs->trans("ExpectedChecksum") . '</td>';
+        $out.='<td align="center">' . $langs->trans("CurrentChecksum") . '</td>';
+        $out.='<td align="right">' . $langs->trans("Size") . '</td>';
+        $out.='<td align="right">' . $langs->trans("DateModification") . '</td>';
+        $out.='</tr>'."\n";
+        $var = true;
+        $tmpfilelist3 = dol_sort_array($file_list['added'], 'filename');
+        if (is_array($tmpfilelist3) && count($tmpfilelist3))
+        {
+            $i = 0;
+            foreach ($tmpfilelist3 as $file)
+            {
+                $i++;
+                $var = !$var;
+                $out.='<tr ' . $bc[$var] . '>';
+                $out.='<td>'.$i.'</td>' . "\n";
+                $out.='<td>'.$file['filename'].'</td>' . "\n";
+                $out.='<td align="center">'.$file['expectedmd5'].'</td>' . "\n";
+                $out.='<td align="center">'.$file['md5'].'</td>' . "\n";
+                $out.='<td align="right">'.dol_print_size(dol_filesize(DOL_DOCUMENT_ROOT.'/'.$file['filename'])).'</td>' . "\n";
+                $out.='<td align="right">'.dol_print_date(dol_filemtime(DOL_DOCUMENT_ROOT.'/'.$file['filename']),'dayhour').'</td>' . "\n";
+                $out.="</tr>\n";
+            }
+        }
+        else
+        {
+            $out.='<tr ' . $bc[false] . '><td colspan="5" class="opacitymedium">'.$langs->trans("None").'</td></tr>';
+        }
+        $out.='</table>';
+        
+     
+        // Show warning
+        if (empty($tmpfilelist) && empty($tmpfilelist2) && empty($tmpfilelist3))
         {
             setEventMessage($langs->trans("FileIntegrityIsStrictlyConformedWithReference"));
         }
         else
         {
             setEventMessage($langs->trans("FileIntegritySomeFilesWereRemovedOrModified"), 'warnings');
-        }
+        }        
     }
     else
     {
@@ -253,13 +316,30 @@ if ($xml)
     asort($checksumconcat); // Sort list of checksum        
     //var_dump($checksumconcat);
     $checksumget = md5(join(',',$checksumconcat));
-    $checksumtoget = $xml->dolibarr_htdocs_dir_checksum;
-
-    print '<br>';
+    $checksumtoget = trim((string) $xml->dolibarr_htdocs_dir_checksum);
 
+    /*var_dump(count($file_list['added']));
+    var_dump($checksumget);
+    var_dump($checksumtoget);
+    var_dump($checksumget == $checksumtoget);*/
     print_fiche_titre($langs->trans("GlobalChecksum")).'<br>';
     print $langs->trans("ExpectedChecksum").' = '. ($checksumtoget ? $checksumtoget : $langs->trans("Unknown")) .'<br>';
-    print $langs->trans("CurrentChecksum").' = '.$checksumget;
+    print $langs->trans("CurrentChecksum").' = ';
+    if ($checksumget == $checksumtoget)
+    {
+        if (count($file_list['added'])) print $checksumget.' - <span class="warning">'.$langs->trans("FileIntegrityIsOkButFilesWereAdded").'</span>';
+        else print '<span class="ok">'.$checksumget.'</span>';
+    }
+    else
+    {
+        print '<span class="error">'.$checksumget.'</span>';
+    }
+    
+    print '<br>';
+    print '<br>';
+    
+    // Output detail
+    print $out;
 }
 
 
@@ -287,10 +367,11 @@ function getFilesUpdated(&$file_list, SimpleXMLElement $dir, $path = '', $pathre
 {
     $exclude = 'install';
 
-    foreach ($dir->md5file as $file)
+    foreach ($dir->md5file as $file)    // $file is a simpleXMLElement
     {
         $filename = $path.$file['name'];
-
+        $file_list['insignature'][] = $filename;
+        
         //if (preg_match('#'.$exclude.'#', $filename)) continue;
 
         if (!file_exists($pathref.'/'.$filename))
@@ -309,3 +390,4 @@ function getFilesUpdated(&$file_list, SimpleXMLElement $dir, $path = '', $pathre
 
     return $file_list;
 }
+
diff --git a/htdocs/core/lib/files.lib.php b/htdocs/core/lib/files.lib.php
index c1aadd3df6d..1f7974775b5 100644
--- a/htdocs/core/lib/files.lib.php
+++ b/htdocs/core/lib/files.lib.php
@@ -44,8 +44,8 @@ function dol_basename($pathfile)
  *  @param	string		$path        	Starting path from which to search. This is a full path.
  *  @param	string		$types        	Can be "directories", "files", or "all"
  *  @param	int			$recursive		Determines whether subdirectories are searched
- *  @param	string		$filter        	Regex filter to restrict list. This regex value must be escaped for '/', since this char is used for preg_match function
- *  @param	array		$excludefilter  Array of Regex for exclude filter (example: array('(\.meta|_preview\.png)$','^\.'))
+ *  @param	string		$filter        	Regex filter to restrict list. This regex value must be escaped for '/', since this char is used for preg_match function. Filter is checked into basename only.
+ *  @param	array		$excludefilter  Array of Regex for exclude filter (example: array('(\.meta|_preview\.png)$','^\.')). Exclude is checked into fullpath.
  *  @param	string		$sortcriteria	Sort criteria ("","fullname","name","date","size")
  *  @param	string		$sortorder		Sort order (SORT_ASC, SORT_DESC)
  *	@param	int			$mode			0=Return array minimum keys loaded (faster), 1=Force all keys like date and size to be loaded (slower), 2=Force load of date only, 3=Force load of size only
@@ -104,7 +104,7 @@ function dol_dir_list($path, $types="all", $recursive=0, $filter="", $excludefil
 			$filesize='';
 			$file_list = array();
 
-			while (false !== ($file = readdir($dir)))
+			while (false !== ($file = readdir($dir)))        // $file is always a basename (into directory $newpath)
 			{
 				if (! utf8_check($file)) $file=utf8_encode($file);	// To be sure data is stored in utf8 in memory
 
@@ -137,7 +137,7 @@ function dol_dir_list($path, $types="all", $recursive=0, $filter="", $excludefil
 							if ($loaddate || $sortcriteria == 'date') $filedate=dol_filemtime($path."/".$file);
 							if ($loadsize || $sortcriteria == 'size') $filesize=dol_filesize($path."/".$file);
 
-							if (! $filter || preg_match('/'.$filter.'/i',$file))	// We do not search key $filter into $path, only into $file
+							if (! $filter || preg_match('/'.$filter.'/i',$file))	// We do not search key $filter into all $path, only into $file part
 							{
 								preg_match('/([^\/]+)\/[^\/]+$/',$path.'/'.$file,$reg);
 								$level1name=(isset($reg[1])?$reg[1]:'');
diff --git a/htdocs/langs/en_US/admin.lang b/htdocs/langs/en_US/admin.lang
index 202618b7b43..defcafa7b65 100644
--- a/htdocs/langs/en_US/admin.lang
+++ b/htdocs/langs/en_US/admin.lang
@@ -11,13 +11,16 @@ VersionRecommanded=Recommended
 FileCheck=Files integrity checker
 FileCheckDesc=This tool allows you to check the integrity of files of your application, comparing each files with the official ones. You can use this tool to detect if some files were modified by a hacker for example.
 FileIntegrityIsStrictlyConformedWithReference=Files integrity is strictly conformed with the reference.
-FileIntegritySomeFilesWereRemovedOrModified=Files integrity check has failed. Some files were modified of removed.
+FileIntegrityIsOkButFilesWereAdded=Files integrity check has passed, however some new files were added.
+FileIntegritySomeFilesWereRemovedOrModified=Files integrity check has failed. Some files were modified, removed or added.
 GlobalChecksum=Global checksum
 MakeIntegrityAnalysisFrom=Make integrity analysis of application files from
 LocalSignature=Embedded local signature (less reliable)
 RemoteSignature=Remote distant signature (more reliable)
 FilesMissing=Missing Files
 FilesUpdated=Updated Files
+FilesModified=Modified Files
+FilesAdded=Added Files
 FileCheckDolibarr=Check integrity of application files
 AvailableOnlyOnPackagedVersions=The local file for integrity checking is only available when application is installed from a certified package 
 XmlNotFound=Xml Integrity File of application not found
-- 
GitLab