Passed
Push — EXTRACT_CLASSES ( 0382f2...c25e41 )
by Rafael
52:18
created

Utils::generateDoc()   F

Complexity

Conditions 28
Paths 2679

Size

Total Lines 193
Code Lines 129

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 129
nc 2679
nop 1
dl 0
loc 193
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
                    require_once constant('DOL_DOCUMENT_ROOT') . '/ecm/class/ecmdirectory.class.php';
211
                    $ecmdirstatic = new EcmDirectory($this->db);
0 ignored issues
show
Bug introduced by
The type Dolibarr\Code\Core\Classes\EcmDirectory was not found. Did you mean EcmDirectory? If so, make sure to prefix the type with \.
Loading history...
212
                    $result = $ecmdirstatic->refreshcachenboffile(1);
213
                }
214
            }
215
        }
216
217
        if ($count > 0) {
218
            $langs->load("admin");
219
            $this->output = $langs->trans("PurgeNDirectoriesDeleted", $countdeleted);
220
            if ($count > $countdeleted) {
221
                $this->output .= '<br>' . $langs->trans("PurgeNDirectoriesFailed", ($count - $countdeleted));
222
            }
223
        } else {
224
            $this->output = $langs->trans("PurgeNothingToDelete") . (in_array('tempfilesold', $choicesarray) ? ' (older than 24h for temp files)' : '');
225
        }
226
227
        // Recreate temp dir that are not automatically recreated by core code for performance purpose, we need them
228
        if (isModEnabled('api')) {
229
            dol_mkdir($conf->api->dir_temp);
230
        }
231
        dol_mkdir($conf->user->dir_temp);
232
233
        //return $count;
234
        return 0; // This function can be called by cron so must return 0 if OK
235
    }
236
237
238
    /**
239
     *  Make a backup of database
240
     *  CAN BE A CRON TASK
241
     *
242
     * @param string $compression 'gz' or 'bz' or 'none'
243
     * @param string $type 'mysql', 'postgresql', ...
244
     * @param int $usedefault 1=Use default backup profile (Set this to 1 when used as cron)
245
     * @param string $file 'auto' or filename to build
246
     * @param int $keeplastnfiles Keep only last n files (not used yet)
247
     * @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)
248
     * @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().
249
     * @return int                            0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
250
     */
251
    public function dumpDatabase($compression = 'none', $type = 'auto', $usedefault = 1, $file = 'auto', $keeplastnfiles = 0, $execmethod = 0, $lowmemorydump = 0)
