From 52b5a8ece787e4d0c6d5ae893819777fdfe4368a Mon Sep 17 00:00:00 2001
From: Laurent Destailleur <eldy@destailleur.fr>
Date: Sun, 21 May 2017 14:06:43 +0200
Subject: [PATCH] More powerfull dol_move function.

---
 htdocs/api/class/api_documents.class.php |  26 +--
 htdocs/core/class/antivir.class.php      |   7 +
 htdocs/core/lib/files.lib.php            | 216 ++++++++++++++---------
 test/phpunit/RestAPIDocumentTest.php     |   2 +-
 4 files changed, 160 insertions(+), 91 deletions(-)

diff --git a/htdocs/api/class/api_documents.class.php b/htdocs/api/class/api_documents.class.php
index 18ccee58b07..9794dad4a12 100644
--- a/htdocs/api/class/api_documents.class.php
+++ b/htdocs/api/class/api_documents.class.php
@@ -65,8 +65,8 @@ class Documents extends DolibarrApi
 
     /**
      * Push a file. 
-     * Test sample 1: { "filename": "mynewfile.txt", "modulepart": "facture", "ref": "FA1701-001", "subdir": "", "filecontent": "content text", "fileencoding": "" }.
-     * Test sample 2: { "filename": "mynewfile.txt", "modulepart": "medias", "ref": "", "subdir": "mysubdir1/mysubdir2", "filecontent": "content text", "fileencoding": "" }.
+     * Test sample 1: { "filename": "mynewfile.txt", "modulepart": "facture", "ref": "FA1701-001", "subdir": "", "filecontent": "content text", "fileencoding": "", "overwriteifexists": "0" }.
+     * Test sample 2: { "filename": "mynewfile.txt", "modulepart": "medias", "ref": "", "subdir": "mysubdir1/mysubdir2", "filecontent": "content text", "fileencoding": "", "overwriteifexists": "0" }.
      *
      * @param   string  $filename           Name of file to create ('FA1705-0123')
      * @param   string  $modulepart         Name of module or area concerned by file upload ('facture', ...)
@@ -74,10 +74,12 @@ class Documents extends DolibarrApi
      * @param   string  $subdir             Subdirectory (Only if ref not provided)
      * @param   string  $filecontent        File content (string with file content. An empty file will be created if this parameter is not provided)
      * @param   string  $fileencoding       File encoding (''=no encoding, 'base64'=Base 64)
+     * @param   int 	$overwriteifexists  Overwrite file if exists (1 by default)
      * @return  bool     				    State of copy
      * @throws RestException
      */
