Passed
Pull Request — dev (#6)
by Rafael
79:24 queued 24:08
created

Utils::dumpDatabase()   F

Complexity

Conditions 119
Paths > 20000

Size

Total Lines 456
Code Lines 295

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 119
eloc 295
nc 1142161491
nop 7
dl 0
loc 456
rs 0
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/* Copyright (C) 2016       Laurent Destailleur         <[email protected]>
4
 * Copyright (C) 2021		Regis Houssin		        <[email protected]>
5
 * Copyright (C) 2022		Anthony Berton		        <[email protected]>
6
 * Copyright (C) 2023-2024	William Mead		        <[email protected]>
7
 * Copyright (C) 2024		MDW							<[email protected]>
8
 * Copyright (C) 2024       Frédéric France             <[email protected]>
9
 * Copyright (C) 2024       Rafael San José             <[email protected]>
10
 *
11
 * This program is free software; you can redistribute it and/or modify
12
 * it under the terms of the GNU General Public License as published by
13
 * the Free Software Foundation; either version 3 of the License, or
14
 * any later version.
15
 *
16
 * This program is distributed in the hope that it will be useful,
17
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
18
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
19
 * GNU General Public License for more details.
20
 *
21
 * You should have received a copy of the GNU General Public License
22
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
23
 */
24
25
namespace Dolibarr\Code\Core\Classes;
26
27
use DoliDB;
28
29
/**
30
 *      \file       htdocs/core/class/utils.class.php
31
 *      \ingroup    core
32
 *      \brief      File for Utils class
33
 */
34
35
36
/**
37
 *      Class to manage utility methods
38
 */
39
class Utils
40
{
41
    /**
42
     * @var DoliDB      Database handler
43
     */
44
    public $db;
45
46
    /**
47
     * @var string      Error message
48
     * @see             $errors
49
     */
50
    public $error;
51
52
    /**
53
     * @var string[]    Array of error messages
54
     */
55
    public $errors;
56
57
    /**
58
     * @var string      Used by Cron method to return message
59
     */
60
    public $output;
61
62
    /**
63
     * @var array{commandbackuplastdone:string,commandbackuptorun:string}   Used by Cron method to return data
64
     */
65
    public $result;
66
67
    /**
68
     *  Constructor
69
     *
70
     * @param DoliDB $db Database handler
71
     */
72
    public function __construct($db)
73
    {
74
        $this->db = $db;
75
    }
76
77
78
    /**
79
     *  Purge files into directory of data files.
80
     *  CAN BE A CRON TASK
81
     *
82
     * @param string $choices Choice of purge mode ('tempfiles', 'tempfilesold' to purge temp older than $nbsecondsold seconds, 'logfiles', or mix of this). Note that 'allfiles' is also possible but very dangerous.
83
     * @param int $nbsecondsold Nb of seconds old to accept deletion of a directory if $choice is 'tempfilesold', or deletion of file if $choice is 'allfiles'
84
     * @return int                        0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
85
     */
86
    public function purgeFiles($choices = 'tempfilesold+logfiles', $nbsecondsold = 86400)
87
    {
88
        global $conf, $langs, $user;
89
        global $dolibarr_main_data_root;
90
91
        $langs->load("admin");
92
93
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
94
95
        if (empty($choices)) {
96
            $choices = 'tempfilesold+logfiles';
97
        }
98
        if ($choices == 'allfiles' && $nbsecondsold > 0) {
99
            $choices = 'allfilesold';
100
        }
101
102
        dol_syslog("Utils::purgeFiles choice=" . $choices, LOG_DEBUG);
103
104
        // For dangerous action, we check the user is admin
105
        if (in_array($choices, array('allfiles', 'allfilesold'))) {
106
            if (empty($user->admin)) {
107
                $this->output = 'Error: to erase data files, user running the batch (currently ' . $user->login . ') must be an admin user';
108
                return 1;
109
            }
110
        }
111
112
        $count = 0;
113
        $countdeleted = 0;
114
        $counterror = 0;
115
        $filelog = '';
116
117
        $choicesarray = preg_split('/[\+,]/', $choices);
118
        foreach ($choicesarray as $choice) {
119
            $now = dol_now();
120
            $filesarray = array();
121
122
            if ($choice == 'tempfiles' || $choice == 'tempfilesold') {
123
                // Delete temporary files
124
                if ($dolibarr_main_data_root) {
125
                    $filesarray = dol_dir_list($dolibarr_main_data_root, "directories", 1, '^temp$', '', 'name', SORT_ASC, 2, 0, '', 1); // Do not follow symlinks
126
127
                    if ($choice == 'tempfilesold') {
128
                        foreach ($filesarray as $key => $val) {
129
                            if ($val['date'] > ($now - ($nbsecondsold))) {
130
                                unset($filesarray[$key]); // Discard temp dir not older than $nbsecondsold
131
                            }
132
                        }
133
                    }
134
                }
135
            }
136
137
            if ($choice == 'allfiles') {
138
                // Delete all files (except .lock and .unlock files, do not follow symbolic links)
139
                if ($dolibarr_main_data_root) {
140
                    $filesarray = dol_dir_list($dolibarr_main_data_root, "all", 0, '', '(\.lock|\.unlock)$', 'name', SORT_ASC, 0, 0, '', 1);    // No need to use recursive, we will delete directory
141
                }
142
            }
143
144
            if ($choice == 'allfilesold') {
145
                // Delete all files (except .lock and .unlock files, do not follow symbolic links)
146
                if ($dolibarr_main_data_root) {
147
                    $filesarray = dol_dir_list($dolibarr_main_data_root, "files", 1, '', '(\.lock|\.unlock)$', 'name', SORT_ASC, 0, 0, '', 1, $nbsecondsold);   // No need to use recursive, we will delete directory
148
                }
149
            }
150
151
            if ($choice == 'logfile' || $choice == 'logfiles') {
152
                // Define files log
153
                if ($dolibarr_main_data_root) {
154
                    $filesarray = dol_dir_list($dolibarr_main_data_root, "files", 0, '.*\.log[\.0-9]*(\.gz)?$', '(\.lock|\.unlock)$', 'name', SORT_ASC, 0, 0, '', 1);
155
                }
156
157
                if (isModEnabled('syslog')) {
158
                    $filelog = getDolGlobalString('SYSLOG_FILE');
159
                    $filelog = preg_replace('/DOL_DATA_ROOT/i', DOL_DATA_ROOT, $filelog);
160
161
                    $alreadyincluded = false;
162
                    foreach ($filesarray as $tmpcursor) {
163
                        if ($tmpcursor['fullname'] == $filelog) {
164
                            $alreadyincluded = true;
165
                        }
166
                    }
167
                    if (!$alreadyincluded) {
168
                        $filesarray[] = array('fullname' => $filelog, 'type' => 'file');
169
                    }
170
                }
171
            }
172
173
            if (is_array($filesarray) && count($filesarray)) {
174
                foreach ($filesarray as $key => $value) {
175
                    //print "x ".$filesarray[$key]['fullname']."-".$filesarray[$key]['type']."<br>\n";
176
                    if ($filesarray[$key]['type'] == 'dir') {
177
                        $startcount = 0;
178
                        $tmpcountdeleted = 0;
179
180
                        $result = dol_delete_dir_recursive($filesarray[$key]['fullname'], $startcount, 1, 0, $tmpcountdeleted);
181
                        $excluded = [
182
                            $conf->user->dir_temp,
183
                        ];
184
                        if (isModEnabled('api')) {
185
                            $excluded[] = $conf->api->dir_temp;
186
                        }
187
                        // The 2 directories $conf->api->dir_temp and $conf->user->dir_temp are recreated at end, so we do not count them
188
                        if (!in_array($filesarray[$key]['fullname'], $excluded)) {
189
                            $count += $result;
190
                            $countdeleted += $tmpcountdeleted;
191
                        }
192
                    } elseif ($filesarray[$key]['type'] == 'file') {
193
                        if ($choice != 'allfilesold' || $filesarray[$key]['date'] < ($now - $nbsecondsold)) {
194
                            // If (file that is not logfile) or (if mode is logfile)
195
                            if ($filesarray[$key]['fullname'] != $filelog || $choice == 'logfile' || $choice == 'logfiles') {
196
                                $result = dol_delete_file($filesarray[$key]['fullname'], 1, 1);
197
                                if ($result) {
198
                                    $count++;
199
                                    $countdeleted++;
200
                                } else {
201
                                    $counterror++;
202
                                }
203
                            }
204
                        }
205
                    }
206
                }
207
208
                // Update cachenbofdoc
209
                if (isModEnabled('ecm') && $choice == 'allfiles') {
210
                    $ecmdirstatic = new EcmDirectory($this->db);
211
                    $result = $ecmdirstatic->refreshcachenboffile(1);
212
                }
213
            }
214
        }
215
216
        if ($count > 0) {
217
            $langs->load("admin");
218
            $this->output = $langs->trans("PurgeNDirectoriesDeleted", $countdeleted);
219
            if ($count > $countdeleted) {
220
                $this->output .= '<br>' . $langs->trans("PurgeNDirectoriesFailed", ($count - $countdeleted));
221
            }
222
        } else {
223
            $this->output = $langs->trans("PurgeNothingToDelete") . (in_array('tempfilesold', $choicesarray) ? ' (older than 24h for temp files)' : '');
224
        }
225
226
        // Recreate temp dir that are not automatically recreated by core code for performance purpose, we need them
227
        if (isModEnabled('api')) {
228
            dol_mkdir($conf->api->dir_temp);
229
        }
230
        dol_mkdir($conf->user->dir_temp);
231
232
        //return $count;
233
        return 0; // This function can be called by cron so must return 0 if OK
234
    }
235
236
237
    /**
238
     *  Make a backup of database
239
     *  CAN BE A CRON TASK
240
     *
241
     * @param string $compression 'gz' or 'bz' or 'none'
242
     * @param string $type 'mysql', 'postgresql', ...
243
     * @param int $usedefault 1=Use default backup profile (Set this to 1 when used as cron)
244
     * @param string $file 'auto' or filename to build
245
     * @param int $keeplastnfiles Keep only last n files (not used yet)
246
     * @param int $execmethod 0=Use default method (that is 1 by default), 1=Use the PHP 'exec' - need size of dump in memory, but low memory method is used if GETPOST('lowmemorydump') is set, 2=Use the 'popen' method (low memory method)
247
     * @param int $lowmemorydump 1=Use the low memory method. If $lowmemorydump is set, it means we want to make the compression using an external pipe instead retrieving the content of the dump in PHP memory array $output_arr and then print it into the PHP pipe open with xopen().
248
     * @return int                            0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
249
     */
250
    public function dumpDatabase($compression = 'none', $type = 'auto', $usedefault = 1, $file = 'auto', $keeplastnfiles = 0, $execmethod = 0, $lowmemorydump = 0)
251
    {
252
        global $db, $conf, $langs, $dolibarr_main_data_root;
253
        global $dolibarr_main_db_name, $dolibarr_main_db_host, $dolibarr_main_db_user, $dolibarr_main_db_port, $dolibarr_main_db_pass;
254
        global $dolibarr_main_db_character_set;
255
256
        $langs->load("admin");
257
258
        dol_syslog("Utils::dumpDatabase type=" . $type . " compression=" . $compression . " file=" . $file, LOG_DEBUG);
259
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
260
261
        // Clean data
262
        $file = dol_sanitizeFileName($file);
263
264
        // Check compression parameter
265
        if (!in_array($compression, array('none', 'gz', 'bz', 'zip', 'zstd'))) {
266
            $langs->load("errors");
267
            $this->error = $langs->transnoentitiesnoconv("ErrorBadValueForParameter", $compression, "Compression");
268
            return -1;
269
        }
270
271
        // Check type parameter
272
        if ($type == 'auto') {
273
            $type = $this->db->type;
274
        }
275
        if (!in_array($type, array('postgresql', 'pgsql', 'mysql', 'mysqli', 'mysqlnobin'))) {
276
            $langs->load("errors");
277
            $this->error = $langs->transnoentitiesnoconv("ErrorBadValueForParameter", $type, "Basetype");
278
            return -1;
279
        }
280
281
        // Check file parameter
282
        if ($file == 'auto') {
283
            $prefix = 'dump';
284
            $ext = 'sql';
285
            if (in_array($type, array('mysql', 'mysqli'))) {
286
                $prefix = 'mysqldump';
287
            }
288
            //if ($label == 'PostgreSQL') { $prefix='pg_dump'; $ext='dump'; }
289
            if (in_array($type, array('pgsql'))) {
290
                $prefix = 'pg_dump';
291
            }
292
            $file = $prefix . '_' . $dolibarr_main_db_name . '_' . dol_sanitizeFileName(DOL_VERSION) . '_' . dol_print_date(dol_now('gmt'), "dayhourlogsmall", 'tzuser') . '.' . $ext;
293
        }
294
295
        $outputdir = $conf->admin->dir_output . '/backup';
296
        $result = dol_mkdir($outputdir);
297
        $errormsg = '';
298
299
        // MYSQL
300
        if ($type == 'mysql' || $type == 'mysqli') {
301
            if (!getDolGlobalString('SYSTEMTOOLS_MYSQLDUMP')) {
302
                $cmddump = $db->getPathOfDump();
303
            } else {
304
                $cmddump = getDolGlobalString('SYSTEMTOOLS_MYSQLDUMP');
305
            }
306
            if (empty($cmddump)) {
307
                $this->error = "Failed to detect command to use for mysqldump. Try a manual backup before to set path of command.";
308
                return -1;
309
            }
310
311
            $outputfile = $outputdir . '/' . $file;
312
            // for compression format, we add extension
313
            $compression = $compression ? $compression : 'none';
314
            if ($compression == 'gz') {
315
                $outputfile .= '.gz';
316
            } elseif ($compression == 'bz') {
317
                $outputfile .= '.bz2';
318
            } elseif ($compression == 'zstd') {
319
                $outputfile .= '.zst';
320
            }
321
            $outputerror = $outputfile . '.err';
322
            dol_mkdir($conf->admin->dir_output . '/backup');
323
324
            // Parameters execution
325
            $command = $cmddump;
326
            $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.
327
            if (preg_match("/\s/", $command)) {
328
                $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
329
            }
330
331
            //$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
332
            $param = $dolibarr_main_db_name . " -h " . $dolibarr_main_db_host;
333
            $param .= " -u " . $dolibarr_main_db_user;
334
            if (!empty($dolibarr_main_db_port)) {
335
                $param .= " -P " . $dolibarr_main_db_port . " --protocol=tcp";
336
            }
337
            if (GETPOST("use_transaction", "alpha")) {
338
                $param .= " --single-transaction";
339
            }
340
            if (GETPOST("disable_fk", "alpha") || $usedefault) {
341
                $param .= " -K";
342
            }
343
            if (GETPOST("sql_compat", "alpha") && GETPOST("sql_compat", "alpha") != 'NONE') {
344
                $param .= " --compatible=" . escapeshellarg(GETPOST("sql_compat", "alpha"));
345
            }
346
            if (GETPOST("drop_database", "alpha")) {
347
                $param .= " --add-drop-database";
348
            }
349
            if (GETPOST("use_mysql_quick_param", "alpha")) {
350
                $param .= " --quick";
351
            }
352
            if (GETPOST("use_force", "alpha")) {
353
                $param .= " -f";
354
            }
355
            if (GETPOST("sql_structure", "alpha") || $usedefault) {
356
                if (GETPOST("drop", "alpha") || $usedefault) {
357
                    $param .= " --add-drop-table=TRUE";
358
                } else {
359
                    $param .= " --add-drop-table=FALSE";
360
                }
361
            } else {
362
                $param .= " -t";
363
            }
364
            if (GETPOST("disable-add-locks", "alpha")) {
365
                $param .= " --add-locks=FALSE";
366
            }
367
            if (GETPOST("sql_data", "alpha") || $usedefault) {
368
                $param .= " --tables";
369
                if (GETPOST("showcolumns", "alpha") || $usedefault) {
370
                    $param .= " -c";
371
                }
372
                if (GETPOST("extended_ins", "alpha") || $usedefault) {
373
                    $param .= " -e";
374
                } else {
375
                    $param .= " --skip-extended-insert";
376
                }
377
                if (GETPOST("delayed", "alpha")) {
378
                    $param .= " --delayed-insert";
379
                }
380
                if (GETPOST("sql_ignore", "alpha")) {
381
                    $param .= " --insert-ignore";
382
                }
383
                if (GETPOST("hexforbinary", "alpha") || $usedefault) {
384
                    $param .= " --hex-blob";
385
                }
386
            } else {
387
                $param .= " -d"; // No row information (no data)
388
            }
389
            if ($dolibarr_main_db_character_set == 'utf8mb4') {
390
                // We save output into utf8mb4 charset
391
                $param .= " --default-character-set=utf8mb4 --no-tablespaces";
392
            } else {
393
                $param .= " --default-character-set=utf8 --no-tablespaces"; // We always save output into utf8 charset
394
            }
395
            $paramcrypted = $param;
396
            $paramclear = $param;
397
            if (!empty($dolibarr_main_db_pass)) {
398
                $paramcrypted .= ' -p"' . preg_replace('/./i', '*', $dolibarr_main_db_pass) . '"';
399
                $paramclear .= ' -p"' . str_replace(array('"', '`', '$'), array('\"', '\`', '\$'), $dolibarr_main_db_pass) . '"';
400
            }
401
402
            $handle = '';
403
404
            // Start call method to execute dump
405
            $fullcommandcrypted = $command . " " . $paramcrypted . " 2>&1";
406
            $fullcommandclear = $command . " " . $paramclear . " 2>&1";
407
            if (!$lowmemorydump) {
408
                if ($compression == 'none') {
409
                    $handle = fopen($outputfile, 'w');
410
                } elseif ($compression == 'gz') {
411
                    $handle = gzopen($outputfile, 'w');
412
                } elseif ($compression == 'bz') {
413
                    $handle = bzopen($outputfile, 'w');
414
                } elseif ($compression == 'zstd') {
415
                    $handle = fopen($outputfile, 'w');
416
                }
417
            } else {
418
                if ($compression == 'none') {
419
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." > "' . dol_sanitizePathName($outputfile) . '"';
420
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." > "' . dol_sanitizePathName($outputfile) . '"';
421
                    $handle = 1;
422
                } elseif ($compression == 'gz') {
423
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | gzip > "' . dol_sanitizePathName($outputfile) . '"';
424
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | gzip > "' . dol_sanitizePathName($outputfile) . '"';
425
                    $paramcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | gzip';
426
                    $handle = 1;
427
                } elseif ($compression == 'bz') {
428
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | bzip2 > "' . dol_sanitizePathName($outputfile) . '"';
429
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | bzip2 > "' . dol_sanitizePathName($outputfile) . '"';
430
                    $paramcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | bzip2';
431
                    $handle = 1;
432
                } elseif ($compression == 'zstd') {
433
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | zstd > "' . dol_sanitizePathName($outputfile) . '"';
434
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | zstd > "' . dol_sanitizePathName($outputfile) . '"';
435
                    $paramcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | zstd';
436
                    $handle = 1;
437
                }
438
            }
439
440
            $ok = 0;
441
            if ($handle) {
442
                if (getDolGlobalString('MAIN_EXEC_USE_POPEN')) {
443
                    $execmethod = getDolGlobalString('MAIN_EXEC_USE_POPEN');
444
                }
445
                if (empty($execmethod)) {
446
                    $execmethod = 1;
447
                }
448
449
                dol_syslog("Utils::dumpDatabase execmethod=" . $execmethod . " command:" . $fullcommandcrypted, LOG_INFO);
450
451
452
                /* If value has been forced with a php_admin_value, this has no effect. Example of value: '512M' */
453
                $MemoryLimit = getDolGlobalString('MAIN_MEMORY_LIMIT_DUMP');
454
                if (!empty($MemoryLimit)) {
455
                    @ini_set('memory_limit', $MemoryLimit);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for ini_set(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

455
                    /** @scrutinizer ignore-unhandled */ @ini_set('memory_limit', $MemoryLimit);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
456
                }
457
458
459
                if ($execmethod == 1) {
460
                    $output_arr = array();
461
                    $retval = null;
462
463
                    exec($fullcommandclear, $output_arr, $retval);
464
                    // TODO Replace this exec with Utils->executeCLI() function.
465
                    // We must check that the case for $lowmemorydump works too...
466
                    //$utils = new Utils($db);
467
                    //$outputfile = $conf->admin->dir_temp.'/dump.tmp';
468
                    //$utils->executeCLI($fullcommandclear, $outputfile, 0);
469
470
                    if ($retval != 0) {
471
                        $langs->load("errors");
472
                        dol_syslog("Datadump retval after exec=" . $retval, LOG_ERR);
473
                        $errormsg = 'Error ' . $retval;
474
                        $ok = 0;
475
                    } else {
476
                        $i = 0;
477
                        if (!empty($output_arr)) {
478
                            foreach ($output_arr as $key => $read) {
479
                                $i++; // output line number
480
                                if ($i == 1 && preg_match('/Warning.*Using a password/i', $read)) {
481
                                    continue;
482
                                }
483
                                // Now check into the result file, that the file end with "-- Dump completed"
484
                                // This is possible only if $output_arr is the clear dump file, so not possible with $lowmemorydump set because file is already compressed.
485
                                if (!$lowmemorydump) {
486
                                    fwrite($handle, $read . ($execmethod == 2 ? '' : "\n"));
487
                                    if (preg_match('/' . preg_quote('-- Dump completed', '/') . '/i', $read)) {
488
                                        $ok = 1;
489
                                    } elseif (preg_match('/' . preg_quote('SET SQL_NOTES=@OLD_SQL_NOTES', '/') . '/i', $read)) {
490
                                        $ok = 1;
491
                                    }
492
                                } else {
493
                                    // If we have a result here in lowmemorydump mode, something is strange
494
                                }
495
                            }
496
                        } elseif ($lowmemorydump) {
497
                            $ok = 1;
498
                        }
499
                    }
500
                }
501
502
                if ($execmethod == 2) { // With this method, there is no way to get the return code, only output
503
                    $handlein = popen($fullcommandclear, 'r');
504
                    $i = 0;
505
                    if ($handlein) {
506
                        while (!feof($handlein)) {
507
                            $i++; // output line number
508
                            $read = fgets($handlein);
509
                            // Exclude warning line we don't want
510
                            if ($i == 1 && preg_match('/Warning.*Using a password/i', $read)) {
511
                                continue;
512
                            }
513
                            fwrite($handle, $read);
514
                            if (preg_match('/' . preg_quote('-- Dump completed') . '/i', $read)) {
515
                                $ok = 1;
516
                            } elseif (preg_match('/' . preg_quote('SET SQL_NOTES=@OLD_SQL_NOTES') . '/i', $read)) {
517
                                $ok = 1;
518
                            }
519
                        }
520
                        pclose($handlein);
521
                    }
522
                }
523
524
                if (!$lowmemorydump) {
525
                    if ($compression == 'none') {
526
                        fclose($handle);
527
                    } elseif ($compression == 'gz') {
528
                        gzclose($handle);
529
                    } elseif ($compression == 'bz') {
530
                        fclose($handle);
531
                    } elseif ($compression == 'zstd') {
532
                        fclose($handle);
533
                    }
534
                }
535
536
                dolChmod($outputfile);
537
            } else {
538
                $langs->load("errors");
539
                dol_syslog("Failed to open file " . $outputfile, LOG_ERR);
540
                $errormsg = $langs->trans("ErrorFailedToWriteInDir");
541
            }
542
543
            // Get errorstring
544
            if ($compression == 'none') {
545
                $handle = fopen($outputfile, 'r');
546
            } elseif ($compression == 'gz') {
547
                $handle = gzopen($outputfile, 'r');
548
            } elseif ($compression == 'bz') {
549
                $handle = bzopen($outputfile, 'r');
550
            } elseif ($compression == 'zstd') {
551
                $handle = fopen($outputfile, 'r');
552
            }
553
            if ($handle) {
554
                // Get 2048 first chars of error message.
555
                $errormsg = fgets($handle, 2048);
556
                //$ok=0;$errormsg='';  To force error
557
558
                // Close file
559
                if ($compression == 'none') {
560
                    fclose($handle);
561
                } elseif ($compression == 'gz') {
562
                    gzclose($handle);
563
                } elseif ($compression == 'bz') {
564
                    fclose($handle);
565
                } elseif ($compression == 'zstd') {
566
                    fclose($handle);
567
                }
568
                if ($ok && preg_match('/^-- (MySql|MariaDB)/i', $errormsg)) {   // No error
569
                    $errormsg = '';
570
                } else {
571
                    // Renommer fichier sortie en fichier erreur
572
                    //print "$outputfile -> $outputerror";
573
                    @dol_delete_file($outputerror, 1, 0, 0, null, false, 0);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for dol_delete_file(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

573
                    /** @scrutinizer ignore-unhandled */ @dol_delete_file($outputerror, 1, 0, 0, null, false, 0);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
574
                    @rename($outputfile, $outputerror);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for rename(). This can introduce security issues, and is generally not recommended. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unhandled  annotation

574
                    /** @scrutinizer ignore-unhandled */ @rename($outputfile, $outputerror);

If you suppress an error, we recommend checking for the error condition explicitly:

// For example instead of
@mkdir($dir);

// Better use
if (@mkdir($dir) === false) {
    throw new \RuntimeException('The directory '.$dir.' could not be created.');
}
Loading history...
575
                    // Si safe_mode on et command hors du parameter exec, on a un fichier out vide donc errormsg vide
576
                    if (!$errormsg) {
577
                        $langs->load("errors");
578
                        $errormsg = $langs->trans("ErrorFailedToRunExternalCommand");
579
                    }
580
                }
581
            }
582
            // Fin execution commande
583
584
            $this->output = $errormsg;
585
            $this->error = $errormsg;
586
            $this->result = array("commandbackuplastdone" => $command . " " . $paramcrypted, "commandbackuptorun" => "");
587
            //if (empty($this->output)) $this->output=$this->result['commandbackuplastdone'];
588
        }
589
590
        // MYSQL NO BIN
591
        if ($type == 'mysqlnobin') {
592
            $outputfile = $outputdir . '/' . $file;
593
            $outputfiletemp = $outputfile . '-TMP.sql';
594
            // for compression format, we add extension
595
            $compression = $compression ? $compression : 'none';
596
            if ($compression == 'gz') {
597
                $outputfile .= '.gz';
598
            }
599
            if ($compression == 'bz') {
600
                $outputfile .= '.bz2';
601
            }
602
            $outputerror = $outputfile . '.err';
603
            dol_mkdir($conf->admin->dir_output . '/backup');
604
605
            if ($compression == 'gz' or $compression == 'bz') {
606
                $this->backupTables($outputfiletemp);
607
                dol_compress_file($outputfiletemp, $outputfile, $compression);
608
                unlink($outputfiletemp);
609
            } else {
610
                $this->backupTables($outputfile);
611
            }
612
613
            $this->output = "";
614
            $this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => "");
615
        }
616
617
        // POSTGRESQL
618
        if ($type == 'postgresql' || $type == 'pgsql') {
619
            $cmddump = getDolGlobalString('SYSTEMTOOLS_POSTGRESQLDUMP');
620
621
            $outputfile = $outputdir . '/' . $file;
622
            // for compression format, we add extension
623
            $compression = $compression ? $compression : 'none';
624
            if ($compression == 'gz') {
625
                $outputfile .= '.gz';
626
            }
627
            if ($compression == 'bz') {
628
                $outputfile .= '.bz2';
629
            }
630
            $outputerror = $outputfile . '.err';
631
            dol_mkdir($conf->admin->dir_output . '/backup');
632
633
            // Parameters execution
634
            $command = $cmddump;
635
            $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.
636
            if (preg_match("/\s/", $command)) {
637
                $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
638
            }
639
640
            //$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
641
            //$param="-F c";
642
            $param = "-F p";
643
            $param .= " --no-tablespaces --inserts -h " . $dolibarr_main_db_host;
644
            $param .= " -U " . $dolibarr_main_db_user;
645
            if (!empty($dolibarr_main_db_port)) {
646
                $param .= " -p " . $dolibarr_main_db_port;
647
            }
648
            if (GETPOST("sql_compat") && GETPOST("sql_compat") == 'ANSI') {
649
                $param .= "  --disable-dollar-quoting";
650
            }
651
            if (GETPOST("drop_database")) {
652
                $param .= " -c -C";
653
            }
654
            if (GETPOST("sql_structure")) {
655
                if (GETPOST("drop")) {
656
                    $param .= " --add-drop-table";
657
                }
658
                if (!GETPOST("sql_data")) {
659
                    $param .= " -s";
660
                }
661
            }
662
            if (GETPOST("sql_data")) {
663
                if (!GETPOST("sql_structure")) {
664
                    $param .= " -a";
665
                }
666
                if (GETPOST("showcolumns")) {
667
                    $param .= " -c";
668
                }
669
            }
670
            $param .= ' -f "' . $outputfile . '"';
671
            //if ($compression == 'none')
672
            if ($compression == 'gz') {
673
                $param .= ' -Z 9';
674
            }
675
            //if ($compression == 'bz')
676
            $paramcrypted = $param;
677
            $paramclear = $param;
678
            /*if (!empty($dolibarr_main_db_pass))
679
             {
680
             $paramcrypted.=" -W".preg_replace('/./i','*',$dolibarr_main_db_pass);
681
             $paramclear.=" -W".$dolibarr_main_db_pass;
682
             }*/
683
            $paramcrypted .= " -w " . $dolibarr_main_db_name;
684
            $paramclear .= " -w " . $dolibarr_main_db_name;
685
686
            $this->output = "";
687
            $this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => $command . " " . $paramcrypted);
688
        }
689
690
        // Clean old files
691
        if (!$errormsg && $keeplastnfiles > 0) {
692
            $tmpfiles = dol_dir_list($conf->admin->dir_output . '/backup', 'files', 0, '', '(\.err|\.old|\.sav)$', 'date', SORT_DESC);
693
            $i = 0;
694
            if (is_array($tmpfiles)) {
695
                foreach ($tmpfiles as $key => $val) {
696
                    $i++;
697
                    if ($i <= $keeplastnfiles) {
698
                        continue;
699
                    }
700
                    dol_delete_file($val['fullname'], 0, 0, 0, null, false, 0);
701
                }
702
            }
703
        }
704
705
        return ($errormsg ? -1 : 0);
706
    }
707
708
    /** Backup the db OR just a table without mysqldump binary, with PHP only (does not require any exec permission)
709
     *  Author: David Walsh (http://davidwalsh.name/backup-mysql-database-php)
710
     *  Updated and enhanced by Stephen Larroque (lrq3000) and by the many commentators from the blog
711
     *  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.
712
     *  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).
713
     *
714
     * @param string $outputfile Output file name
715
     * @param string $tables Table name or '*' for all
716
     * @return int                     Return integer <0 if KO, >0 if OK
717
     */
718
    public function backupTables($outputfile, $tables = '*')
719
    {
720
        global $db, $langs;
721
        global $errormsg;
722
723
        // Set to UTF-8
724
        if (is_a($db, 'DoliDBMysqli')) {
725
            /** @var DoliDBMysqli $db */
726
            $db->db->set_charset('utf8');
727
        } else {
728
            /** @var DoliDB $db */
729
            $db->query('SET NAMES utf8');
730
            $db->query('SET CHARACTER SET utf8');
731
        }
732
733
        //get all of the tables
734
        if ($tables == '*') {
735
            $tables = array();
736
            $result = $db->query('SHOW FULL TABLES WHERE Table_type = \'BASE TABLE\'');
737
            while ($row = $db->fetch_row($result)) {
738
                $tables[] = $row[0];
739
            }
740
        } else {
741
            $tables = is_array($tables) ? $tables : explode(',', $tables);
742
        }
743
744
        //cycle through
745
        $handle = fopen($outputfile, 'w+');
746
        if (fwrite($handle, '') === false) {
747
            $langs->load("errors");
748
            dol_syslog("Failed to open file " . $outputfile, LOG_ERR);
749
            $errormsg = $langs->trans("ErrorFailedToWriteInDir");
750
            return -1;
751
        }
752
753
        // Print headers and global mysql config vars
754
        $sqlhead = '';
755
        $sqlhead .= "-- " . $db::LABEL . " dump via php with Dolibarr " . DOL_VERSION . "
756
--
757
-- Host: " . $db->db->host_info . "    Database: " . $db->database_name . "
758
-- ------------------------------------------------------
759
-- Server version	" . $db->db->server_info . "
760
761
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
762
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
763
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
764
/*!40101 SET NAMES utf8 */;
765
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
766
/*!40103 SET TIME_ZONE='+00:00' */;
767
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
768
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
769
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
770
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
771
772
";
773
774
        if (GETPOST("nobin_disable_fk")) {
775
            $sqlhead .= "SET FOREIGN_KEY_CHECKS=0;\n";
776
        }
777
        //$sqlhead .= "SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";\n";
778
        if (GETPOST("nobin_use_transaction")) {
779
            $sqlhead .= "SET AUTOCOMMIT=0;\nSTART TRANSACTION;\n";
780
        }
781
782
        fwrite($handle, $sqlhead);
783
784
        $ignore = '';
785
        if (GETPOST("nobin_sql_ignore")) {
786
            $ignore = 'IGNORE ';
787
        }
788
        $delayed = '';
789
        if (GETPOST("nobin_delayed")) {
790
            $delayed = 'DELAYED ';
791
        }
792
793
        // Process each table and print their definition + their datas
794
        foreach ($tables as $table) {
795
            // Saving the table structure
796
            fwrite($handle, "\n--\n-- Table structure for table `" . $table . "`\n--\n");
797
798
            if (GETPOST("nobin_drop")) {
799
                fwrite($handle, "DROP TABLE IF EXISTS `" . $table . "`;\n"); // Dropping table if exists prior to re create it
800
            }
801
            fwrite($handle, "/*!40101 SET @saved_cs_client     = @@character_set_client */;\n");
802
            fwrite($handle, "/*!40101 SET character_set_client = utf8 */;\n");
803
            $resqldrop = $db->query('SHOW CREATE TABLE ' . $table);
804
            $row2 = $db->fetch_row($resqldrop);
805
            if (empty($row2[1])) {
806
                fwrite($handle, "\n-- WARNING: Show create table " . $table . " return empty string when it should not.\n");
807
            } else {
808
                fwrite($handle, $row2[1] . ";\n");
809
                //fwrite($handle,"/*!40101 SET character_set_client = @saved_cs_client */;\n\n");
810
811
                // Dumping the data (locking the table and disabling the keys check while doing the process)
812
                fwrite($handle, "\n--\n-- Dumping data for table `" . $table . "`\n--\n");
813
                if (!GETPOST("nobin_nolocks")) {
814
                    fwrite($handle, "LOCK TABLES `" . $table . "` WRITE;\n"); // Lock the table before inserting data (when the data will be imported back)
815
                }
816
                if (GETPOST("nobin_disable_fk")) {
817
                    fwrite($handle, "ALTER TABLE `" . $table . "` DISABLE KEYS;\n");
818
                } else {
819
                    fwrite($handle, "/*!40000 ALTER TABLE `" . $table . "` DISABLE KEYS */;\n");
820
                }
821
822
                $sql = "SELECT * FROM " . $table; // Here SELECT * is allowed because we don't have definition of columns to take
823
                $result = $db->query($sql);
824
                while ($row = $db->fetch_row($result)) {
825
                    // For each row of data we print a line of INSERT
826
                    fwrite($handle, "INSERT " . $delayed . $ignore . "INTO " . $table . " VALUES (");
827
                    $columns = count($row);
828
                    for ($j = 0; $j < $columns; $j++) {
829
                        // 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)
830
                        if ($row[$j] == null && !is_string($row[$j])) {
831
                            // IMPORTANT: if the field is NULL we set it NULL
832
                            $row[$j] = 'NULL';
833
                        } elseif (is_string($row[$j]) && $row[$j] == '') {
834
                            // if it's an empty string, we set it as an empty string
835
                            $row[$j] = "''";
836
                        } elseif (is_numeric($row[$j]) && !strcmp((string)$row[$j], (string)((float)$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)
837
                            // if it's a number, we return it as-is
838
                            //                      $row[$j] = $row[$j];
839
                        } else { // else for all other cases we escape the value and put quotes around
840
                            $row[$j] = addslashes($row[$j]);
841
                            $row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
842
                            $row[$j] = "'" . $row[$j] . "'";
843
                        }
844
                    }
845
                    fwrite($handle, implode(',', $row) . ");\n");
846
                }
847
                if (GETPOST("nobin_disable_fk")) {
848
                    fwrite($handle, "ALTER TABLE `" . $table . "` ENABLE KEYS;\n"); // Enabling back the keys/index checking
849
                }
850
                if (!GETPOST("nobin_nolocks")) {
851
                    fwrite($handle, "UNLOCK TABLES;\n"); // Unlocking the table
852
                }
853
                fwrite($handle, "\n\n\n");
854
            }
855
        }
856
857
        /* Backup Procedure structure*/
858
        /*
859
         $result = $db->query('SHOW PROCEDURE STATUS');
860
         if ($db->num_rows($result) > 0)
861
         {
862
         while ($row = $db->fetch_row($result)) { $procedures[] = $row[1]; }
863
         foreach($procedures as $proc)
864
         {
865
         fwrite($handle,"DELIMITER $$\n\n");
866
         fwrite($handle,"DROP PROCEDURE IF EXISTS '$name'.'$proc'$$\n");
867
         $resqlcreateproc=$db->query("SHOW CREATE PROCEDURE '$proc'");
868
         $row2 = $db->fetch_row($resqlcreateproc);
869
         fwrite($handle,"\n".$row2[2]."$$\n\n");
870
         fwrite($handle,"DELIMITER ;\n\n");
871
         }
872
         }
873
         */
874
        /* Backup Procedure structure*/
875
876
        // Write the footer (restore the previous database settings)
877
        $sqlfooter = "\n\n";
878
        if (GETPOST("nobin_use_transaction")) {
879
            $sqlfooter .= "COMMIT;\n";
880
        }
881
        if (GETPOST("nobin_disable_fk")) {
882
            $sqlfooter .= "SET FOREIGN_KEY_CHECKS=1;\n";
883
        }
884
        $sqlfooter .= "\n\n-- Dump completed on " . date('Y-m-d G-i-s');
885
        fwrite($handle, $sqlfooter);
886
887
        fclose($handle);
888
889
        return 1;
890
    }
891
892
    /**
893
     * Generate documentation of a Module
894
     *
895
     * @param string $module Module name
896
     * @return  int                 Return integer <0 if KO, >0 if OK
897
     */
898
    public function generateDoc($module)
899
    {
900
        global $conf, $langs, $user, $mysoc;
901
        global $dirins;
902
903
        $error = 0;
904
905
        $modulelowercase = strtolower($module);
906
        $now = dol_now();
907
908
        // Dir for module
909
        $dir = $dirins . '/' . $modulelowercase;
910
        // Zip file to build
911
        $FILENAMEDOC = '';
912
913
        // Load module
914
        dol_include_once($modulelowercase . '/core/modules/mod' . $module . '.class.php');
915
        $class = 'mod' . $module;
916
917
        if (class_exists($class)) {
918
            try {
919
                $moduleobj = new $class($this->db);
920
            } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Core\Classes\Exception was not found. Did you mean Exception? If so, make sure to prefix the type with \.
Loading history...
921
                $error++;
922
                dol_print_error(null, $e->getMessage());
923
            }
924
        } else {
925
            $error++;
926
            $langs->load("errors");
927
            dol_print_error(null, $langs->trans("ErrorFailedToLoadModuleDescriptorForXXX", $module));
928
            exit;
0 ignored issues
show
Best Practice introduced by
Using exit here is not recommended.

In general, usage of exit should be done with care and only when running in a scripting context like a CLI script.

Loading history...
929
        }
930
931
        $arrayversion = explode('.', $moduleobj->version, 3);
932
        if (count($arrayversion)) {
933
            $FILENAMEASCII = strtolower($module) . '.asciidoc';
934
            $FILENAMEDOC = strtolower($module) . '.html';
935
            $FILENAMEDOCPDF = strtolower($module) . '.pdf';
936
937
            $dirofmodule = dol_buildpath(strtolower($module), 0);
938
            $dirofmoduledoc = dol_buildpath(strtolower($module), 0) . '/doc';
939
            $dirofmoduletmp = dol_buildpath(strtolower($module), 0) . '/doc/temp';
940
            $outputfiledoc = $dirofmoduledoc . '/' . $FILENAMEDOC;
941
            if ($dirofmoduledoc) {
942
                if (!dol_is_dir($dirofmoduledoc)) {
943
                    dol_mkdir($dirofmoduledoc);
944
                }
945
                if (!dol_is_dir($dirofmoduletmp)) {
946
                    dol_mkdir($dirofmoduletmp);
947
                }
948
                if (!is_writable($dirofmoduletmp)) {
949
                    $this->error = 'Dir ' . $dirofmoduletmp . ' does not exists or is not writable';
950
                    return -1;
951
                }
952
953
                if (!getDolGlobalString('MODULEBUILDER_ASCIIDOCTOR') && !getDolGlobalString('MODULEBUILDER_ASCIIDOCTORPDF')) {
954
                    $this->error = 'Setup of module ModuleBuilder not complete';
955
                    return -1;
956
                }
957
958
                // Copy some files into temp directory, so instruction include::ChangeLog.md[] will works inside the asciidoc file.
959
                dol_copy($dirofmodule . '/README.md', $dirofmoduletmp . '/README.md', 0, 1);
960
                dol_copy($dirofmodule . '/ChangeLog.md', $dirofmoduletmp . '/ChangeLog.md', 0, 1);
961
962
                // Replace into README.md and ChangeLog.md (in case they are included into documentation with tag __README__ or __CHANGELOG__)
963
                $arrayreplacement = array();
964
                $arrayreplacement['/^#\s.*/m'] = ''; // Remove first level of title into .md files
965
                $arrayreplacement['/^#/m'] = '##'; // Add on # to increase level
966
967
                dolReplaceInFile($dirofmoduletmp . '/README.md', $arrayreplacement, '', 0, 0, 1);
968
                dolReplaceInFile($dirofmoduletmp . '/ChangeLog.md', $arrayreplacement, '', 0, 0, 1);
969
970
971
                $destfile = $dirofmoduletmp . '/' . $FILENAMEASCII;
972
973
                $fhandle = fopen($destfile, 'w+');
974
                if ($fhandle) {
975
                    $specs = dol_dir_list(dol_buildpath(strtolower($module) . '/doc', 0), 'files', 1, '(\.md|\.asciidoc)$', array('\/temp\/'));
976
977
                    $i = 0;
978
                    foreach ($specs as $spec) {
979
                        if (preg_match('/notindoc/', $spec['relativename'])) {
980
                            continue; // Discard file
981
                        }
982
                        if (preg_match('/example/', $spec['relativename'])) {
983
                            continue; // Discard file
984
                        }
985
                        if (preg_match('/disabled/', $spec['relativename'])) {
986
                            continue; // Discard file
987
                        }
988
989
                        $pathtofile = strtolower($module) . '/doc/' . $spec['relativename'];
990
                        $format = 'asciidoc';
991
                        if (preg_match('/\.md$/i', $spec['name'])) {
992
                            $format = 'markdown';
993
                        }
994
995
                        $filecursor = @file_get_contents($spec['fullname']);
996
                        if ($filecursor) {
997
                            fwrite($fhandle, ($i ? "\n<<<\n\n" : "") . $filecursor . "\n");
998
                        } else {
999
                            $this->error = 'Failed to concat content of file ' . $spec['fullname'];
1000
                            return -1;
1001
                        }
1002
1003
                        $i++;
1004
                    }
1005
1006
                    fclose($fhandle);
1007
1008
                    $contentreadme = file_get_contents($dirofmoduletmp . '/README.md');
1009
                    $contentchangelog = file_get_contents($dirofmoduletmp . '/ChangeLog.md');
1010
1011
                    include DOL_DOCUMENT_ROOT . '/core/lib/parsemd.lib.php';
1012
1013
                    //var_dump($phpfileval['fullname']);
1014
                    $arrayreplacement = array(
1015
                        'mymodule' => strtolower($module),
1016
                        'MyModule' => $module,
1017
                        'MYMODULE' => strtoupper($module),
1018
                        'My module' => $module,
1019
                        'my module' => $module,
1020
                        'Mon module' => $module,
1021
                        'mon module' => $module,
1022
                        'htdocs/modulebuilder/template' => strtolower($module),
1023
                        '__MYCOMPANY_NAME__' => $mysoc->name,
1024
                        '__KEYWORDS__' => $module,
1025
                        '__USER_FULLNAME__' => $user->getFullName($langs),
1026
                        '__USER_EMAIL__' => $user->email,
1027
                        '__YYYY-MM-DD__' => dol_print_date($now, 'dayrfc'),
1028
                        '---Put here your own copyright and developer email---' => dol_print_date($now, 'dayrfc') . ' ' . $user->getFullName($langs) . ($user->email ? ' <' . $user->email . '>' : ''),
1029
                        '__DATA_SPECIFICATION__' => 'Not yet available',
1030
                        '__README__' => dolMd2Asciidoc($contentreadme),
1031
                        '__CHANGELOG__' => dolMd2Asciidoc($contentchangelog),
1032
                    );
1033
1034
                    // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1035
                    dolReplaceInFile($destfile, $arrayreplacement);
1036
                }
1037
1038
                // Launch doc generation
1039
                $currentdir = getcwd();
1040
                chdir($dirofmodule);
1041
1042
                require_once constant('DOL_DOCUMENT_ROOT') . '/core/class/utils.class.php';
1043
                $utils = new Utils($this->db);
1044
1045
                // Build HTML doc
1046
                $command = getDolGlobalString('MODULEBUILDER_ASCIIDOCTOR') . ' ' . $destfile . ' -n -o ' . $dirofmoduledoc . '/' . $FILENAMEDOC;
1047
                $outfile = $dirofmoduletmp . '/out.tmp';
1048
1049
                $resarray = $utils->executeCLI($command, $outfile);
1050
                if ($resarray['result'] != '0') {
1051
                    $this->error = $resarray['error'] . ' ' . $resarray['output'];
1052
                    $this->errors[] = $this->error;
1053
                }
1054
                $result = ($resarray['result'] == 0) ? 1 : 0;
1055
                if ($result < 0 && empty($this->errors)) {
1056
                    $this->error = $langs->trans("ErrorFailToGenerateFile", $FILENAMEDOC);
1057
                    $this->errors[] = $this->error;
1058
                }
1059
1060
                // Build PDF doc
1061
                $command = getDolGlobalString('MODULEBUILDER_ASCIIDOCTORPDF') . ' ' . $destfile . ' -n -o ' . $dirofmoduledoc . '/' . $FILENAMEDOCPDF;
1062
                $outfile = $dirofmoduletmp . '/outpdf.tmp';
1063
                $resarray = $utils->executeCLI($command, $outfile);
1064
                if ($resarray['result'] != '0') {
1065
                    $this->error = $resarray['error'] . ' ' . $resarray['output'];
1066
                    $this->errors[] = $this->error;
1067
                }
1068
                $result = ($resarray['result'] == 0) ? 1 : 0;
1069
                if ($result < 0 && empty($this->errors)) {
1070
                    $this->error = $langs->trans("ErrorFailToGenerateFile", $FILENAMEDOCPDF);
1071
                    $this->errors[] = $this->error;
1072
                }
1073
1074
                chdir($currentdir);
1075
            } else {
1076
                $result = 0;
1077
            }
1078
1079
            if ($result > 0) {
1080
                return 1;
1081
            } else {
1082
                $error++;
1083
            }
1084
        } else {
1085
            $error++;
1086
            $langs->load("errors");
1087
            $this->error = $langs->trans("ErrorCheckVersionIsDefined");
1088
        }
1089
1090
        return -1;
1091
    }
1092
1093
    /**
1094
     * Execute a CLI command.
1095
     *
1096
     * @param string $command Command line to execute.
1097
     *                                      Warning: The command line is sanitize by escapeshellcmd(), except if $noescapecommand set, so can't contains any redirection char '>'. Use param $redirectionfile if you need it.
1098
     * @param string $outputfile A path for an output file (used only when method is 2). For example: $conf->admin->dir_temp.'/out.tmp';
1099
     * @param int $execmethod 0=Use default method (that is 1 by default), 1=Use the PHP 'exec', 2=Use the 'popen' method
1100
     * @param string $redirectionfile If defined, a redirection of output to this file is added.
1101
     * @param int $noescapecommand 1=Do not escape command. Warning: Using this parameter needs you already have sanitized the $command parameter. If not, it will lead to security vulnerability.
1102
     *                                      This parameter is provided for backward compatibility with external modules. Always use 0 in core.
1103
     * @param string $redirectionfileerr If defined, a redirection of error is added to this file instead of to channel 1.
1104
     * @return  array                       array('result'=>...,'output'=>...,'error'=>...). result = 0 means OK.
1105
     */
1106
    public function executeCLI($command, $outputfile, $execmethod = 0, $redirectionfile = null, $noescapecommand = 0, $redirectionfileerr = null)
1107
    {
1108
        global $conf, $langs;
1109
1110
        $result = 0;
1111
        $output = '';
1112
        $error = '';
1113
1114
        if (empty($noescapecommand)) {
1115
            $command = escapeshellcmd($command);
1116
        }
1117
1118
        if ($redirectionfile) {
1119
            $command .= " > " . dol_sanitizePathName($redirectionfile);
1120
        }
1121
1122
        if ($redirectionfileerr && ($redirectionfileerr != $redirectionfile)) {
1123
            // If we ask a redirect of stderr on a given file not already used for stdout
1124
            $command .= " 2> " . dol_sanitizePathName($redirectionfileerr);
1125
        } else {
1126
            $command .= " 2>&1";
1127
        }
1128
1129
        if (getDolGlobalString('MAIN_EXEC_USE_POPEN')) {
1130
            $execmethod = getDolGlobalString('MAIN_EXEC_USE_POPEN');
1131
        }
1132
        if (empty($execmethod)) {
1133
            $execmethod = 1;
1134
        }
1135
        //$execmethod=1;
1136
        dol_syslog("Utils::executeCLI execmethod=" . $execmethod . " command=" . $command, LOG_DEBUG);
1137
        $output_arr = array();
1138
1139
        if ($execmethod == 1) {
1140
            $retval = null;
1141
            exec($command, $output_arr, $retval);
1142
            $result = $retval;
1143
            if ($retval != 0) {
1144
                $langs->load("errors");
1145
                dol_syslog("Utils::executeCLI retval after exec=" . $retval, LOG_ERR);
1146
                $error = 'Error ' . $retval;
1147
            }
1148
        }
1149
        if ($execmethod == 2) { // With this method, there is no way to get the return code, only output
1150
            $handle = fopen($outputfile, 'w+b');
1151
            if ($handle) {
1152
                dol_syslog("Utils::executeCLI run command " . $command);
1153
                $handlein = popen($command, 'r');
1154
                while (!feof($handlein)) {
1155
                    $read = fgets($handlein);
1156
                    fwrite($handle, $read);
1157
                    $output_arr[] = $read;
1158
                }
1159
                pclose($handlein);
1160
                fclose($handle);
1161
            }
1162
            dolChmod($outputfile);
1163
        }
1164
1165
        // Update with result
1166
        if (is_array($output_arr) && count($output_arr) > 0) {
1167
            foreach ($output_arr as $val) {
1168
                $output .= $val . ($execmethod == 2 ? '' : "\n");
1169
            }
1170
        }
1171
1172
        dol_syslog("Utils::executeCLI result=" . $result . " output=" . $output . " error=" . $error, LOG_DEBUG);
1173
1174
        return array('result' => $result, 'output' => $output, 'error' => $error);
1175
    }
1176
1177
    /**
1178
     * This saves syslog files and compresses older ones.
1179
     * Nb of archive to keep is defined into $conf->global->SYSLOG_FILE_SAVES
1180
     * CAN BE A CRON TASK
1181
     *
1182
     * @return  int                     0 if OK, < 0 if KO
1183
     */
1184
    public function compressSyslogs()
1185
    {
1186
        global $conf;
1187
1188
        if (empty($conf->loghandlers['mod_syslog_file'])) { // File Syslog disabled
1189
            return 0;
1190
        }
1191
1192
        if (!function_exists('gzopen')) {
1193
            $this->error = 'Support for gzopen not available in this PHP';
1194
            return -1;
1195
        }
1196
1197
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
1198
1199
        $nbSaves = intval(getDolGlobalString('SYSLOG_FILE_SAVES', 10));
1200
1201
        if (!getDolGlobalString('SYSLOG_FILE')) {
1202
            $mainlogdir = DOL_DATA_ROOT;
1203
            $mainlog = 'alixar.log';
1204
        } else {
1205
            $mainlogfull = str_replace('DOL_DATA_ROOT', DOL_DATA_ROOT, $conf->global->SYSLOG_FILE);
1206
            $mainlogdir = dirname($mainlogfull);
1207
            $mainlog = basename($mainlogfull);
1208
        }
1209
1210
        $tabfiles = dol_dir_list(DOL_DATA_ROOT, 'files', 0, '^(dolibarr_.+|odt2pdf)\.log$'); // Also handle other log files like dolibarr_install.log
1211
        $tabfiles[] = array('name' => $mainlog, 'path' => $mainlogdir);
1212
1213
        foreach ($tabfiles as $file) {
1214
            $logname = $file['name'];
1215
            $logpath = $file['path'];
1216
1217
            if (dol_is_file($logpath . '/' . $logname) && dol_filesize($logpath . '/' . $logname) > 0) {    // If log file exists and is not empty
1218
                // Handle already compressed files to rename them and add +1
1219
1220
                $filter = '^' . preg_quote($logname, '/') . '\.([0-9]+)\.gz$';
1221
1222
                $gzfilestmp = dol_dir_list($logpath, 'files', 0, $filter);
1223
                $gzfiles = array();
1224
1225
                foreach ($gzfilestmp as $gzfile) {
1226
                    $tabmatches = array();
1227
                    preg_match('/' . $filter . '/i', $gzfile['name'], $tabmatches);
1228
1229
                    $numsave = intval($tabmatches[1]);
1230
1231
                    $gzfiles[$numsave] = $gzfile;
1232
                }
1233
1234
                krsort($gzfiles, SORT_NUMERIC);
1235
1236
                foreach ($gzfiles as $numsave => $dummy) {
1237
                    if (dol_is_file($logpath . '/' . $logname . '.' . ($numsave + 1) . '.gz')) {
1238
                        return -2;
1239
                    }
1240
1241
                    if ($numsave >= $nbSaves) {
1242
                        dol_delete_file($logpath . '/' . $logname . '.' . $numsave . '.gz', 0, 0, 0, null, false, 0);
1243
                    } else {
1244
                        dol_move($logpath . '/' . $logname . '.' . $numsave . '.gz', $logpath . '/' . $logname . '.' . ($numsave + 1) . '.gz', 0, 1, 0, 0);
1245
                    }
1246
                }
1247
1248
                // Compress current file and recreate it
1249
1250
                if ($nbSaves > 0) {         // If $nbSaves is 1, we keep 1 archive .gz file, If 2, we keep 2 .gz files
1251
                    $gzfilehandle = gzopen($logpath . '/' . $logname . '.1.gz', 'wb9');
1252
1253
                    if (empty($gzfilehandle)) {
1254
                        $this->error = 'Failted to open file ' . $logpath . '/' . $logname . '.1.gz';
1255
                        return -3;
1256
                    }
1257
1258
                    $sourcehandle = fopen($logpath . '/' . $logname, 'r');
1259
1260
                    if (empty($sourcehandle)) {
1261
                        $this->error = 'Failed to open file ' . $logpath . '/' . $logname;
1262
                        return -4;
1263
                    }
1264
1265
                    while (!feof($sourcehandle)) {
1266
                        gzwrite($gzfilehandle, fread($sourcehandle, 512 * 1024)); // Read 512 kB at a time
1267
                    }
1268
1269
                    fclose($sourcehandle);
1270
                    gzclose($gzfilehandle);
1271
1272
                    dolChmod($logpath . '/' . $logname . '.1.gz');
1273
                }
1274
1275
                dol_delete_file($logpath . '/' . $logname, 0, 0, 0, null, false, 0);
1276
1277
                // Create empty file
1278
                $newlog = fopen($logpath . '/' . $logname, 'a+');
1279
                fclose($newlog);
1280
1281
                //var_dump($logpath.'/'.$logname." - ".octdec(empty($conf->global->MAIN_UMASK)?'0664':$conf->global->MAIN_UMASK));
1282
                dolChmod($logpath . '/' . $logname);
1283
            }
1284
        }
1285
1286
        $this->output = 'Archive log files (keeping last SYSLOG_FILE_SAVES=' . $nbSaves . ' files) done.';
1287
        return 0;
1288
    }
1289
1290
    /**
1291
     *  Make a send last backup of database or fil in param
1292
     *  CAN BE A CRON TASK
1293
     *
1294
     * @param string $sendto Recipients emails
1295
     * @param string $from Sender email
1296
     * @param string $subject Topic/Subject of mail
1297
     * @param string $message Message
1298
     * @param string $filename List of files to attach (full path of filename on file system)
1299
     * @param string $filter Filter file send
1300
     * @param int $sizelimit Limit size to send file
1301
     * @return int                          0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
1302
     */
1303
    public function sendBackup($sendto = '', $from = '', $subject = '', $message = '', $filename = '', $filter = '', $sizelimit = 100000000)
1304
    {
1305
        global $conf, $langs;
1306
        global $dolibarr_main_url_root;
1307
1308
        $filepath = '';
1309
        $output = '';
1310
        $error = 0;
1311
1312
        if (!empty($from)) {
1313
            $from = dol_escape_htmltag($from);
1314
        } elseif (getDolGlobalString('MAIN_INFO_SOCIETE_MAIL')) {
1315
            $from = dol_escape_htmltag(getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'));
1316
        } else {
1317
            $error++;
1318
        }
1319
1320
        if (!empty($sendto)) {
1321
            $sendto = dol_escape_htmltag($sendto);
1322
        } elseif (getDolGlobalString('MAIN_INFO_SOCIETE_MAIL')) {
1323
            $from = dol_escape_htmltag(getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'));
1324
        } else {
1325
            $error++;
1326
        }
1327
1328
        if (!empty($subject)) {
1329
            $subject = dol_escape_htmltag($subject);
1330
        } else {
1331
            $subject = dol_escape_htmltag($langs->trans('MakeSendLocalDatabaseDumpShort'));
1332
        }
1333
1334
        if (empty($message)) {
1335
            $message = dol_escape_htmltag($langs->trans('MakeSendLocalDatabaseDumpShort'));
1336
        }
1337
1338
        $tmpfiles = array();
1339
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
1340
        if ($filename) {
1341
            if (dol_is_file($conf->admin->dir_output . '/backup/' . $filename)) {
1342
                $tmpfiles = dol_most_recent_file($conf->admin->dir_output . '/backup', $filename);
1343
            }
1344
        } else {
1345
            $tmpfiles = dol_most_recent_file($conf->admin->dir_output . '/backup', $filter);
1346
        }
1347
        if ($tmpfiles && is_array($tmpfiles)) {
1348
            foreach ($tmpfiles as $key => $val) {
1349
                if ($key == 'fullname') {
1350
                    $filepath = array($val);
1351
                    $filesize = dol_filesize($val);
1352
                }
1353
                if ($key == 'type') {
1354
                    $mimetype = array($val);
1355
                }
1356
                if ($key == 'relativename') {
1357
                    $filename = array($val);
1358
                }
1359
            }
1360
        }
1361
1362
        if ($filepath) {
1363
            if ($filesize > $sizelimit) {
1364
                $message .= '<br>' . $langs->trans("BackupIsTooLargeSend");
1365
                $documenturl = $dolibarr_main_url_root . '/document.php?modulepart=systemtools&atachement=1&file=backup/' . urlencode($filename[0]);
1366
                $message .= '<br><a href=' . $documenturl . '>Lien de téléchargement</a>';
1367
                $filepath = '';
1368
                $mimetype = '';
1369
                $filename = '';
1370
            }
1371
        } else {
1372
            $output = 'No backup file found';
1373
            $error++;
1374
        }
1375
1376
        if (!$error) {
1377
            $mailfile = new CMailFile($subject, $sendto, $from, $message, $filepath, $mimetype, $filename, '', '', 0, -1);
1378
            if ($mailfile->error) {
1379
                $error++;
1380
                $output = $mailfile->error;
1381
            }
1382
        }
1383
1384
        $result = false;
1385
        if (!$error) {
1386
            $result = $mailfile->sendfile();
1387
            if (!$result) {
1388
                $error++;
1389
                $output = $mailfile->error;
1390
            }
1391
        }
1392
1393
        dol_syslog(__METHOD__, LOG_DEBUG);
1394
1395
        $this->error = "Error sending backp file " . ((string)$error);
1396
        $this->output = $output;
1397
1398
        if ($result) {
1399
            return 0;
1400
        } else {
1401
            return -1;
1402
        }
1403
    }
1404
1405
    /**
1406
     *  Clean unfinished cronjob in processing when pid is no longer present in the system
1407
     *  CAN BE A CRON TASK
1408
     *
1409
     * @return    int                               0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
1410
     * @throws Exception
1411
     */
1412
    public function cleanUnfinishedCronjob()
1413
    {
1414
        global $db, $user;
1415
        dol_syslog("Utils::cleanUnfinishedCronjob Starting cleaning");
1416
1417
        // Import Cronjob class if not present
1418
        
1419
        // Get this job object
1420
        $this_job = new Cronjob($db);
1421
        $this_job->fetch(-1, 'Utils', 'cleanUnfinishedCronjob');
1422
        if (empty($this_job->id) || !empty($this_job->error)) {
1423
            dol_syslog("Utils::cleanUnfinishedCronjob Unable to fetch himself: " . $this_job->error, LOG_ERR);
1424
            return -1;
1425
        }
1426
1427
        // Set this job processing to 0 to avoid being locked by his processing state
1428
        $this_job->processing = 0;
1429
        if ($this_job->update($user) < 0) {
1430
            dol_syslog("Utils::cleanUnfinishedCronjob Unable to update himself: " . implode(', ', $this_job->errors), LOG_ERR);
1431
            return -1;
1432
        }
1433
1434
        $cron_job = new Cronjob($db);
1435
        $cron_job->fetchAll('DESC', 't.rowid', 100, 0, 1, [], 1);   // Fetch jobs that are currently running
1436
1437
        // Iterate over all jobs in processing (this can't be this job since his state is set to 0 before)
1438
        foreach ($cron_job->lines as $job_line) {
1439
            // Avoid job with no PID
1440
            if (empty($job_line->pid)) {
1441
                dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " don't have a PID", LOG_DEBUG);
1442
                continue;
1443
            }
1444
1445
            $job = new Cronjob($db);
1446
            $job->fetch($job_line->id);
1447
            if (empty($job->id) || !empty($job->error)) {
1448
                dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " can't be fetch: " . $job->error, LOG_ERR);
1449
                continue;
1450
            }
1451
1452
            // Calling posix_kill with the 0 kill signal will return true if the process is running, false otherwise.
1453
            if (!posix_kill($job->pid, 0)) {
1454
                // Clean processing and pid values
1455
                $job->processing = 0;
1456
                $job->pid = null;
1457
1458
                // Set last result as an error and add the reason on the last output
1459
                $job->lastresult = strval(-1);
1460
                $job->lastoutput = 'Job killed by job cleanUnfinishedCronjob';
1461
1462
                if ($job->update($user) < 0) {
1463
                    dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " can't be updated: " . implode(', ', $job->errors), LOG_ERR);
1464
                    continue;
1465
                }
1466
                dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " cleaned");
1467
            }
1468
        }
1469
1470
        dol_syslog("Utils::cleanUnfinishedCronjob Cleaning completed");
1471
        return 0;
1472
    }
1473
}
1474