252
    {
253
        global $db, $conf, $langs, $dolibarr_main_data_root;
254
        global $dolibarr_main_db_name, $dolibarr_main_db_host, $dolibarr_main_db_user, $dolibarr_main_db_port, $dolibarr_main_db_pass;
255
        global $dolibarr_main_db_character_set;
256
257
        $langs->load("admin");
258
259
        dol_syslog("Utils::dumpDatabase type=" . $type . " compression=" . $compression . " file=" . $file, LOG_DEBUG);
260
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
261
262
        // Clean data
263
        $file = dol_sanitizeFileName($file);
264
265
        // Check compression parameter
266
        if (!in_array($compression, array('none', 'gz', 'bz', 'zip', 'zstd'))) {
267
            $langs->load("errors");
268
            $this->error = $langs->transnoentitiesnoconv("ErrorBadValueForParameter", $compression, "Compression");
269
            return -1;
270
        }
271
272
        // Check type parameter
273
        if ($type == 'auto') {
274
            $type = $this->db->type;
275
        }
276
        if (!in_array($type, array('postgresql', 'pgsql', 'mysql', 'mysqli', 'mysqlnobin'))) {
277
            $langs->load("errors");
278
            $this->error = $langs->transnoentitiesnoconv("ErrorBadValueForParameter", $type, "Basetype");
279
            return -1;
280
        }
281
282
        // Check file parameter
283
        if ($file == 'auto') {
284
            $prefix = 'dump';
285
            $ext = 'sql';
286
            if (in_array($type, array('mysql', 'mysqli'))) {
287
                $prefix = 'mysqldump';
288
            }
289
            //if ($label == 'PostgreSQL') { $prefix='pg_dump'; $ext='dump'; }
290
            if (in_array($type, array('pgsql'))) {
291
                $prefix = 'pg_dump';
292
            }
293
            $file = $prefix . '_' . $dolibarr_main_db_name . '_' . dol_sanitizeFileName(DOL_VERSION) . '_' . dol_print_date(dol_now('gmt'), "dayhourlogsmall", 'tzuser') . '.' . $ext;
294
        }
295
296
        $outputdir = $conf->admin->dir_output . '/backup';
297
        $result = dol_mkdir($outputdir);
298
        $errormsg = '';
299
300
        // MYSQL
301
        if ($type == 'mysql' || $type == 'mysqli') {
302
            if (!getDolGlobalString('SYSTEMTOOLS_MYSQLDUMP')) {
303
                $cmddump = $db->getPathOfDump();
304
            } else {
305
                $cmddump = getDolGlobalString('SYSTEMTOOLS_MYSQLDUMP');
306
            }
307
            if (empty($cmddump)) {
308
                $this->error = "Failed to detect command to use for mysqldump. Try a manual backup before to set path of command.";
309
                return -1;
310
            }
311
312
            $outputfile = $outputdir . '/' . $file;
313
            // for compression format, we add extension
314
            $compression = $compression ? $compression : 'none';
315
            if ($compression == 'gz') {
316
                $outputfile .= '.gz';
317
            } elseif ($compression == 'bz') {
318
                $outputfile .= '.bz2';
319
            } elseif ($compression == 'zstd') {
320
                $outputfile .= '.zst';
321
            }
322
            $outputerror = $outputfile . '.err';
323
            dol_mkdir($conf->admin->dir_output . '/backup');
324
325
            // Parameters execution
326
            $command = $cmddump;
327
            $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.
328
            if (preg_match("/\s/", $command)) {
329
                $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
330
            }
331
332
            //$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
333
            $param = $dolibarr_main_db_name . " -h " . $dolibarr_main_db_host;
334
            $param .= " -u " . $dolibarr_main_db_user;
335
            if (!empty($dolibarr_main_db_port)) {
336
                $param .= " -P " . $dolibarr_main_db_port . " --protocol=tcp";
337
            }
338
            if (GETPOST("use_transaction", "alpha")) {
339
                $param .= " --single-transaction";
340
            }
341
            if (GETPOST("disable_fk", "alpha") || $usedefault) {
342
                $param .= " -K";
343
            }
344
            if (GETPOST("sql_compat", "alpha") && GETPOST("sql_compat", "alpha") != 'NONE') {
345
                $param .= " --compatible=" . escapeshellarg(GETPOST("sql_compat", "alpha"));
346
            }
347
            if (GETPOST("drop_database", "alpha")) {
348
                $param .= " --add-drop-database";
349
            }
350
            if (GETPOST("use_mysql_quick_param", "alpha")) {
351
                $param .= " --quick";
352
            }
353
            if (GETPOST("use_force", "alpha")) {
354
                $param .= " -f";
355
            }
356
            if (GETPOST("sql_structure", "alpha") || $usedefault) {
357
                if (GETPOST("drop", "alpha") || $usedefault) {
358
                    $param .= " --add-drop-table=TRUE";
359
                } else {
360
                    $param .= " --add-drop-table=FALSE";
361
                }
362
            } else {
363
                $param .= " -t";
364
            }
365
            if (GETPOST("disable-add-locks", "alpha")) {
366
                $param .= " --add-locks=FALSE";
367
            }
368
            if (GETPOST("sql_data", "alpha") || $usedefault) {
369
                $param .= " --tables";
370
                if (GETPOST("showcolumns", "alpha") || $usedefault) {
371
                    $param .= " -c";
372
                }
373
                if (GETPOST("extended_ins", "alpha") || $usedefault) {
374
                    $param .= " -e";
375
                } else {
376
                    $param .= " --skip-extended-insert";
377
                }
378
                if (GETPOST("delayed", "alpha")) {
379
                    $param .= " --delayed-insert";
380
                }
381
                if (GETPOST("sql_ignore", "alpha")) {
382
                    $param .= " --insert-ignore";
383
                }
384
                if (GETPOST("hexforbinary", "alpha") || $usedefault) {
385
                    $param .= " --hex-blob";
386
                }
387
            } else {
388
                $param .= " -d"; // No row information (no data)
389
            }
390
            if ($dolibarr_main_db_character_set == 'utf8mb4') {
391
                // We save output into utf8mb4 charset
392
                $param .= " --default-character-set=utf8mb4 --no-tablespaces";
393
            } else {
394
                $param .= " --default-character-set=utf8 --no-tablespaces"; // We always save output into utf8 charset
395
            }
396
            $paramcrypted = $param;
397
            $paramclear = $param;
398
            if (!empty($dolibarr_main_db_pass)) {
399
                $paramcrypted .= ' -p"' . preg_replace('/./i', '*', $dolibarr_main_db_pass) . '"';
400
                $paramclear .= ' -p"' . str_replace(array('"', '`', '$'), array('\"', '\`', '\$'), $dolibarr_main_db_pass) . '"';
401
            }
402
403
            $handle = '';
404
405
            // Start call method to execute dump
406
            $fullcommandcrypted = $command . " " . $paramcrypted . " 2>&1";
407
            $fullcommandclear = $command . " " . $paramclear . " 2>&1";
408
            if (!$lowmemorydump) {
409
                if ($compression == 'none') {
410
                    $handle = fopen($outputfile, 'w');
411
                } elseif ($compression == 'gz') {
412
                    $handle = gzopen($outputfile, 'w');
413
                } elseif ($compression == 'bz') {
414
                    $handle = bzopen($outputfile, 'w');
415
                } elseif ($compression == 'zstd') {
416
                    $handle = fopen($outputfile, 'w');
417
                }
418
            } else {
419
                if ($compression == 'none') {
420
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." > "' . dol_sanitizePathName($outputfile) . '"';
421
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." > "' . dol_sanitizePathName($outputfile) . '"';
422
                    $handle = 1;
423
                } elseif ($compression == 'gz') {
424
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | gzip > "' . dol_sanitizePathName($outputfile) . '"';
425
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | gzip > "' . dol_sanitizePathName($outputfile) . '"';
426
                    $paramcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | gzip';
427
                    $handle = 1;
428
                } elseif ($compression == 'bz') {
429
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | bzip2 > "' . dol_sanitizePathName($outputfile) . '"';
430
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | bzip2 > "' . dol_sanitizePathName($outputfile) . '"';
431
                    $paramcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | bzip2';
432
                    $handle = 1;
433
                } elseif ($compression == 'zstd') {
434
                    $fullcommandclear .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | zstd > "' . dol_sanitizePathName($outputfile) . '"';
435
                    $fullcommandcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | zstd > "' . dol_sanitizePathName($outputfile) . '"';
436
                    $paramcrypted .= ' | grep -v "Warning: Using a password on the command line interface can be insecure." | zstd';
437
                    $handle = 1;
438
                }
439
            }
440
441
            $ok = 0;
442
            if ($handle) {
443
                if (getDolGlobalString('MAIN_EXEC_USE_POPEN')) {
444
                    $execmethod = getDolGlobalString('MAIN_EXEC_USE_POPEN');
445
                }
446
                if (empty($execmethod)) {
447
                    $execmethod = 1;
448
                }
449
450
                dol_syslog("Utils::dumpDatabase execmethod=" . $execmethod . " command:" . $fullcommandcrypted, LOG_INFO);
451
452
453
                /* If value has been forced with a php_admin_value, this has no effect. Example of value: '512M' */
454
                $MemoryLimit = getDolGlobalString('MAIN_MEMORY_LIMIT_DUMP');
455
                if (!empty($MemoryLimit)) {
456
                    @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

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

574
                    /** @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...
575
                    @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

575
                    /** @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...
576
                    // Si safe_mode on et command hors du parameter exec, on a un fichier out vide donc errormsg vide
577
                    if (!$errormsg) {
578
                        $langs->load("errors");
579
                        $errormsg = $langs->trans("ErrorFailedToRunExternalCommand");
580
                    }
581
                }
582
            }
583
            // Fin execution commande
584
585
            $this->output = $errormsg;
586
            $this->error = $errormsg;
587
            $this->result = array("commandbackuplastdone" => $command . " " . $paramcrypted, "commandbackuptorun" => "");
588
            //if (empty($this->output)) $this->output=$this->result['commandbackuplastdone'];
589
        }
590
591
        // MYSQL NO BIN
592
        if ($type == 'mysqlnobin') {
593
            $outputfile = $outputdir . '/' . $file;
594
            $outputfiletemp = $outputfile . '-TMP.sql';
595
            // for compression format, we add extension
596
            $compression = $compression ? $compression : 'none';
597
            if ($compression == 'gz') {
598
                $outputfile .= '.gz';
599
            }
600
            if ($compression == 'bz') {
601
                $outputfile .= '.bz2';
602
            }
603
            $outputerror = $outputfile . '.err';
604
            dol_mkdir($conf->admin->dir_output . '/backup');
605
606
            if ($compression == 'gz' or $compression == 'bz') {
607
                $this->backupTables($outputfiletemp);
608
                dol_compress_file($outputfiletemp, $outputfile, $compression);
609
                unlink($outputfiletemp);
610
            } else {
611
                $this->backupTables($outputfile);
612
            }
613
614
            $this->output = "";
615
            $this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => "");
616
        }
617
618
        // POSTGRESQL
619
        if ($type == 'postgresql' || $type == 'pgsql') {
620
            $cmddump = getDolGlobalString('SYSTEMTOOLS_POSTGRESQLDUMP');
621
622
            $outputfile = $outputdir . '/' . $file;
623
            // for compression format, we add extension
624
            $compression = $compression ? $compression : 'none';
625
            if ($compression == 'gz') {
626
                $outputfile .= '.gz';
627
            }
628
            if ($compression == 'bz') {
629
                $outputfile .= '.bz2';
630
            }
631
            $outputerror = $outputfile . '.err';
632
            dol_mkdir($conf->admin->dir_output . '/backup');
633
634
            // Parameters execution
635
            $command = $cmddump;
636
            $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.
637
            if (preg_match("/\s/", $command)) {
638
                $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
639
            }
640
641
            //$param=escapeshellarg($dolibarr_main_db_name)." -h ".escapeshellarg($dolibarr_main_db_host)." -u ".escapeshellarg($dolibarr_main_db_user)." -p".escapeshellarg($dolibarr_main_db_pass);
642
            //$param="-F c";
643
            $param = "-F p";
644
            $param .= " --no-tablespaces --inserts -h " . $dolibarr_main_db_host;
645
            $param .= " -U " . $dolibarr_main_db_user;
646
            if (!empty($dolibarr_main_db_port)) {
647
                $param .= " -p " . $dolibarr_main_db_port;
648
            }
649
            if (GETPOST("sql_compat") && GETPOST("sql_compat") == 'ANSI') {
650
                $param .= "  --disable-dollar-quoting";
651
            }
652
            if (GETPOST("drop_database")) {
653
                $param .= " -c -C";
654
            }
655
            if (GETPOST("sql_structure")) {
656
                if (GETPOST("drop")) {
657
                    $param .= " --add-drop-table";
658
                }
659
                if (!GETPOST("sql_data")) {
660
                    $param .= " -s";
661
                }
662
            }
663
            if (GETPOST("sql_data")) {
664
                if (!GETPOST("sql_structure")) {
665
                    $param .= " -a";
666
                }
667
                if (GETPOST("showcolumns")) {
668
                    $param .= " -c";
669
                }
670
            }
671
            $param .= ' -f "' . $outputfile . '"';
672
            //if ($compression == 'none')
673
            if ($compression == 'gz') {
674
                $param .= ' -Z 9';
675
            }
676
            //if ($compression == 'bz')
677
            $paramcrypted = $param;
678
            $paramclear = $param;
679
            /*if (!empty($dolibarr_main_db_pass))
680
             {
681
             $paramcrypted.=" -W".preg_replace('/./i','*',$dolibarr_main_db_pass);
682
             $paramclear.=" -W".$dolibarr_main_db_pass;
683
             }*/
684
            $paramcrypted .= " -w " . $dolibarr_main_db_name;
685
            $paramclear .= " -w " . $dolibarr_main_db_name;
686
687
            $this->output = "";
688
            $this->result = array("commandbackuplastdone" => "", "commandbackuptorun" => $command . " " . $paramcrypted);
689
        }
690
691
        // Clean old files
692
        if (!$errormsg && $keeplastnfiles > 0) {
693
            $tmpfiles = dol_dir_list($conf->admin->dir_output . '/backup', 'files', 0, '', '(\.err|\.old|\.sav)$', 'date', SORT_DESC);
694
            $i = 0;
695
            if (is_array($tmpfiles)) {
696
                foreach ($tmpfiles as $key => $val) {
697
                    $i++;
698
                    if ($i <= $keeplastnfiles) {
699
                        continue;
700
                    }
701
                    dol_delete_file($val['fullname'], 0, 0, 0, null, false, 0);
702
                }
703
            }
704
        }
705
706
        return ($errormsg ? -1 : 0);
707
    }
708
709
    /** Backup the db OR just a table without mysqldump binary, with PHP only (does not require any exec permission)
710
     *  Author: David Walsh (http://davidwalsh.name/backup-mysql-database-php)
711
     *  Updated and enhanced by Stephen Larroque (lrq3000) and by the many commentators from the blog
712
     *  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.
713
     *  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).
714
     *
715
     * @param string $outputfile Output file name
716
     * @param string $tables Table name or '*' for all
717
     * @return int                     Return integer <0 if KO, >0 if OK
718
     */
719
    public function backupTables($outputfile, $tables = '*')
720
    {
721
        global $db, $langs;
722
        global $errormsg;
723
724
        // Set to UTF-8
725
        if (is_a($db, 'DoliDBMysqli')) {
726
            /** @var DoliDBMysqli $db */
727
            $db->db->set_charset('utf8');
728
        } else {
729
            /** @var DoliDB $db */
730
            $db->query('SET NAMES utf8');
731
            $db->query('SET CHARACTER SET utf8');
732
        }
733
734
        //get all of the tables
735
        if ($tables == '*') {
736
            $tables = array();
737
            $result = $db->query('SHOW FULL TABLES WHERE Table_type = \'BASE TABLE\'');
738
            while ($row = $db->fetch_row($result)) {
739
                $tables[] = $row[0];
740
            }
741
        } else {
742
            $tables = is_array($tables) ? $tables : explode(',', $tables);
743
        }
744
745
        //cycle through
746
        $handle = fopen($outputfile, 'w+');
747
        if (fwrite($handle, '') === false) {
748
            $langs->load("errors");
749
            dol_syslog("Failed to open file " . $outputfile, LOG_ERR);
750
            $errormsg = $langs->trans("ErrorFailedToWriteInDir");
751
            return -1;
752
        }
753
754
        // Print headers and global mysql config vars
755
        $sqlhead = '';
756
        $sqlhead .= "-- " . $db::LABEL . " dump via php with Dolibarr " . DOL_VERSION . "
757
--
758
-- Host: " . $db->db->host_info . "    Database: " . $db->database_name . "
759
-- ------------------------------------------------------
760
-- Server version	" . $db->db->server_info . "
761
762
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
763
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
764
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
765
/*!40101 SET NAMES utf8 */;
766
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
767
/*!40103 SET TIME_ZONE='+00:00' */;
768
/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
769
/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
770
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
771
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
772
773
";
774
775
        if (GETPOST("nobin_disable_fk")) {
776
            $sqlhead .= "SET FOREIGN_KEY_CHECKS=0;\n";
777
        }
778
        //$sqlhead .= "SET SQL_MODE=\"NO_AUTO_VALUE_ON_ZERO\";\n";
779
        if (GETPOST("nobin_use_transaction")) {
780
            $sqlhead .= "SET AUTOCOMMIT=0;\nSTART TRANSACTION;\n";
781
        }
782
783
        fwrite($handle, $sqlhead);
784
785
        $ignore = '';
786
        if (GETPOST("nobin_sql_ignore")) {
787
            $ignore = 'IGNORE ';
788
        }
789
        $delayed = '';
790
        if (GETPOST("nobin_delayed")) {
791
            $delayed = 'DELAYED ';
792
        }
793
794
        // Process each table and print their definition + their datas
795
        foreach ($tables as $table) {
796
            // Saving the table structure
797
            fwrite($handle, "\n--\n-- Table structure for table `" . $table . "`\n--\n");
798
799
            if (GETPOST("nobin_drop")) {
800
                fwrite($handle, "DROP TABLE IF EXISTS `" . $table . "`;\n"); // Dropping table if exists prior to re create it
801
            }
802
            fwrite($handle, "/*!40101 SET @saved_cs_client     = @@character_set_client */;\n");
803
            fwrite($handle, "/*!40101 SET character_set_client = utf8 */;\n");
804
            $resqldrop = $db->query('SHOW CREATE TABLE ' . $table);
805
            $row2 = $db->fetch_row($resqldrop);
806
            if (empty($row2[1])) {
807
                fwrite($handle, "\n-- WARNING: Show create table " . $table . " return empty string when it should not.\n");
808
            } else {
809
                fwrite($handle, $row2[1] . ";\n");
810
                //fwrite($handle,"/*!40101 SET character_set_client = @saved_cs_client */;\n\n");
811
812
                // Dumping the data (locking the table and disabling the keys check while doing the process)
813
                fwrite($handle, "\n--\n-- Dumping data for table `" . $table . "`\n--\n");
814
                if (!GETPOST("nobin_nolocks")) {
815
                    fwrite($handle, "LOCK TABLES `" . $table . "` WRITE;\n"); // Lock the table before inserting data (when the data will be imported back)
816
                }
817
                if (GETPOST("nobin_disable_fk")) {
818
                    fwrite($handle, "ALTER TABLE `" . $table . "` DISABLE KEYS;\n");
819
                } else {
820
                    fwrite($handle, "/*!40000 ALTER TABLE `" . $table . "` DISABLE KEYS */;\n");
821
                }
822
823
                $sql = "SELECT * FROM " . $table; // Here SELECT * is allowed because we don't have definition of columns to take
824
                $result = $db->query($sql);
825
                while ($row = $db->fetch_row($result)) {
826
                    // For each row of data we print a line of INSERT
827
                    fwrite($handle, "INSERT " . $delayed . $ignore . "INTO " . $table . " VALUES (");
828
                    $columns = count($row);
829
                    for ($j = 0; $j < $columns; $j++) {
830
                        // 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)
831
                        if ($row[$j] == null && !is_string($row[$j])) {
832
                            // IMPORTANT: if the field is NULL we set it NULL
833
                            $row[$j] = 'NULL';
834
                        } elseif (is_string($row[$j]) && $row[$j] == '') {
835
                            // if it's an empty string, we set it as an empty string
836
                            $row[$j] = "''";
837
                        } 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)
838
                            // if it's a number, we return it as-is
839
                            //                      $row[$j] = $row[$j];
840
                        } else { // else for all other cases we escape the value and put quotes around
841
                            $row[$j] = addslashes($row[$j]);
842
                            $row[$j] = preg_replace("#\n#", "\\n", $row[$j]);
843
                            $row[$j] = "'" . $row[$j] . "'";
844
                        }
845
                    }
846
                    fwrite($handle, implode(',', $row) . ");\n");
847
                }
848
                if (GETPOST("nobin_disable_fk")) {
849
                    fwrite($handle, "ALTER TABLE `" . $table . "` ENABLE KEYS;\n"); // Enabling back the keys/index checking
850
                }
851
                if (!GETPOST("nobin_nolocks")) {
852
                    fwrite($handle, "UNLOCK TABLES;\n"); // Unlocking the table
853
                }
854
                fwrite($handle, "\n\n\n");
855
            }
856
        }
857
858
        /* Backup Procedure structure*/
859
        /*
860
         $result = $db->query('SHOW PROCEDURE STATUS');
861
         if ($db->num_rows($result) > 0)
862
         {
863
         while ($row = $db->fetch_row($result)) { $procedures[] = $row[1]; }
864
         foreach($procedures as $proc)
865
         {
866
         fwrite($handle,"DELIMITER $$\n\n");
867
         fwrite($handle,"DROP PROCEDURE IF EXISTS '$name'.'$proc'$$\n");
868
         $resqlcreateproc=$db->query("SHOW CREATE PROCEDURE '$proc'");
869
         $row2 = $db->fetch_row($resqlcreateproc);
870
         fwrite($handle,"\n".$row2[2]."$$\n\n");
871
         fwrite($handle,"DELIMITER ;\n\n");
872
         }
873
         }
874
         */
875
        /* Backup Procedure structure*/
876
877
        // Write the footer (restore the previous database settings)
878
        $sqlfooter = "\n\n";
879
        if (GETPOST("nobin_use_transaction")) {
880
            $sqlfooter .= "COMMIT;\n";
881
        }
882
        if (GETPOST("nobin_disable_fk")) {
883
            $sqlfooter .= "SET FOREIGN_KEY_CHECKS=1;\n";
884
        }
885
        $sqlfooter .= "\n\n-- Dump completed on " . date('Y-m-d G-i-s');
886
        fwrite($handle, $sqlfooter);
887
888
        fclose($handle);
889
890
        return 1;
891
    }