-    public function post($filename, $modulepart, $ref='', $subdir='', $filecontent='', $fileencoding='') {
+    public function post($filename, $modulepart, $ref='', $subdir='', $filecontent='', $fileencoding='', $overwriteifexists=0)
+    {
         global $db, $conf;
         
         /*var_dump($modulepart);
@@ -138,30 +140,34 @@ class Documents extends DolibarrApi
 		
 		$upload_dir = dol_sanitizePathName($upload_dir);
 		
-        $destfile = $upload_dir . '/' . $original_file;
-
+		$destfile = $upload_dir . '/' . $original_file;
+		$destfiletmp = DOL_DATA_ROOT.'/admin/temp/' . $original_file;
+		dol_delete_file($destfiletmp);
+		
         if (!dol_is_dir($upload_dir)) {
             throw new RestException(401,'Directory not exists : '.$upload_dir);
         }
 
-        if (dol_is_file($destfile))
+        if (! $overwriteifexists && dol_is_file($destfile))
         {
             throw new RestException(500, "File with name '".$original_file."' already exists.");
         }
         
-        $fhandle = fopen($destfile, 'w');
+        $fhandle = @fopen($destfiletmp, 'w');
         if ($fhandle)
         {
             $nbofbyteswrote = fwrite($fhandle, $newfilecontent);
             fclose($fhandle);
-            @chmod($destfile, octdec($conf->global->MAIN_UMASK));
+            @chmod($destfiletmp, octdec($conf->global->MAIN_UMASK));
         }
         else
         {
-            throw new RestException(500, 'Failed to open file for write');
+            throw new RestException(500, "Failed to open file '".$destfiletmp."' for write");
         }
         
-        return true;
+        $result = dol_move($destfiletmp, $destfile, 0, $overwriteifexists, 1);
+        
+        return $result;
     }
 
     /**
diff --git a/htdocs/core/class/antivir.class.php b/htdocs/core/class/antivir.class.php
index d7f765fa510..357f184eea8 100644
--- a/htdocs/core/class/antivir.class.php
+++ b/htdocs/core/class/antivir.class.php
@@ -49,6 +49,7 @@ class AntiVir
 	/**
 	 *	Scan a file with antivirus.
 	 *  This function runs the command defined in setup. This antivirus command must return 0 if OK.
+	 *  Return also true (virus found) if file end with '.virus' (so we can make test safely).
 	 *
 	 *	@param	string	$file		File to scan
 	 *	@return	int					<0 if KO (-98 if error, -99 if virus), 0 if OK
@@ -59,6 +60,12 @@ class AntiVir
 
 		$return = 0;
 
+		if (preg_match('/\.virus$/i', $file))
+		{
+		    $this->errors='File has an extension saying file is a virus';
+		    return -97;		    
+		}
+		
 		$fullcommand=$this->getCliCommand($file);
 		//$fullcommand='"c:\Program Files (x86)\ClamWin\bin\clamscan.exe" --database="C:\Program Files (x86)\ClamWin\lib" "c:\temp\aaa.txt"';
         $fullcommand.=' 2>&1';      // This is to get error output
diff --git a/htdocs/core/lib/files.lib.php b/htdocs/core/lib/files.lib.php
index 437811d8e98..008f4c15e3c 100644
--- a/htdocs/core/lib/files.lib.php
+++ b/htdocs/core/lib/files.lib.php
@@ -648,15 +648,19 @@ function dolCopyDir($srcfile, $destfile, $newmask, $overwriteifexists)
 
 /**
  * Move a file into another name.
- * This function differs from dol_move_uploaded_file, because it can be called in any context.
+ * Note: 
+ *  - This function differs from dol_move_uploaded_file, because it can be called in any context.
+ *  - Database of files is updated.
+ *  - Test on antivirus is done only if param testvirus is provided and an antivirus was set. 
  *
  * @param	string  $srcfile            Source file (can't be a directory. use native php @rename() to move a directory)
  * @param   string	$destfile           Destination file (can't be a directory. use native php @rename() to move a directory)
  * @param   integer	$newmask            Mask in octal string for new file (0 by default means $conf->global->MAIN_UMASK)
  * @param   int		$overwriteifexists  Overwrite file if exists (1 by default)
  * @return  boolean 		            True if OK, false if KO
+ * @see dol_move_uploaded_file
  */
-function dol_move($srcfile, $destfile, $newmask=0, $overwriteifexists=1)
+function dol_move($srcfile, $destfile, $newmask=0, $overwriteifexists=1, $testvirus=0)
 {
     global $user, $db, $conf;
     $result=false;
@@ -676,6 +680,18 @@ function dol_move($srcfile, $destfile, $newmask=0, $overwriteifexists=1)
         $newpathofsrcfile=dol_osencode($srcfile);
         $newpathofdestfile=dol_osencode($destfile);
 
+        // Check virus
+        $testvirusarray=array();
+        if ($testvirus)
+        {
+            $testvirusarray=dolCheckVirus($newpathofsrcfile);
+            if (count($testvirusarray)) 
+            {
+                dol_syslog("files.lib.php::dol_move canceled because a virus was found into source file. we ignore the move request.", LOG_WARNING);
+                return false;
+            }
+        }
+
         $result=@rename($newpathofsrcfile, $newpathofdestfile); // To see errors, remove @
         if (! $result)
         {
@@ -703,9 +719,17 @@ function dol_move($srcfile, $destfile, $newmask=0, $overwriteifexists=1)
 
                 dol_syslog("Try to rename also entries in database for full relative path before = ".$rel_filetorenamebefore." after = ".$rel_filetorenameafter, LOG_DEBUG);
                 include_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmfiles.class.php';
+                
+                $ecmfiletarget=new EcmFiles($db);
+                $resultecmtarget = $ecmfiletarget->fetch(0, '', $rel_filetorenameafter);
+                if ($resultecmtarget > 0)   // An entry for target name already exists for target, we delete it, a new one will be created.
+                {
+                    $ecmfiletarget->delete($user);
+                }
+                
                 $ecmfile=new EcmFiles($db);
-                $result = $ecmfile->fetch(0, '', $rel_filetorenamebefore);
-                if ($result > 0)   // If found
+                $resultecm = $ecmfile->fetch(0, '', $rel_filetorenamebefore);
+                if ($resultecm > 0)   // If an entry was found for src file, we use it to move entry
                 {
                     $filename = basename($rel_filetorenameafter);
                     $rel_dir = dirname($rel_filetorenameafter);
@@ -714,9 +738,9 @@ function dol_move($srcfile, $destfile, $newmask=0, $overwriteifexists=1)
                     
                     $ecmfile->filepath = $rel_dir;
                     $ecmfile->filename = $filename;
-                    $result = $ecmfile->update($user);
+                    $resultecm = $ecmfile->update($user);
                 }
-                elseif ($result == 0)   // If not found
+                elseif ($resultecm == 0)   // If no entry were found for src files, create/update target file
                 {
                     $filename = basename($rel_filetorenameafter);
                     $rel_dir = dirname($rel_filetorenameafter);
@@ -730,16 +754,19 @@ function dol_move($srcfile, $destfile, $newmask=0, $overwriteifexists=1)
                     $ecmfile->gen_or_uploaded = 'unknown';
                     $ecmfile->description = '';    // indexed content
                     $ecmfile->keyword = '';        // keyword content
-                    $result = $ecmfile->create($user);
-                    if ($result < 0)
+                    $resultecm = $ecmfile->create($user);
+                    if ($resultecm < 0)
                     {
                         setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
-                    }                    
+                    }
                 }
-                elseif ($result < 0)
+                elseif ($resultecm < 0)
                 {
                     setEventMessages($ecmfile->error, $ecmfile->errors, 'warnings');
                 }
+                
+                if ($resultecm > 0) $result=true;
+                else $result = false;
             }        
         }
         
@@ -769,10 +796,41 @@ function dol_unescapefile($filename)
 	return trim(basename($filename), ".\x00..\x20");
 }
 
