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