892
893
    /**
894
     * Generate documentation of a Module
895
     *
896
     * @param string $module Module name
897
     * @return  int                 Return integer <0 if KO, >0 if OK
898
     */
899
    public function generateDoc($module)
900
    {
901
        global $conf, $langs, $user, $mysoc;
902
        global $dirins;
903
904
        $error = 0;
905
906
        $modulelowercase = strtolower($module);
907
        $now = dol_now();
908
909
        // Dir for module
910
        $dir = $dirins . '/' . $modulelowercase;
911
        // Zip file to build
912
        $FILENAMEDOC = '';
913
914
        // Load module
915
        dol_include_once($modulelowercase . '/core/modules/mod' . $module . '.class.php');
916
        $class = 'mod' . $module;
917
918
        if (class_exists($class)) {
919
            try {
920
                $moduleobj = new $class($this->db);
921
            } 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...
922
                $error++;
923
                dol_print_error(null, $e->getMessage());
924
            }
925
        } else {
926
            $error++;
927
            $langs->load("errors");
928
            dol_print_error(null, $langs->trans("ErrorFailedToLoadModuleDescriptorForXXX", $module));
929
            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...
930
        }
931
932
        $arrayversion = explode('.', $moduleobj->version, 3);
933
        if (count($arrayversion)) {
934
            $FILENAMEASCII = strtolower($module) . '.asciidoc';
935
            $FILENAMEDOC = strtolower($module) . '.html';
936
            $FILENAMEDOCPDF = strtolower($module) . '.pdf';
937
938
            $dirofmodule = dol_buildpath(strtolower($module), 0);
939
            $dirofmoduledoc = dol_buildpath(strtolower($module), 0) . '/doc';
940
            $dirofmoduletmp = dol_buildpath(strtolower($module), 0) . '/doc/temp';
941
            $outputfiledoc = $dirofmoduledoc . '/' . $FILENAMEDOC;
942
            if ($dirofmoduledoc) {
943
                if (!dol_is_dir($dirofmoduledoc)) {
944
                    dol_mkdir($dirofmoduledoc);
945
                }
946
                if (!dol_is_dir($dirofmoduletmp)) {
947
                    dol_mkdir($dirofmoduletmp);
948
                }
949
                if (!is_writable($dirofmoduletmp)) {
950
                    $this->error = 'Dir ' . $dirofmoduletmp . ' does not exists or is not writable';
951
                    return -1;
952
                }
953
954
                if (!getDolGlobalString('MODULEBUILDER_ASCIIDOCTOR') && !getDolGlobalString('MODULEBUILDER_ASCIIDOCTORPDF')) {
955
                    $this->error = 'Setup of module ModuleBuilder not complete';
956
                    return -1;
957
                }
958
959
                // Copy some files into temp directory, so instruction include::ChangeLog.md[] will works inside the asciidoc file.
960
                dol_copy($dirofmodule . '/README.md', $dirofmoduletmp . '/README.md', 0, 1);
961
                dol_copy($dirofmodule . '/ChangeLog.md', $dirofmoduletmp . '/ChangeLog.md', 0, 1);
962
963
                // Replace into README.md and ChangeLog.md (in case they are included into documentation with tag __README__ or __CHANGELOG__)
964
                $arrayreplacement = array();
965
                $arrayreplacement['/^#\s.*/m'] = ''; // Remove first level of title into .md files
966
                $arrayreplacement['/^#/m'] = '##'; // Add on # to increase level
967
968
                dolReplaceInFile($dirofmoduletmp . '/README.md', $arrayreplacement, '', 0, 0, 1);
969
                dolReplaceInFile($dirofmoduletmp . '/ChangeLog.md', $arrayreplacement, '', 0, 0, 1);
970
971
972
                $destfile = $dirofmoduletmp . '/' . $FILENAMEASCII;
973
974
                $fhandle = fopen($destfile, 'w+');
975
                if ($fhandle) {
976
                    $specs = dol_dir_list(dol_buildpath(strtolower($module) . '/doc', 0), 'files', 1, '(\.md|\.asciidoc)$', array('\/temp\/'));
977
978
                    $i = 0;
979
                    foreach ($specs as $spec) {
980
                        if (preg_match('/notindoc/', $spec['relativename'])) {
981
                            continue; // Discard file
982
                        }
983
                        if (preg_match('/example/', $spec['relativename'])) {
984
                            continue; // Discard file
985
                        }
986
                        if (preg_match('/disabled/', $spec['relativename'])) {
987
                            continue; // Discard file
988
                        }
989
990
                        $pathtofile = strtolower($module) . '/doc/' . $spec['relativename'];
991
                        $format = 'asciidoc';
992
                        if (preg_match('/\.md$/i', $spec['name'])) {
993
                            $format = 'markdown';
994
                        }
995
996
                        $filecursor = @file_get_contents($spec['fullname']);
997
                        if ($filecursor) {
998
                            fwrite($fhandle, ($i ? "\n<<<\n\n" : "") . $filecursor . "\n");
999
                        } else {
1000
                            $this->error = 'Failed to concat content of file ' . $spec['fullname'];
1001
                            return -1;
1002
                        }
1003
1004
                        $i++;
1005
                    }
1006
1007
                    fclose($fhandle);
1008
1009
                    $contentreadme = file_get_contents($dirofmoduletmp . '/README.md');
1010
                    $contentchangelog = file_get_contents($dirofmoduletmp . '/ChangeLog.md');
1011
1012
                    include DOL_DOCUMENT_ROOT . '/core/lib/parsemd.lib.php';
1013
1014
                    //var_dump($phpfileval['fullname']);
1015
                    $arrayreplacement = array(
1016
                        'mymodule' => strtolower($module),
1017
                        'MyModule' => $module,
1018
                        'MYMODULE' => strtoupper($module),
1019
                        'My module' => $module,
1020
                        'my module' => $module,
1021
                        'Mon module' => $module,
1022
                        'mon module' => $module,
1023
                        'htdocs/modulebuilder/template' => strtolower($module),
1024
                        '__MYCOMPANY_NAME__' => $mysoc->name,
1025
                        '__KEYWORDS__' => $module,
1026
                        '__USER_FULLNAME__' => $user->getFullName($langs),
1027
                        '__USER_EMAIL__' => $user->email,
1028
                        '__YYYY-MM-DD__' => dol_print_date($now, 'dayrfc'),
1029
                        '---Put here your own copyright and developer email---' => dol_print_date($now, 'dayrfc') . ' ' . $user->getFullName($langs) . ($user->email ? ' <' . $user->email . '>' : ''),
1030
                        '__DATA_SPECIFICATION__' => 'Not yet available',
1031
                        '__README__' => dolMd2Asciidoc($contentreadme),
1032
                        '__CHANGELOG__' => dolMd2Asciidoc($contentchangelog),
1033
                    );
1034
1035
                    // @phan-suppress-next-line PhanPluginSuspiciousParamPosition
1036
                    dolReplaceInFile($destfile, $arrayreplacement);
1037
                }
1038
1039
                // Launch doc generation
1040
                $currentdir = getcwd();
1041
                chdir($dirofmodule);
1042
1043
                require_once constant('DOL_DOCUMENT_ROOT') . '/core/class/utils.class.php';
1044
                $utils = new Utils($this->db);
1045
1046
                // Build HTML doc
1047
                $command = getDolGlobalString('MODULEBUILDER_ASCIIDOCTOR') . ' ' . $destfile . ' -n -o ' . $dirofmoduledoc . '/' . $FILENAMEDOC;
1048
                $outfile = $dirofmoduletmp . '/out.tmp';
1049
1050
                $resarray = $utils->executeCLI($command, $outfile);
1051
                if ($resarray['result'] != '0') {
1052
                    $this->error = $resarray['error'] . ' ' . $resarray['output'];
1053
                    $this->errors[] = $this->error;
1054
                }
1055
                $result = ($resarray['result'] == 0) ? 1 : 0;
1056
                if ($result < 0 && empty($this->errors)) {
1057
                    $this->error = $langs->trans("ErrorFailToGenerateFile", $FILENAMEDOC);
1058
                    $this->errors[] = $this->error;
1059
                }
1060
1061
                // Build PDF doc
1062
                $command = getDolGlobalString('MODULEBUILDER_ASCIIDOCTORPDF') . ' ' . $destfile . ' -n -o ' . $dirofmoduledoc . '/' . $FILENAMEDOCPDF;
1063
                $outfile = $dirofmoduletmp . '/outpdf.tmp';
1064
                $resarray = $utils->executeCLI($command, $outfile);
1065
                if ($resarray['result'] != '0') {
1066
                    $this->error = $resarray['error'] . ' ' . $resarray['output'];
1067
                    $this->errors[] = $this->error;
1068
                }
1069
                $result = ($resarray['result'] == 0) ? 1 : 0;
1070
                if ($result < 0 && empty($this->errors)) {
1071
                    $this->error = $langs->trans("ErrorFailToGenerateFile", $FILENAMEDOCPDF);
1072
                    $this->errors[] = $this->error;
1073
                }
1074
1075
                chdir($currentdir);
1076
            } else {
1077
                $result = 0;
1078
            }
1079
1080
            if ($result > 0) {
1081
                return 1;
1082
            } else {
1083
                $error++;
1084
            }
1085
        } else {
1086
            $error++;
1087
            $langs->load("errors");
1088
            $this->error = $langs->trans("ErrorCheckVersionIsDefined");
1089
        }
