1<?php
2/* Copyright (C) 2016 Destailleur Laurent <eldy@users.sourceforge.net>
3 *
4 * This program is free software; you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation; either version 3 of the License, or
7 * any later version.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18/**
19 *   	\file       htdocs/core/class/utils.class.php
20 *      \ingroup    core
21 *		\brief      File for Utils class
22 */
23
24
25/**
26 *		Class to manage utility methods
27 */
28class Utils
29{
30	/**
31	 * @var DoliDB Database handler.
32	 */
33	public $db;
34
35	public $output; // Used by Cron method to return message
36	public $result; // Used by Cron method to return data
37
38	/**
39	 *	Constructor
40	 *
41	 *  @param	DoliDB	$db		Database handler
42	 */
43	public function __construct($db)
44	{
45		$this->db = $db;
46	}
47
48
49	/**
50	 *  Purge files into directory of data files.
51	 *  CAN BE A CRON TASK
52	 *
53	 *  @param	string      $choices	   Choice of purge mode ('tempfiles', '' or 'tempfilesold' to purge temp older than $nbsecondsold seconds, 'allfiles', 'logfile')
54	 *  @param  int         $nbsecondsold  Nb of seconds old to accept deletion of a directory if $choice is 'tempfilesold'
55	 *  @return	int						   0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
56	 */
57	public function purgeFiles($choices = 'tempfilesold,logfile', $nbsecondsold = 86400)
58	{
59		global $conf, $langs, $dolibarr_main_data_root;
60
61		$langs->load("admin");
62
63		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
64
65		if (empty($choices)) $choices = 'tempfilesold,logfile';
66
67		dol_syslog("Utils::purgeFiles choice=".$choices, LOG_DEBUG);
68
69		$count = 0;
70		$countdeleted = 0;
71		$counterror = 0;
72
73		$choicesarray = explode(',', $choices);
74		foreach ($choicesarray as $choice) {
75			$filesarray = array();
76
77			if ($choice == 'tempfiles' || $choice == 'tempfilesold')
78			{
79				// Delete temporary files
80				if ($dolibarr_main_data_root)
81				{
82					$filesarray = dol_dir_list($dolibarr_main_data_root, "directories", 1, '^temp$', '', 'name', SORT_ASC, 2, 0, '', 1); // Do not follow symlinks
83
84					if ($choice == 'tempfilesold')
85					{
86						$now = dol_now();
87						foreach ($filesarray as $key => $val)
88						{
89							if ($val['date'] > ($now - ($nbsecondsold))) unset($filesarray[$key]); // Discard temp dir not older than $nbsecondsold
90						}
91					}
92				}
93			}
94
95			if ($choice == 'allfiles')
96			{
97				// Delete all files (except install.lock, do not follow symbolic links)
98				if ($dolibarr_main_data_root)
99				{
100					$filesarray = dol_dir_list($dolibarr_main_data_root, "all", 0, '', 'install\.lock$', 'name', SORT_ASC, 0, 0, '', 1);
101				}
102			}
103
104			if ($choice == 'logfile')
105			{
106				// Define files log
107				if ($dolibarr_main_data_root)
108				{
109					$filesarray = dol_dir_list($dolibarr_main_data_root, "files", 0, '.*\.log[\.0-9]*(\.gz)?$', 'install\.lock$', 'name', SORT_ASC, 0, 0, '', 1);
110				}
111
112				$filelog = '';
113				if (!empty($conf->syslog->enabled))
114				{
115					$filelog = $conf->global->SYSLOG_FILE;
116					$filelog = preg_replace('/DOL_DATA_ROOT/i', DOL_DATA_ROOT, $filelog);
117
118					$alreadyincluded = false;
119					foreach ($filesarray as $tmpcursor)
120					{
121						if ($tmpcursor['fullname'] == $filelog) { $alreadyincluded = true; }
122					}
123					if (!$alreadyincluded) $filesarray[] = array('fullname'=>$filelog, 'type'=>'file');
124				}
125			}
126
127			if (is_array($filesarray) && count($filesarray)) {
128				foreach ($filesarray as $key => $value)
129				{
130					//print "x ".$filesarray[$key]['fullname']."-".$filesarray[$key]['type']."<br>\n";
131					if ($filesarray[$key]['type'] == 'dir') {
132						$startcount = 0;
133						$tmpcountdeleted = 0;
134
135						$result = dol_delete_dir_recursive($filesarray[$key]['fullname'], $startcount, 1, 0, $tmpcountdeleted);
136
137						if (!in_array($filesarray[$key]['fullname'], array($conf->api->dir_temp, $conf->user->dir_temp))) {		// The 2 directories $conf->api->dir_temp and $conf->user->dir_temp are recreated at end, so we do not count them
138							$count += $result;
139							$countdeleted += $tmpcountdeleted;
140						}
141					} elseif ($filesarray[$key]['type'] == 'file') {
142						// If (file that is not logfile) or (if mode is logfile)
143						if ($filesarray[$key]['fullname'] != $filelog || $choice == 'logfile')
144						{
145							$result = dol_delete_file($filesarray[$key]['fullname'], 1, 1);
146							if ($result)
147							{
148								$count++;
149								$countdeleted++;
150							} else {
151								$counterror++;
152							}
153						}
154					}
155				}
156
157				// Update cachenbofdoc
158				if (!empty($conf->ecm->enabled) && $choice == 'allfiles')
159				{
160					require_once DOL_DOCUMENT_ROOT.'/ecm/class/ecmdirectory.class.php';
161					$ecmdirstatic = new EcmDirectory($this->db);
162					$result = $ecmdirstatic->refreshcachenboffile(1);
163				}
164			}
165		}
166
167		if ($count > 0) {
168			$this->output = $langs->trans("PurgeNDirectoriesDeleted", $countdeleted);
169			if ($count > $countdeleted) $this->output .= '<br>'.$langs->trans("PurgeNDirectoriesFailed", ($count - $countdeleted));
170		} else {
171			$this->output = $langs->trans("PurgeNothingToDelete").(in_array('tempfilesold', $choicesarray) ? ' (older than 24h for temp files)' : '');
172		}
173
174		// Recreate temp dir that are not automatically recreated by core code for performance purpose, we need them
175		if (!empty($conf->api->enabled)) {
176			dol_mkdir($conf->api->dir_temp);
177		}
178		dol_mkdir($conf->user->dir_temp);
179
180		//return $count;
181		return 0; // This function can be called by cron so must return 0 if OK
182	}
183
184
185	/**
186	 *  Make a backup of database
187	 *  CAN BE A CRON TASK
188	 *
189	 *  @param	string		$compression	   'gz' or 'bz' or 'none'
190	 *  @param  string      $type              'mysql', 'postgresql', ...
191	 *  @param  int         $usedefault        1=Use default backup profile (Set this to 1 when used as cron)
192	 *  @param  string      $file              'auto' or filename to build
193	 *  @param  int         $keeplastnfiles    Keep only last n files (not used yet)
194	 *  @param	int		    $execmethod		   0=Use default method (that is 1 by default), 1=Use the PHP 'exec', 2=Use the 'popen' method
195	 *  @return	int						       0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
196	 */
197	public function dumpDatabase($compression = 'none', $type = 'auto', $usedefault = 1, $file = 'auto', $keeplastnfiles = 0, $execmethod = 0)
198	{
199		global $db, $conf, $langs, $dolibarr_main_data_root;
200		global $dolibarr_main_db_name, $dolibarr_main_db_host, $dolibarr_main_db_user, $dolibarr_main_db_port, $dolibarr_main_db_pass;
201
202		$langs->load("admin");
203
204		dol_syslog("Utils::dumpDatabase type=".$type." compression=".$compression." file=".$file, LOG_DEBUG);
205		require_once DOL_DOCUMENT_ROOT.'/core/lib/files.lib.php';
206
207		// Check compression parameter
208		if (!in_array($compression, array('none', 'gz', 'bz', 'zip')))
209		{
210			$langs->load("errors");
211			$this->error = $langs->transnoentitiesnoconv("ErrorBadValueForParameter", $compression, "Compression");
212			return -1;
213		}
214
215		// Check type parameter
216		if ($type == 'auto') $type = $this->db->type;
217		if (!in_array($type, array('postgresql', 'pgsql', 'mysql', 'mysqli', 'mysqlnobin')))
218		{
219			$langs->load("errors");
220			$this->error = $langs->transnoentitiesnoconv("ErrorBadValueForParameter", $type, "Basetype");
221			return -1;
222		}
223
224		// Check file parameter
225		if ($file == 'auto')
226		{
227			$prefix = 'dump';
228			$ext = 'sql';
229			if (in_array($type, array('mysql', 'mysqli'))) { $prefix = 'mysqldump'; $ext = 'sql'; }
230			//if ($label == 'PostgreSQL') { $prefix='pg_dump'; $ext='dump'; }
231			if (in_array($type, array('pgsql'))) { $prefix = 'pg_dump'; $ext = 'sql'; }
232			$file = $prefix.'_'.$dolibarr_main_db_name.'_'.dol_sanitizeFileName(DOL_VERSION).'_'.strftime("%Y%m%d%H%M").'.'.$ext;
233		}
234
235		$outputdir = $conf->admin->dir_output.'/backup';
236		$result = dol_mkdir($outputdir);
237		$errormsg = '';
238
239		// MYSQL
240		if ($type == 'mysql' || $type == 'mysqli')
241		{
242			$cmddump = $conf->global->SYSTEMTOOLS_MYSQLDUMP;
243
244
245			$outputfile = $outputdir.'/'.$file;
246			// for compression format, we add extension
247			$compression = $compression ? $compression : 'none';
248			if ($compression == 'gz') $outputfile .= '.gz';
249			if ($compression == 'bz') $outputfile .= '.bz2';
250			$outputerror = $outputfile.'.err';
251			dol_mkdir($conf->admin->dir_output.'/backup');
252
253			// Parameteres execution
254			$command = $cmddump;
255			$command = preg_replace('/(\$|%)/', '', $command); // We removed chars that can be used to inject vars that contains space inside path of command without seeing there is a space to bypass the escapeshellarg.
256			if (preg_match("/\s/", $command)) $command = escapeshellarg($command); // If there is spaces, we add quotes on command to be sure $command is only a program and not a program+parameters
257
258			//$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
259			$param = $dolibarr_main_db_name." -h ".$dolibarr_main_db_host;
260			$param .= " -u ".$dolibarr_main_db_user;
261			if (!empty($dolibarr_main_db_port)) $param .= " -P ".$dolibarr_main_db_port;
262			if (!GETPOST("use_transaction", "alpha"))    $param .= " -l --single-transaction";
263			if (GETPOST("disable_fk", "alpha") || $usedefault) $param .= " -K";
264			if (GETPOST("sql_compat", "alpha") && GETPOST("sql_compat", "alpha") != 'NONE') $param .= " --compatible=".escapeshellarg(GETPOST("sql_compat", "alpha"));
265			if (GETPOST("drop_database", "alpha"))        $param .= " --add-drop-database";
266			if (GETPOST("use_mysql_quick_param", "alpha"))$param .= " --quick";
267			if (GETPOST("sql_structure", "alpha") || $usedefault)
268			{
269				if (GETPOST("drop", "alpha") || $usedefault)	$param .= " --add-drop-table=TRUE";
270				else $param .= " --add-drop-table=FALSE";
271			} else {
272				$param .= " -t";
273			}
274			if (GETPOST("disable-add-locks", "alpha")) $param .= " --add-locks=FALSE";
275			if (GETPOST("sql_data", "alpha") || $usedefault)
276			{
277				$param .= " --tables";
278				if (GETPOST("showcolumns", "alpha") || $usedefault)	 $param .= " -c";
279				if (GETPOST("extended_ins", "alpha") || $usedefault) $param .= " -e";
280				else $param .= " --skip-extended-insert";
281				if (GETPOST("delayed", "alpha"))	 	 $param .= " --delayed-insert";
282				if (GETPOST("sql_ignore", "alpha"))	 $param .= " --insert-ignore";
283				if (GETPOST("hexforbinary", "alpha") || $usedefault) $param .= " --hex-blob";
284			} else {
285				$param .= " -d"; // No row information (no data)
286			}
287			$param .= " --default-character-set=utf8"; // We always save output into utf8 charset
288			$paramcrypted = $param;
289			$paramclear = $param;
290			if (!empty($dolibarr_main_db_pass))
291			{
292				$paramcrypted .= ' -p"'.preg_replace('/./i', '*', $dolibarr_main_db_pass).'"';
293				$paramclear .= ' -p"'.str_replace(array('"', '`', '$'), array('\"', '\`', '\$'), $dolibarr_main_db_pass).'"';
294			}
295
296			$handle = '';
297
298			// Start call method to execute dump
299			$fullcommandcrypted = $command." ".$paramcrypted." 2>&1";
300			$fullcommandclear = $command." ".$paramclear." 2>&1";
301			if ($compression == 'none') $handle = fopen($outputfile, 'w');
302			if ($compression == 'gz')   $handle = gzopen($outputfile, 'w');
303			if ($compression == 'bz')   $handle = bzopen($outputfile, 'w');
304
305			$ok = 0;
306			if ($handle)
307			{
308				if (!empty($conf->global->MAIN_EXEC_USE_POPEN)) $execmethod = $conf->global->MAIN_EXEC_USE_POPEN;
309				if (empty($execmethod)) $execmethod = 1;
310
311				dol_syslog("Utils::dumpDatabase execmethod=".$execmethod." command:".$fullcommandcrypted, LOG_DEBUG);
312
313				// TODO Replace with executeCLI function
314				if ($execmethod == 1)
315				{
316					$output_arr = array(); $retval = null;
317					exec($fullcommandclear, $output_arr, $retval);
318
319					if ($retval != 0)
320					{
321						$langs->load("errors");
322						dol_syslog("Datadump retval after exec=".$retval, LOG_ERR);
323						$errormsg = 'Error '.$retval;
324						$ok = 0;
325					} else {
326						$i = 0;
327						if (!empty($output_arr))
328						{
329							foreach ($output_arr as $key => $read)
330							{
331								$i++; // output line number
332								if ($i == 1 && preg_match('/Warning.*Using a password/i', $read)) continue;
333								fwrite($handle, $read.($execmethod == 2 ? '' : "\n"));
334								if (preg_match('/'.preg_quote('-- Dump completed').'/i', $read)) $ok = 1;
335								elseif (preg_match('/'.preg_quote('SET SQL_NOTES=@OLD_SQL_NOTES').'/i', $read)) $ok = 1;
336							}
337						}
338					}
339				}
340				if ($execmethod == 2)	// With this method, there is no way to get the return code, only output
341				{
342					$handlein = popen($fullcommandclear, 'r');
343					$i = 0;
344					while (!feof($handlein))
345					{
346						$i++; // output line number
347						$read = fgets($handlein);
348						// Exclude warning line we don't want
349						if ($i == 1 && preg_match('/Warning.*Using a password/i', $read)) continue;
350						fwrite($handle, $read);
351						if (preg_match('/'.preg_quote('-- Dump completed').'/i', $read)) $ok = 1;
352						elseif (preg_match('/'.preg_quote('SET SQL_NOTES=@OLD_SQL_NOTES').'/i', $read)) $ok = 1;
353					}
354					pclose($handlein);
355				}
356
357
358				if ($compression == 'none') fclose($handle);
359				if ($compression == 'gz')   gzclose($handle);
360				if ($compression == 'bz')   bzclose($handle);
361
362				if (!empty($conf->global->MAIN_UMASK))
363					@chmod($outputfile, octdec($conf->global->MAIN_UMASK));
364			} else {
365				$langs->load("errors");
366				dol_syslog("Failed to open file ".$outputfile, LOG_ERR);
367				$errormsg = $langs->trans("ErrorFailedToWriteInDir");
368			}
369
370			// Get errorstring
371			if ($compression == 'none') $handle = fopen($outputfile, 'r');
372			if ($compression == 'gz')   $handle = gzopen($outputfile, 'r');
373			if ($compression == 'bz')   $handle = bzopen($outputfile, 'r');
374			if ($handle)
375			{
376				// Get 2048 first chars of error message.
377				$errormsg = fgets($handle, 2048);
378				//$ok=0;$errormsg='';  To force error
379
380				// Close file
381				if ($compression == 'none') fclose($handle);
382				if ($compression == 'gz')   gzclose($handle);
383				if ($compression == 'bz')   bzclose($handle);
384				if ($ok && preg_match('/^-- (MySql|MariaDB)/i', $errormsg)) {	// No error
385					$errormsg = '';
386				} else {
387					// Renommer fichier sortie en fichier erreur
388					//print "$outputfile -> $outputerror";
389					@dol_delete_file($outputerror, 1, 0, 0, null, false, 0);
390					@rename($outputfile, $outputerror);
391					// Si safe_mode on et command hors du parametre exec, on a un fichier out vide donc errormsg vide
392					if (!$errormsg)
393					{
394						$langs->load("errors");
395						$errormsg = $langs->trans("ErrorFailedToRunExternalCommand");
396					}
397				}
398			}
399			// Fin execution commande
400
401			$this->output = $errormsg;
402			$this->error = $errormsg;
403			$this->result = array("commandbackuplastdone" => $command." ".$paramcrypted, "commandbackuptorun" => "");
404			//if (empty($this->output)) $this->output=$this->result['commandbackuplastdone'];
405		}
406
407		// MYSQL NO BIN
408		if ($type == 'mysqlnobin')
409		{
410			$outputfile = $outputdir.'/'.$file;
411			$outputfiletemp = $outputfile.'-TMP.sql';
412			// for compression format, we add extension
413			$compression = $compression ? $compression : 'none';
414			if ($compression == 'gz') $outputfile .= '.gz';
415			if ($compression == 'bz') $outputfile .= '.bz2';
416			$outputerror = $outputfile.'.err';
417			dol_mkdir($conf->admin->dir_output.'/backup');
418
419			if ($compression == 'gz' or $compression == 'bz')
420			{
421				$this->backupTables($outputfiletemp);
422				dol_compress_file($outputfiletemp, $outputfile, $compression);
423				unlink($outputfiletemp);
424			} else {
425				$this->backupTables($outputfile);
426			}
427
428			$this->output = "";
429			$this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => "");
430		}
431
432		// POSTGRESQL
433		if ($type == 'postgresql' || $type == 'pgsql')
434		{
435			$cmddump = $conf->global->SYSTEMTOOLS_POSTGRESQLDUMP;
436
437			$outputfile = $outputdir.'/'.$file;
438			// for compression format, we add extension
439			$compression = $compression ? $compression : 'none';
440			if ($compression == 'gz') $outputfile .= '.gz';
441			if ($compression == 'bz') $outputfile .= '.bz2';
442			$outputerror = $outputfile.'.err';
443			dol_mkdir($conf->admin->dir_output.'/backup');
444
445			// Parameteres execution
446			$command = $cmddump;
447			$command = preg_replace('/(\$|%)/', '', $command); // We removed chars that can be used to inject vars that contains space inside path of command without seeing there is a space to bypass the escapeshellarg.
448			if (preg_match("/\s/", $command)) $command = escapeshellarg($command); // If there is spaces, we add quotes on command to be sure $command is only a program and not a program+parameters
449
450			//$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
451			//$param="-F c";
452			$param = "-F p";
453			$param .= " --no-tablespaces --inserts -h ".$dolibarr_main_db_host;
454			$param .= " -U ".$dolibarr_main_db_user;
455			if (!empty($dolibarr_main_db_port)) $param .= " -p ".$dolibarr_main_db_port;
456			if (GETPOST("sql_compat") && GETPOST("sql_compat") == 'ANSI') $param .= "  --disable-dollar-quoting";
457			if (GETPOST("drop_database"))        $param .= " -c -C";
458			if (GETPOST("sql_structure"))
459			{
460				if (GETPOST("drop"))			 $param .= " --add-drop-table";
461				if (!GETPOST("sql_data"))       $param .= " -s";
462			}
463			if (GETPOST("sql_data"))
464			{
465				if (!GETPOST("sql_structure"))	 $param .= " -a";
466				if (GETPOST("showcolumns"))	     $param .= " -c";
467			}
468			$param .= ' -f "'.$outputfile.'"';
469			//if ($compression == 'none')
470			if ($compression == 'gz')   $param .= ' -Z 9';
471			//if ($compression == 'bz')
472			$paramcrypted = $param;
473			$paramclear = $param;
474			/*if (! empty($dolibarr_main_db_pass))
475			 {
476			 $paramcrypted.=" -W".preg_replace('/./i','*',$dolibarr_main_db_pass);
477			 $paramclear.=" -W".$dolibarr_main_db_pass;
478			 }*/
479			$paramcrypted .= " -w ".$dolibarr_main_db_name;
480			$paramclear .= " -w ".$dolibarr_main_db_name;
481
482			$this->output = "";
483			$this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => $command." ".$paramcrypted);
484		}
485
486		// Clean old files
487		if (!$errormsg && $keeplastnfiles > 0)
488		{
489			$tmpfiles = dol_dir_list($conf->admin->dir_output.'/backup', 'files', 0, '', '(\.err|\.old|\.sav)$', 'date', SORT_DESC);
490			$i = 0;
491			foreach ($tmpfiles as $key => $val)
492			{
493				$i++;
494				if ($i <= $keeplastnfiles) continue;
495				dol_delete_file($val['fullname'], 0, 0, 0, null, false, 0);
496			}
497		}
498
499		return ($errormsg ? -1 : 0);
500	}
501
502
503
504	/**
505	 * Execute a CLI command.
506	 *
507	 * @param 	string	$command		Command line to execute.
508	 * @param 	string	$outputfile		Output file (used only when method is 2). For exemple $conf->admin->dir_temp.'/out.tmp';
509	 * @param	int		$execmethod		0=Use default method (that is 1 by default), 1=Use the PHP 'exec', 2=Use the 'popen' method
510	 * @return	array					array('result'=>...,'output'=>...,'error'=>...). result = 0 means OK.
511	 */
512	public function executeCLI($command, $outputfile, $execmethod = 0)
513	{
514		global $conf, $langs;
515
516		$result = 0;
517		$output = '';
518		$error = '';
519
520		$command = escapeshellcmd($command);
521		$command .= " 2>&1";
522
523		if (!empty($conf->global->MAIN_EXEC_USE_POPEN)) $execmethod = $conf->global->MAIN_EXEC_USE_POPEN;
524		if (empty($execmethod)) $execmethod = 1;
525		//$execmethod=1;
526
527		dol_syslog("Utils::executeCLI execmethod=".$execmethod." system:".$command, LOG_DEBUG);
528		$output_arr = array();
529
530		if ($execmethod == 1)
531		{
532			$retval = null;
533			exec($command, $output_arr, $retval);
534			$result = $retval;
535			if ($retval != 0)
536			{
537				$langs->load("errors");
538				dol_syslog("Utils::executeCLI retval after exec=".$retval, LOG_ERR);
539				$error = 'Error '.$retval;
540			}
541		}
542		if ($execmethod == 2)	// With this method, there is no way to get the return code, only output
543		{
544			$handle = fopen($outputfile, 'w+b');
545			if ($handle)
546			{
547				dol_syslog("Utils::executeCLI run command ".$command);
548				$handlein = popen($command, 'r');
549				while (!feof($handlein))
550				{
551					$read = fgets($handlein);
552					fwrite($handle, $read);
553					$output_arr[] = $read;
554				}
555				pclose($handlein);
556				fclose($handle);
557			}
558			if (!empty($conf->global->MAIN_UMASK)) @chmod($outputfile, octdec($conf->global->MAIN_UMASK));
559		}
560
561		// Update with result
562		if (is_array($output_arr) && count($output_arr) > 0)
563		{
564			foreach ($output_arr as $val)
565			{
566				$output .= $val.($execmethod == 2 ? '' : "\n");
567			}
568		}
569
570		dol_syslog("Utils::executeCLI result=".$result." output=".$output." error=".$error, LOG_DEBUG);
571
572		return array('result'=>$result, 'output'=>$output, 'error'=>$error);
573	}
574
575	/**
576	 * Generate documentation of a Module
577	 *
578	 * @param 	string	$module		Module name
579	 * @return	int					<0 if KO, >0 if OK
580	 */
581	public function generateDoc($module)
582	{
583		global $conf, $langs, $user, $mysoc;
584		global $dirins;
585
586		$error = 0;
587
588		$modulelowercase = strtolower($module);
589		$now = dol_now();
590
591		// Dir for module
592		$dir = $dirins.'/'.$modulelowercase;
593		// Zip file to build
594		$FILENAMEDOC = '';
595
596		// Load module
597		dol_include_once($modulelowercase.'/core/modules/mod'.$module.'.class.php');
598		$class = 'mod'.$module;
599
600		if (class_exists($class))
601		{
602			try {
603				$moduleobj = new $class($this->db);
604			} catch (Exception $e)
605			{
606				$error++;
607				dol_print_error($e->getMessage());
608			}
609		} else {
610			$error++;
611			$langs->load("errors");
612			dol_print_error($langs->trans("ErrorFailedToLoadModuleDescriptorForXXX", $module));
613			exit;
614		}
615
616		$arrayversion = explode('.', $moduleobj->version, 3);
617		if (count($arrayversion))
618		{
619			$FILENAMEASCII = strtolower($module).'.asciidoc';
620			$FILENAMEDOC = strtolower($module).'.html';
621			$FILENAMEDOCPDF = strtolower($module).'.pdf';
622
623			$dirofmodule = dol_buildpath(strtolower($module), 0);
624			$dirofmoduledoc = dol_buildpath(strtolower($module), 0).'/doc';
625			$dirofmoduletmp = dol_buildpath(strtolower($module), 0).'/doc/temp';
626			$outputfiledoc = $dirofmoduledoc.'/'.$FILENAMEDOC;
627			if ($dirofmoduledoc)
628			{
629				if (!dol_is_dir($dirofmoduledoc)) dol_mkdir($dirofmoduledoc);
630				if (!dol_is_dir($dirofmoduletmp)) dol_mkdir($dirofmoduletmp);
631				if (!is_writable($dirofmoduletmp))
632				{
633					$this->error = 'Dir '.$dirofmoduletmp.' does not exists or is not writable';
634					return -1;
635				}
636
637				if (empty($conf->global->MODULEBUILDER_ASCIIDOCTOR) && empty($conf->global->MODULEBUILDER_ASCIIDOCTORPDF))
638				{
639					$this->error = 'Setup of module ModuleBuilder not complete';
640					return -1;
641				}
642
643				// Copy some files into temp directory, so instruction include::ChangeLog.md[] will works inside the asciidoc file.
644				dol_copy($dirofmodule.'/README.md', $dirofmoduletmp.'/README.md', 0, 1);
645				dol_copy($dirofmodule.'/ChangeLog.md', $dirofmoduletmp.'/ChangeLog.md', 0, 1);
646
647				// Replace into README.md and ChangeLog.md (in case they are included into documentation with tag __README__ or __CHANGELOG__)
648				$arrayreplacement = array();
649				$arrayreplacement['/^#\s.*/m'] = ''; // Remove first level of title into .md files
650				$arrayreplacement['/^#/m'] = '##'; // Add on # to increase level
651
652				dolReplaceInFile($dirofmoduletmp.'/README.md', $arrayreplacement, '', 0, 0, 1);
653				dolReplaceInFile($dirofmoduletmp.'/ChangeLog.md', $arrayreplacement, '', 0, 0, 1);
654
655
656				$destfile = $dirofmoduletmp.'/'.$FILENAMEASCII;
657
658				$fhandle = fopen($destfile, 'w+');
659				if ($fhandle)
660				{
661					$specs = dol_dir_list(dol_buildpath(strtolower($module).'/doc', 0), 'files', 1, '(\.md|\.asciidoc)$', array('\/temp\/'));
662
663					$i = 0;
664					foreach ($specs as $spec)
665					{
666						if (preg_match('/notindoc/', $spec['relativename'])) continue; // Discard file
667						if (preg_match('/example/', $spec['relativename'])) continue; // Discard file
668						if (preg_match('/disabled/', $spec['relativename'])) continue; // Discard file
669
670						$pathtofile = strtolower($module).'/doc/'.$spec['relativename'];
671						$format = 'asciidoc';
672						if (preg_match('/\.md$/i', $spec['name'])) $format = 'markdown';
673
674						$filecursor = @file_get_contents($spec['fullname']);
675						if ($filecursor)
676						{
677							fwrite($fhandle, ($i ? "\n<<<\n\n" : "").$filecursor."\n");
678						} else {
679							$this->error = 'Failed to concat content of file '.$spec['fullname'];
680							return -1;
681						}
682
683						$i++;
684					}
685
686					fclose($fhandle);
687
688					$contentreadme = file_get_contents($dirofmoduletmp.'/README.md');
689					$contentchangelog = file_get_contents($dirofmoduletmp.'/ChangeLog.md');
690
691					include DOL_DOCUMENT_ROOT.'/core/lib/parsemd.lib.php';
692
693					//var_dump($phpfileval['fullname']);
694					$arrayreplacement = array(
695						'mymodule'=>strtolower($module),
696						'MyModule'=>$module,
697						'MYMODULE'=>strtoupper($module),
698						'My module'=>$module,
699						'my module'=>$module,
700						'Mon module'=>$module,
701						'mon module'=>$module,
702						'htdocs/modulebuilder/template'=>strtolower($module),
703						'__MYCOMPANY_NAME__'=>$mysoc->name,
704						'__KEYWORDS__'=>$module,
705						'__USER_FULLNAME__'=>$user->getFullName($langs),
706						'__USER_EMAIL__'=>$user->email,
707						'__YYYY-MM-DD__'=>dol_print_date($now, 'dayrfc'),
708						'---Put here your own copyright and developer email---'=>dol_print_date($now, 'dayrfc').' '.$user->getFullName($langs).($user->email ? ' <'.$user->email.'>' : ''),
709						'__DATA_SPECIFICATION__'=>'Not yet available',
710						'__README__'=>dolMd2Asciidoc($contentreadme),
711						'__CHANGELOG__'=>dolMd2Asciidoc($contentchangelog),
712					);
713
714					dolReplaceInFile($destfile, $arrayreplacement);
715				}
716
717				// Launch doc generation
718				$currentdir = getcwd();
719				chdir($dirofmodule);
720
721				require_once DOL_DOCUMENT_ROOT.'/core/class/utils.class.php';
722				$utils = new Utils($this->db);
723
724				// Build HTML doc
725				$command = $conf->global->MODULEBUILDER_ASCIIDOCTOR.' '.$destfile.' -n -o '.$dirofmoduledoc.'/'.$FILENAMEDOC;
726				$outfile = $dirofmoduletmp.'/out.tmp';
727
728				$resarray = $utils->executeCLI($command, $outfile);
729				if ($resarray['result'] != '0')
730				{
731					$this->error = $resarray['error'].' '.$resarray['output'];
732				}
733				$result = ($resarray['result'] == 0) ? 1 : 0;
734
735				// Build PDF doc
736				$command = $conf->global->MODULEBUILDER_ASCIIDOCTORPDF.' '.$destfile.' -n -o '.$dirofmoduledoc.'/'.$FILENAMEDOCPDF;
737				$outfile = $dirofmoduletmp.'/outpdf.tmp';
738				$resarray = $utils->executeCLI($command, $outfile);
739				if ($resarray['result'] != '0')
740				{
741					$this->error = $resarray['error'].' '.$resarray['output'];
742				}
743				$result = ($resarray['result'] == 0) ? 1 : 0;
744
745				chdir($currentdir);
746			} else {
747				$result = 0;
748			}
749
750			if ($result > 0)
751			{
752				return 1;
753			} else {
754				$error++;
755				$langs->load("errors");
756				$this->error = $langs->trans("ErrorFailToGenerateFile", $outputfiledoc);
757			}
758		} else {
759			$error++;
760			$langs->load("errors");
761			$this->error = $langs->trans("ErrorCheckVersionIsDefined");
762		}
763
764		return -1;
765	}
766
767	/**
768	 * This saves syslog files and compresses older ones.
769	 * Nb of archive to keep is defined into $conf->global->SYSLOG_FILE_SAVES
770	 * CAN BE A CRON TASK
771	 *
772	 * @return	int						0 if OK, < 0 if KO
773	 */
774	public function compressSyslogs()
775	{
776		global $conf;
777
778		if (empty($conf->loghandlers['mod_syslog_file'])) { // File Syslog disabled
779			return 0;
780		}
781
782		if (!function_exists('gzopen')) {
783			$this->error = 'Support for gzopen not available in this PHP';
784			return -1;
785		}
786
787		dol_include_once('/core/lib/files.lib.php');
788
789		$nbSaves = empty($conf->global->SYSLOG_FILE_SAVES) ? 10 : intval($conf->global->SYSLOG_FILE_SAVES);
790
791		if (empty($conf->global->SYSLOG_FILE)) {
792			$mainlogdir = DOL_DATA_ROOT;
793			$mainlog = 'dolibarr.log';
794		} else {
795			$mainlogfull = str_replace('DOL_DATA_ROOT', DOL_DATA_ROOT, $conf->global->SYSLOG_FILE);
796			$mainlogdir = dirname($mainlogfull);
797			$mainlog = basename($mainlogfull);
798		}
799
800		$tabfiles = dol_dir_list(DOL_DATA_ROOT, 'files', 0, '^(dolibarr_.+|odt2pdf)\.log$'); // Also handle other log files like dolibarr_install.log
801		$tabfiles[] = array('name' => $mainlog, 'path' => $mainlogdir);
802
803		foreach ($tabfiles as $file) {
804			$logname = $file['name'];
805			$logpath = $file['path'];
806
807			if (dol_is_file($logpath.'/'.$logname) && dol_filesize($logpath.'/'.$logname) > 0)	// If log file exists and is not empty
808			{
809				// Handle already compressed files to rename them and add +1
810
811				$filter = '^'.preg_quote($logname, '/').'\.([0-9]+)\.gz$';
812
813				$gzfilestmp = dol_dir_list($logpath, 'files', 0, $filter);
814				$gzfiles = array();
815
816				foreach ($gzfilestmp as $gzfile) {
817					$tabmatches = array();
818					preg_match('/'.$filter.'/i', $gzfile['name'], $tabmatches);
819
820					$numsave = intval($tabmatches[1]);
821
822					$gzfiles[$numsave] = $gzfile;
823				}
824
825				krsort($gzfiles, SORT_NUMERIC);
826
827				foreach ($gzfiles as $numsave => $dummy) {
828					if (dol_is_file($logpath.'/'.$logname.'.'.($numsave + 1).'.gz')) {
829						return -2;
830					}
831
832					if ($numsave >= $nbSaves) {
833						dol_delete_file($logpath.'/'.$logname.'.'.$numsave.'.gz', 0, 0, 0, null, false, 0);
834					} else {
835						dol_move($logpath.'/'.$logname.'.'.$numsave.'.gz', $logpath.'/'.$logname.'.'.($numsave + 1).'.gz', 0, 1, 0, 0);
836					}
837				}
838
839				// Compress current file and recreate it
840
841				if ($nbSaves > 0) {			// If $nbSaves is 1, we keep 1 archive .gz file, If 2, we keep 2 .gz files
842					$gzfilehandle = gzopen($logpath.'/'.$logname.'.1.gz', 'wb9');
843
844					if (empty($gzfilehandle)) {
845						$this->error = 'Failted to open file '.$logpath.'/'.$logname.'.1.gz';
846						return -3;
847					}
848
849					$sourcehandle = fopen($logpath.'/'.$logname, 'r');
850
851					if (empty($sourcehandle)) {
852						$this->error = 'Failed to open file '.$logpath.'/'.$logname;
853						return -4;
854					}
855
856					while (!feof($sourcehandle)) {
857						gzwrite($gzfilehandle, fread($sourcehandle, 512 * 1024)); // Read 512 kB at a time
858					}
859
860					fclose($sourcehandle);
861					gzclose($gzfilehandle);
862
863					@chmod($logpath.'/'.$logname.'.1.gz', octdec(empty($conf->global->MAIN_UMASK) ? '0664' : $conf->global->MAIN_UMASK));
864				}
865
866				dol_delete_file($logpath.'/'.$logname, 0, 0, 0, null, false, 0);
867
868				// Create empty file
869				$newlog = fopen($logpath.'/'.$logname, 'a+');
870				fclose($newlog);
871
872				//var_dump($logpath.'/'.$logname." - ".octdec(empty($conf->global->MAIN_UMASK)?'0664':$conf->global->MAIN_UMASK));
873				@chmod($logpath.'/'.$logname, octdec(empty($conf->global->MAIN_UMASK) ? '0664' : $conf->global->MAIN_UMASK));
874			}
875		}
876
877		$this->output = 'Archive log files (keeping last SYSLOG_FILE_SAVES='.$nbSaves.' files) done.';
878		return 0;
879	}
880
881	/**	Backup the db OR just a table without mysqldump binary, with PHP only (does not require any exec permission)
882	 *	Author: David Walsh (http://davidwalsh.name/backup-mysql-database-php)
883	 *	Updated and enhanced by Stephen Larroque (lrq3000) and by the many commentators from the blog
884	 *	Note about foreign keys constraints: for Dolibarr, since there are a lot of constraints and when imported the tables will be inserted in the dumped order, not in constraints order, then we ABSOLUTELY need to use SET FOREIGN_KEY_CHECKS=0; when importing the sql dump.
885	 *	Note2: db2SQL by Howard Yeend can be an alternative, by using SHOW FIELDS FROM and SHOW KEYS FROM we could generate a more precise dump (eg: by getting the type of the field and then precisely outputting the right formatting - in quotes, numeric or null - instead of trying to guess like we are doing now).
886	 *
887	 *	@param	string	$outputfile		Output file name
888	 *	@param	string	$tables			Table name or '*' for all
889	 *	@return	int						<0 if KO, >0 if OK
890	 */
891	public function backupTables($outputfile, $tables = '*')
892	{
893		global $db, $langs;
894		global $errormsg;
895
896		// Set to UTF-8
897		if (is_a($db, 'DoliDBMysqli')) {
898			/** @var DoliDBMysqli $db */
899			$db->db->set_charset('utf8');
900		} else {
901			/** @var DoliDB $db */
902			$db->query('SET NAMES utf8');
903			$db->query('SET CHARACTER SET utf8');
904		}
905
906		//get all of the tables
907		if ($tables == '*')
908		{
909			$tables = array();
910			$result = $db->query('SHOW FULL TABLES WHERE Table_type = \'BASE TABLE\'');
911			while ($row = $db->fetch_row($result))
912			{
913				$tables[] = $row[0];
914			}
915		} else {
916			$tables = is_array($tables) ? $tables : explode(',', $tables);
917		}
918
919		//cycle through
920		$handle = fopen($outputfile, 'w+');
921		if (fwrite($handle, '') === false)
922		{
923			$langs->load("errors");
924			dol_syslog("Failed to open file ".$outputfile, LOG_ERR);
925			$errormsg = $langs->trans("ErrorFailedToWriteInDir");
926			return -1;
927		}
928
929		// Print headers and global mysql config vars
930		$sqlhead = '';
931		$sqlhead .= "-- ".$db::LABEL." dump via php with Dolibarr ".DOL_VERSION."
932--
933-- Host: ".$db->db->host_info."    Database: ".$db->database_name."
934-- ------------------------------------------------------
935-- Server version	".$db->db->server_info."
936
937/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
938/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
939/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
940/*!40101 SET NAMES utf8 */;
941/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
942/*!40103 SET TIME_ZONE='+00:00' */;
943/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
944/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
945/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
946/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
947
948";
949
950		if (GETPOST("nobin_disable_fk")) $sqlhead .= "SET FOREIGN_KEY_CHECKS=0;\n";
951		//$sqlhead .= "SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";\n";
952		if (GETPOST("nobin_use_transaction")) $sqlhead .= "SET AUTOCOMMIT=0;\nSTART TRANSACTION;\n";
953
954		fwrite($handle, $sqlhead);
955
956		$ignore = '';
957		if (GETPOST("nobin_sql_ignore")) $ignore = 'IGNORE ';
958		$delayed = '';
959		if (GETPOST("nobin_delayed")) $delayed = 'DELAYED ';
960
961		// Process each table and print their definition + their datas
962		foreach ($tables as $table)
963		{
964			// Saving the table structure
965			fwrite($handle, "\n--\n-- Table structure for table `".$table."`\n--\n");
966
967			if (GETPOST("nobin_drop")) fwrite($handle, "DROP TABLE IF EXISTS `".$table."`;\n"); // Dropping table if exists prior to re create it
968			fwrite($handle, "/*!40101 SET @saved_cs_client     = @@character_set_client */;\n");
969			fwrite($handle, "/*!40101 SET character_set_client = utf8 */;\n");
970			$resqldrop = $db->query('SHOW CREATE TABLE '.$table);
971			$row2 = $db->fetch_row($resqldrop);
972			if (empty($row2[1]))
973			{
974				fwrite($handle, "\n-- WARNING: Show create table ".$table." return empy string when it should not.\n");
975			} else {
976				fwrite($handle, $row2[1].";\n");
977				//fwrite($handle,"/*!40101 SET character_set_client = @saved_cs_client */;\n\n");
978
979				// Dumping the data (locking the table and disabling the keys check while doing the process)
980				fwrite($handle, "\n--\n-- Dumping data for table `".$table."`\n--\n");
981				if (!GETPOST("nobin_nolocks")) fwrite($handle, "LOCK TABLES `".$table."` WRITE;\n"); // Lock the table before inserting data (when the data will be imported back)
982				if (GETPOST("nobin_disable_fk")) fwrite($handle, "ALTER TABLE `".$table."` DISABLE KEYS;\n");
983				else fwrite($handle, "/*!40000 ALTER TABLE `".$table."` DISABLE KEYS */;\n");
984
985				$sql = 'SELECT * FROM '.$table; // Here SELECT * is allowed because we don't have definition of columns to take
986				$result = $db->query($sql);
987				while ($row = $db->fetch_row($result))
988				{
989					// For each row of data we print a line of INSERT
990					fwrite($handle, 'INSERT '.$delayed.$ignore.'INTO `'.$table.'` VALUES (');
991					$columns = count($row);
992					for ($j = 0; $j < $columns; $j++) {
993						// Processing each columns of the row to ensure that we correctly save the value (eg: add quotes for string - in fact we add quotes for everything, it's easier)
994						if ($row[$j] == null && !is_string($row[$j])) {
995							// IMPORTANT: if the field is NULL we set it NULL
996							$row[$j] = 'NULL';
997						} elseif (is_string($row[$j]) && $row[$j] == '') {
998							// if it's an empty string, we set it as an empty string
999							$row[$j] = "''";
1000						} elseif (is_numeric($row[$j]) && !strcmp($row[$j], $row[$j] + 0)) { // test if it's a numeric type and the numeric version ($nb+0) == string version (eg: if we have 01, it's probably not a number but rather a string, else it would not have any leading 0)
1001							// if it's a number, we return it as-is
1002							//	                    $row[$j] = $row[$j];
1003						} else { // else for all other cases we escape the value and put quotes around
1004							$row[$j] = addslashes($row[$j]);
1005							$row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
1006							$row[$j] = "'".$row[$j]."'";
1007						}
1008					}
1009					fwrite($handle, implode(',', $row).");\n");
1010				}
1011				if (GETPOST("nobin_disable_fk")) fwrite($handle, "ALTER TABLE `".$table."` ENABLE KEYS;\n"); // Enabling back the keys/index checking
1012				if (!GETPOST("nobin_nolocks")) fwrite($handle, "UNLOCK TABLES;\n"); // Unlocking the table
1013				fwrite($handle, "\n\n\n");
1014			}
1015		}
1016
1017		/* Backup Procedure structure*/
1018		/*
1019		 $result = $db->query('SHOW PROCEDURE STATUS');
1020		 if ($db->num_rows($result) > 0)
1021		 {
1022		 while ($row = $db->fetch_row($result)) { $procedures[] = $row[1]; }
1023		 foreach($procedures as $proc)
1024		 {
1025		 fwrite($handle,"DELIMITER $$\n\n");
1026		 fwrite($handle,"DROP PROCEDURE IF EXISTS '$name'.'$proc'$$\n");
1027		 $resqlcreateproc=$db->query("SHOW CREATE PROCEDURE '$proc'");
1028		 $row2 = $db->fetch_row($resqlcreateproc);
1029		 fwrite($handle,"\n".$row2[2]."$$\n\n");
1030		 fwrite($handle,"DELIMITER ;\n\n");
1031		 }
1032		 }
1033		 */
1034		/* Backup Procedure structure*/
1035
1036		// Write the footer (restore the previous database settings)
1037		$sqlfooter = "\n\n";
1038		if (GETPOST("nobin_use_transaction")) $sqlfooter .= "COMMIT;\n";
1039		if (GETPOST("nobin_disable_fk")) $sqlfooter .= "SET FOREIGN_KEY_CHECKS=1;\n";
1040		$sqlfooter .= "\n\n-- Dump completed on ".date('Y-m-d G-i-s');
1041		fwrite($handle, $sqlfooter);
1042
1043		fclose($handle);
1044
1045		return 1;
1046	}
1047}
1048