+
+/**
+ * Check virus into a file
+ * 
+ * @param   string      $src_file       Source file to check
+ * @return  array                       Array of errors or empty array if not virus found
+ */
+function dolCheckVirus($src_file)
+{
+    global $conf;
+    
+    if (! empty($conf->global->MAIN_ANTIVIRUS_COMMAND))
+    {
+        if (! class_exists('AntiVir')) {
+            require_once DOL_DOCUMENT_ROOT.'/core/class/antivir.class.php';
+        }
+        $antivir=new AntiVir($db);
+        $result = $antivir->dol_avscan_file($src_file);
+        if ($result < 0)	// If virus or error, we stop here
+        {
+            $reterrors=$antivir->errors;
+            return $reterrors;
+        }
+    }
+    return array();
+}
+
+
 /**
  *	Make control on an uploaded file from an GUI page and move it to final destination.
  * 	If there is errors (virus found, antivir in error, bad filename), file is not moved.
- *  Note: This function can be used only into a HTML page context. Use dol_move if you are outside.
+ *  Note: 
+ *  - This function can be used only into a HTML page context. Use dol_move if you are outside.
+ *  - Database of files is not updated.
+ *  - Test on antivirus is always done (if antivirus set).
  *
  *	@param	string	$src_file			Source full path filename ($_FILES['field']['tmp_name'])
  *	@param	string	$dest_file			Target full path filename  ($_FILES['field']['name'])
@@ -794,80 +852,78 @@ function dol_move_uploaded_file($src_file, $dest_file, $allowoverwrite, $disable
 
 	if (empty($nohook))
 	{
-		// If an upload error has been reported
-		if ($uploaderrorcode)
-		{
-			switch($uploaderrorcode)
-			{
-				case UPLOAD_ERR_INI_SIZE:	// 1
-					return 'ErrorFileSizeTooLarge';
-					break;
-				case UPLOAD_ERR_FORM_SIZE:	// 2
-					return 'ErrorFileSizeTooLarge';
-					break;
-				case UPLOAD_ERR_PARTIAL:	// 3
-					return 'ErrorPartialFile';
-					break;
-				case UPLOAD_ERR_NO_TMP_DIR:	//
-					return 'ErrorNoTmpDir';
-					break;
-				case UPLOAD_ERR_CANT_WRITE:
-					return 'ErrorFailedToWriteInDir';
-					break;
-				case UPLOAD_ERR_EXTENSION:
-					return 'ErrorUploadBlockedByAddon';
-					break;
-				default:
-					break;
-			}
-		}
-
-		// If we need to make a virus scan
-		if (empty($disablevirusscan) && file_exists($src_file) && ! empty($conf->global->MAIN_ANTIVIRUS_COMMAND))
-		{
-			if (! class_exists('AntiVir')) {
-				require_once DOL_DOCUMENT_ROOT.'/core/class/antivir.class.php';
-			}
-			$antivir=new AntiVir($db);
-			$result = $antivir->dol_avscan_file($src_file);
-			if ($result < 0)	// If virus or error, we stop here
-			{
-				$reterrors=$antivir->errors;
-				dol_syslog('Files.lib::dol_move_uploaded_file File "'.$src_file.'" (target name "'.$dest_file.'") KO with antivirus: result='.$result.' errors='.join(',',$antivir->errors), LOG_WARNING);
-				return 'ErrorFileIsInfectedWithAVirus: '.join(',',$reterrors);
-			}
-		}
-
-		// Security:
-		// Disallow file with some extensions. We renamed them.
-		// Car si on a mis le rep documents dans un rep de la racine web (pas bien), cela permet d'executer du code a la demande.
-		if (preg_match('/\.htm|\.html|\.php|\.pl|\.cgi$/i',$dest_file) && empty($conf->global->MAIN_DOCUMENT_IS_OUTSIDE_WEBROOT_SO_NOEXE_NOT_REQUIRED))
-		{
-			$file_name.= '.noexe';
-		}
-
-		// Security:
-		// We refuse cache files/dirs, upload using .. and pipes into filenames.
-		if (preg_match('/^\./',$src_file) || preg_match('/\.\./',$src_file) || preg_match('/[<>|]/',$src_file))
-		{
-			dol_syslog("Refused to deliver file ".$src_file, LOG_WARNING);
-			return -1;
-		}
-
-		// Security:
-		// On interdit fichiers caches, remontees de repertoire ainsi que les pipe dans les noms de fichiers.
-		if (preg_match('/^\./',$dest_file) || preg_match('/\.\./',$dest_file) || preg_match('/[<>|]/',$dest_file))
-		{
-			dol_syslog("Refused to deliver file ".$dest_file, LOG_WARNING);
-			return -2;
-		}
-
 		$reshook=$hookmanager->initHooks(array('fileslib'));
 
 		$parameters=array('dest_file' => $dest_file, 'src_file' => $src_file, 'file_name' => $file_name, 'varfiles' => $varfiles, 'allowoverwrite' => $allowoverwrite);
 		$reshook=$hookmanager->executeHooks('moveUploadedFile', $parameters, $object);
 	}
-
+    
+	if (empty($reshook))
+	{
+    	// If an upload error has been reported
+    	if ($uploaderrorcode)
+    	{
+    	    switch($uploaderrorcode)
+    	    {
+    	        case UPLOAD_ERR_INI_SIZE:	// 1
+    	            return 'ErrorFileSizeTooLarge';
+    	            break;
+    	        case UPLOAD_ERR_FORM_SIZE:	// 2
+    	            return 'ErrorFileSizeTooLarge';
+    	            break;
+    	        case UPLOAD_ERR_PARTIAL:	// 3
+    	            return 'ErrorPartialFile';
+    	            break;
+    	        case UPLOAD_ERR_NO_TMP_DIR:	//
+    	            return 'ErrorNoTmpDir';
+    	            break;
+    	        case UPLOAD_ERR_CANT_WRITE:
+    	            return 'ErrorFailedToWriteInDir';
+    	            break;
+    	        case UPLOAD_ERR_EXTENSION:
+    	            return 'ErrorUploadBlockedByAddon';
+    	            break;
+    	        default:
+    	            break;
+    	    }
+    	}
+    	
+    	// If we need to make a virus scan
+    	if (empty($disablevirusscan) && file_exists($src_file))
+    	{
+    	    $checkvirusarray=dolCheckVirus($src_file);
+    	    if (count($checkvirusarray))
+    	    {
+    	       dol_syslog('Files.lib::dol_move_uploaded_file File "'.$src_file.'" (target name "'.$dest_file.'") KO with antivirus: result='.$result.' errors='.join(',',$checkvirusarray), LOG_WARNING);
+    	       return 'ErrorFileIsInfectedWithAVirus: '.join(',',$checkvirusarray);
+    	    }
+    	}
+    	
+    	// Security:
+    	// Disallow file with some extensions. We renamed them.
+    	// Car si on a mis le rep documents dans un rep de la racine web (pas bien), cela permet d'executer du code a la demande.
+    	if (preg_match('/\.htm|\.html|\.php|\.pl|\.cgi$/i',$dest_file) && empty($conf->global->MAIN_DOCUMENT_IS_OUTSIDE_WEBROOT_SO_NOEXE_NOT_REQUIRED))
+    	{
+    	    $file_name.= '.noexe';
+    	}
+    	
+    	// Security:
+    	// We refuse cache files/dirs, upload using .. and pipes into filenames.
+    	if (preg_match('/^\./',$src_file) || preg_match('/\.\./',$src_file) || preg_match('/[<>|]/',$src_file))
+    	{
+    	    dol_syslog("Refused to deliver file ".$src_file, LOG_WARNING);
+    	    return -1;
+    	}
+    	
+    	// Security:
+    	// On interdit fichiers caches, remontees de repertoire ainsi que les pipe dans les noms de fichiers.
+    	if (preg_match('/^\./',$dest_file) || preg_match('/\.\./',$dest_file) || preg_match('/[<>|]/',$dest_file))
+    	{
+    	    dol_syslog("Refused to deliver file ".$dest_file, LOG_WARNING);
+    	    return -2;
+    	}
+	}
+	
 	if ($reshook < 0)	// At least one blocking error returned by one hook
 	{
 		$errmsg = join(',', $hookmanager->errors);
diff --git a/test/phpunit/RestAPIDocumentTest.php b/test/phpunit/RestAPIDocumentTest.php
index b180586aa91..b60a7e9e833 100644
--- a/test/phpunit/RestAPIDocumentTest.php
+++ b/test/phpunit/RestAPIDocumentTest.php
@@ -172,7 +172,7 @@ class RestAPIDocumentTest extends PHPUnit_Framework_TestCase
         dol_mkdir(DOL_DATA_ROOT.'/medias/tmpphpunit/tmpphpunit2');
         
         $data = array(
-            'filename'=>"mynewfilethatwillfails.txt",
+            'filename'=>"mynewfile.txt",
             'modulepart'=>"medias",
             'ref'=>"",
             'subdir'=>"tmpphpunit/tmpphpunit2",
-- 
GitLab