1090
1091
        return -1;
1092
    }
1093
1094
    /**
1095
     * Execute a CLI command.
1096
     *
1097
     * @param string $command Command line to execute.
1098
     *                                      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.
1099
     * @param string $outputfile A path for an output file (used only when method is 2). For example: $conf->admin->dir_temp.'/out.tmp';
1100
     * @param int $execmethod 0=Use default method (that is 1 by default), 1=Use the PHP 'exec', 2=Use the 'popen' method
1101
     * @param string $redirectionfile If defined, a redirection of output to this file is added.
1102
     * @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.
1103
     *                                      This parameter is provided for backward compatibility with external modules. Always use 0 in core.
1104
     * @param string $redirectionfileerr If defined, a redirection of error is added to this file instead of to channel 1.
1105
     * @return  array                       array('result'=>...,'output'=>...,'error'=>...). result = 0 means OK.
1106
     */
1107
    public function executeCLI($command, $outputfile, $execmethod = 0, $redirectionfile = null, $noescapecommand = 0, $redirectionfileerr = null)
1108
    {
1109
        global $conf, $langs;
1110
1111
        $result = 0;
1112
        $output = '';
1113
        $error = '';
1114
1115
        if (empty($noescapecommand)) {
1116
            $command = escapeshellcmd($command);
1117
        }
1118
1119
        if ($redirectionfile) {
1120
            $command .= " > " . dol_sanitizePathName($redirectionfile);
1121
        }
1122
1123
        if ($redirectionfileerr && ($redirectionfileerr != $redirectionfile)) {
1124
            // If we ask a redirect of stderr on a given file not already used for stdout
1125
            $command .= " 2> " . dol_sanitizePathName($redirectionfileerr);
1126
        } else {
1127
            $command .= " 2>&1";
1128
        }
1129
1130
        if (getDolGlobalString('MAIN_EXEC_USE_POPEN')) {
1131
            $execmethod = getDolGlobalString('MAIN_EXEC_USE_POPEN');
1132
        }
1133
        if (empty($execmethod)) {
1134
            $execmethod = 1;
1135
        }
1136
        //$execmethod=1;
1137
        dol_syslog("Utils::executeCLI execmethod=" . $execmethod . " command=" . $command, LOG_DEBUG);
1138
        $output_arr = array();
1139
1140
        if ($execmethod == 1) {
1141
            $retval = null;
1142
            exec($command, $output_arr, $retval);
1143
            $result = $retval;
1144
            if ($retval != 0) {
1145
                $langs->load("errors");
1146
                dol_syslog("Utils::executeCLI retval after exec=" . $retval, LOG_ERR);
1147
                $error = 'Error ' . $retval;
1148
            }
1149
        }
1150
        if ($execmethod == 2) { // With this method, there is no way to get the return code, only output
1151
            $handle = fopen($outputfile, 'w+b');
1152
            if ($handle) {
1153
                dol_syslog("Utils::executeCLI run command " . $command);
1154
                $handlein = popen($command, 'r');
1155
                while (!feof($handlein)) {
1156
                    $read = fgets($handlein);
1157
                    fwrite($handle, $read);
1158
                    $output_arr[] = $read;
1159
                }
1160
                pclose($handlein);
1161
                fclose($handle);
1162
            }
1163
            dolChmod($outputfile);
1164
        }
1165
1166
        // Update with result
1167
        if (is_array($output_arr) && count($output_arr) > 0) {
1168
            foreach ($output_arr as $val) {
1169
                $output .= $val . ($execmethod == 2 ? '' : "\n");
1170
            }
1171
        }
1172
1173
        dol_syslog("Utils::executeCLI result=" . $result . " output=" . $output . " error=" . $error, LOG_DEBUG);
1174
1175
        return array('result' => $result, 'output' => $output, 'error' => $error);
1176
    }
1177
1178
    /**
1179
     * This saves syslog files and compresses older ones.
1180
     * Nb of archive to keep is defined into $conf->global->SYSLOG_FILE_SAVES
1181
     * CAN BE A CRON TASK
1182
     *
1183
     * @return  int                     0 if OK, < 0 if KO
1184
     */
1185
    public function compressSyslogs()
1186
    {
1187
        global $conf;
1188
1189
        if (empty($conf->loghandlers['mod_syslog_file'])) { // File Syslog disabled
1190
            return 0;
1191
        }
1192
1193
        if (!function_exists('gzopen')) {
1194
            $this->error = 'Support for gzopen not available in this PHP';
1195
            return -1;
1196
        }
1197
1198
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
1199
1200
        $nbSaves = intval(getDolGlobalString('SYSLOG_FILE_SAVES', 10));
1201
1202
        if (!getDolGlobalString('SYSLOG_FILE')) {
1203
            $mainlogdir = DOL_DATA_ROOT;
1204
            $mainlog = 'alixar.log';
1205
        } else {
1206
            $mainlogfull = str_replace('DOL_DATA_ROOT', DOL_DATA_ROOT, $conf->global->SYSLOG_FILE);
1207
            $mainlogdir = dirname($mainlogfull);
1208
            $mainlog = basename($mainlogfull);
1209
        }
1210
1211
        $tabfiles = dol_dir_list(DOL_DATA_ROOT, 'files', 0, '^(dolibarr_.+|odt2pdf)\.log$'); // Also handle other log files like dolibarr_install.log
1212
        $tabfiles[] = array('name' => $mainlog, 'path' => $mainlogdir);
1213
1214
        foreach ($tabfiles as $file) {
1215
            $logname = $file['name'];
1216
            $logpath = $file['path'];
1217
1218
            if (dol_is_file($logpath . '/' . $logname) && dol_filesize($logpath . '/' . $logname) > 0) {    // If log file exists and is not empty
1219
                // Handle already compressed files to rename them and add +1
1220
1221
                $filter = '^' . preg_quote($logname, '/') . '\.([0-9]+)\.gz$';
1222
1223
                $gzfilestmp = dol_dir_list($logpath, 'files', 0, $filter);
1224
                $gzfiles = array();
1225
1226
                foreach ($gzfilestmp as $gzfile) {
1227
                    $tabmatches = array();
1228
                    preg_match('/' . $filter . '/i', $gzfile['name'], $tabmatches);
1229
1230
                    $numsave = intval($tabmatches[1]);
1231
1232
                    $gzfiles[$numsave] = $gzfile;
1233
                }
1234
1235
                krsort($gzfiles, SORT_NUMERIC);
1236
1237
                foreach ($gzfiles as $numsave => $dummy) {
1238
                    if (dol_is_file($logpath . '/' . $logname . '.' . ($numsave + 1) . '.gz')) {
1239
                        return -2;
1240
                    }
1241
1242
                    if ($numsave >= $nbSaves) {
1243
                        dol_delete_file($logpath . '/' . $logname . '.' . $numsave . '.gz', 0, 0, 0, null, false, 0);
1244
                    } else {
1245
                        dol_move($logpath . '/' . $logname . '.' . $numsave . '.gz', $logpath . '/' . $logname . '.' . ($numsave + 1) . '.gz', 0, 1, 0, 0);
1246
                    }
1247
                }
1248
1249
                // Compress current file and recreate it
1250
1251
                if ($nbSaves > 0) {         // If $nbSaves is 1, we keep 1 archive .gz file, If 2, we keep 2 .gz files
1252
                    $gzfilehandle = gzopen($logpath . '/' . $logname . '.1.gz', 'wb9');
1253
1254
                    if (empty($gzfilehandle)) {
1255
                        $this->error = 'Failted to open file ' . $logpath . '/' . $logname . '.1.gz';
1256
                        return -3;
1257
                    }
1258
1259
                    $sourcehandle = fopen($logpath . '/' . $logname, 'r');
1260
1261
                    if (empty($sourcehandle)) {
1262
                        $this->error = 'Failed to open file ' . $logpath . '/' . $logname;
1263
                        return -4;
1264
                    }
1265
1266
                    while (!feof($sourcehandle)) {
1267
                        gzwrite($gzfilehandle, fread($sourcehandle, 512 * 1024)); // Read 512 kB at a time
1268
                    }
1269
1270
                    fclose($sourcehandle);
1271
                    gzclose($gzfilehandle);
1272
1273
                    dolChmod($logpath . '/' . $logname . '.1.gz');
1274
                }
1275
1276
                dol_delete_file($logpath . '/' . $logname, 0, 0, 0, null, false, 0);
1277
1278
                // Create empty file
1279
                $newlog = fopen($logpath . '/' . $logname, 'a+');
1280
                fclose($newlog);
1281
1282
                //var_dump($logpath.'/'.$logname." - ".octdec(empty($conf->global->MAIN_UMASK)?'0664':$conf->global->MAIN_UMASK));
1283
                dolChmod($logpath . '/' . $logname);
1284
            }
1285
        }
1286
1287
        $this->output = 'Archive log files (keeping last SYSLOG_FILE_SAVES=' . $nbSaves . ' files) done.';
1288
        return 0;
1289
    }
1290
1291
    /**
1292
     *  Make a send last backup of database or fil in param
1293
     *  CAN BE A CRON TASK
1294
     *
1295
     * @param string $sendto Recipients emails
1296
     * @param string $from Sender email
1297
     * @param string $subject Topic/Subject of mail
1298
     * @param string $message Message
1299
     * @param string $filename List of files to attach (full path of filename on file system)
1300
     * @param string $filter Filter file send
1301
     * @param int $sizelimit Limit size to send file
1302
     * @return int                          0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
1303
     */
1304
    public function sendBackup($sendto = '', $from = '', $subject = '', $message = '', $filename = '', $filter = '', $sizelimit = 100000000)
1305
    {
1306
        global $conf, $langs;
1307
        global $dolibarr_main_url_root;
1308
1309
        $filepath = '';
1310
        $output = '';
1311
        $error = 0;
1312
1313
        if (!empty($from)) {
1314
            $from = dol_escape_htmltag($from);
1315
        } elseif (getDolGlobalString('MAIN_INFO_SOCIETE_MAIL')) {
1316
            $from = dol_escape_htmltag(getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'));
1317
        } else {
1318
            $error++;
1319
        }
1320
1321
        if (!empty($sendto)) {
1322
            $sendto = dol_escape_htmltag($sendto);
1323
        } elseif (getDolGlobalString('MAIN_INFO_SOCIETE_MAIL')) {
1324
            $from = dol_escape_htmltag(getDolGlobalString('MAIN_INFO_SOCIETE_MAIL'));
1325
        } else {
1326
            $error++;
1327
        }
1328
1329
        if (!empty($subject)) {
1330
            $subject = dol_escape_htmltag($subject);
1331
        } else {
1332
            $subject = dol_escape_htmltag($langs->trans('MakeSendLocalDatabaseDumpShort'));
1333
        }
1334
1335
        if (empty($message)) {
1336
            $message = dol_escape_htmltag($langs->trans('MakeSendLocalDatabaseDumpShort'));
1337
        }
1338
1339
        $tmpfiles = array();
1340
        require_once constant('DOL_DOCUMENT_ROOT') . '/core/lib/files.lib.php';
1341
        if ($filename) {
1342
            if (dol_is_file($conf->admin->dir_output . '/backup/' . $filename)) {
1343
                $tmpfiles = dol_most_recent_file($conf->admin->dir_output . '/backup', $filename);
1344
            }
1345
        } else {
1346
            $tmpfiles = dol_most_recent_file($conf->admin->dir_output . '/backup', $filter);
1347
        }
1348
        if ($tmpfiles && is_array($tmpfiles)) {
1349
            foreach ($tmpfiles as $key => $val) {
1350
                if ($key == 'fullname') {
1351
                    $filepath = array($val);
1352
                    $filesize = dol_filesize($val);
1353
                }
1354
                if ($key == 'type') {
1355
                    $mimetype = array($val);
1356
                }
1357
                if ($key == 'relativename') {
1358
                    $filename = array($val);
1359
                }
1360
            }
1361
        }
1362
1363
        if ($filepath) {
1364
            if ($filesize > $sizelimit) {
1365
                $message .= '<br>' . $langs->trans("BackupIsTooLargeSend");
1366
                $documenturl = $dolibarr_main_url_root . '/document.php?modulepart=systemtools&atachement=1&file=backup/' . urlencode($filename[0]);
1367
                $message .= '<br><a href=' . $documenturl . '>Lien de téléchargement</a>';
1368
                $filepath = '';
1369
                $mimetype = '';
1370
                $filename = '';
1371
            }
1372
        } else {
1373
            $output = 'No backup file found';
1374
            $error++;
1375
        }
1376
1377
        if (!$error) {
1378
            include_once DOL_DOCUMENT_ROOT . '/core/class/CMailFile.class.php';
1379
            $mailfile = new CMailFile($subject, $sendto, $from, $message, $filepath, $mimetype, $filename, '', '', 0, -1);
1380
            if ($mailfile->error) {
1381
                $error++;
1382
                $output = $mailfile->error;
1383
            }
1384
        }
1385
1386
        $result = false;
1387
        if (!$error) {
1388
            $result = $mailfile->sendfile();
1389
            if (!$result) {
1390
                $error++;
1391
                $output = $mailfile->error;
1392
            }
1393
        }
1394
1395
        dol_syslog(__METHOD__, LOG_DEBUG);
1396
1397
        $this->error = "Error sending backp file " . ((string)$error);
1398
        $this->output = $output;
1399
1400
        if ($result) {
1401
            return 0;
1402
        } else {
1403
            return -1;
1404
        }
1405
    }
1406
1407
    /**
1408
     *  Clean unfinished cronjob in processing when pid is no longer present in the system
1409
     *  CAN BE A CRON TASK
1410
     *
1411
     * @return    int                               0 if OK, < 0 if KO (this function is used also by cron so only 0 is OK)
1412
     * @throws Exception
1413
     */
1414
    public function cleanUnfinishedCronjob()
1415
    {
1416
        global $db, $user;
1417
        dol_syslog("Utils::cleanUnfinishedCronjob Starting cleaning");
1418
1419
        // Import Cronjob class if not present
1420
        require_once constant('DOL_DOCUMENT_ROOT') . '/cron/class/cronjob.class.php';
1421
1422
        // Get this job object
1423
        $this_job = new Cronjob($db);
1424
        $this_job->fetch(-1, 'Utils', 'cleanUnfinishedCronjob');
1425
        if (empty($this_job->id) || !empty($this_job->error)) {
1426
            dol_syslog("Utils::cleanUnfinishedCronjob Unable to fetch himself: " . $this_job->error, LOG_ERR);
1427
            return -1;
1428
        }
1429
1430
        // Set this job processing to 0 to avoid being locked by his processing state
1431
        $this_job->processing = 0;
1432
        if ($this_job->update($user) < 0) {
1433
            dol_syslog("Utils::cleanUnfinishedCronjob Unable to update himself: " . implode(', ', $this_job->errors), LOG_ERR);
1434
            return -1;
1435
        }
1436
1437
        $cron_job = new Cronjob($db);
1438
        $cron_job->fetchAll('DESC', 't.rowid', 100, 0, 1, [], 1);   // Fetch jobs that are currently running
1439
1440
        // Iterate over all jobs in processing (this can't be this job since his state is set to 0 before)
1441
        foreach ($cron_job->lines as $job_line) {
1442
            // Avoid job with no PID
1443
            if (empty($job_line->pid)) {
1444
                dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " don't have a PID", LOG_DEBUG);
1445
                continue;
1446
            }
1447
1448
            $job = new Cronjob($db);
1449
            $job->fetch($job_line->id);
1450
            if (empty($job->id) || !empty($job->error)) {
1451
                dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " can't be fetch: " . $job->error, LOG_ERR);
1452
                continue;
1453
            }
1454
1455
            // Calling posix_kill with the 0 kill signal will return true if the process is running, false otherwise.
1456
            if (!posix_kill($job->pid, 0)) {
1457
                // Clean processing and pid values
1458
                $job->processing = 0;
1459
                $job->pid = null;
1460
1461
                // Set last result as an error and add the reason on the last output
1462
                $job->lastresult = strval(-1);
1463
                $job->lastoutput = 'Job killed by job cleanUnfinishedCronjob';
1464
1465
                if ($job->update($user) < 0) {
1466
                    dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " can't be updated: " . implode(', ', $job->errors), LOG_ERR);
1467
                    continue;
1468
                }
1469
                dol_syslog("Utils::cleanUnfinishedCronjob Cronjob " . $job_line->id . " cleaned");
1470
            }
1471
        }
1472
1473
        dol_syslog("Utils::cleanUnfinishedCronjob Cleaning completed");
1474
        return 0;
1475
    }
1476
}
1477