Passed
Pull Request — master (#5041)
by
unknown
06:24
created

tpCliIsInteractive()   A

Complexity

Conditions 4
Paths 7

Size

Total Lines 13
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 7
c 1
b 0
f 0
nc 7
nop 0
dl 0
loc 13
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      restore.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2026 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use TeampassClasses\ConfigManager\ConfigManager;
33
34
require_once __DIR__ . '/../sources/main.functions.php';
35
require_once __DIR__ . '/../sources/backup.functions.php';
36
37
loadClasses('DB');
38
39
function tpCliOut(string $msg): void
40
{
41
    fwrite(STDOUT, $msg . PHP_EOL);
42
}
43
44
function tpCliErr(string $msg): void
45
{
46
    fwrite(STDERR, $msg . PHP_EOL);
47
}
48
49
function tpCliHelp(): void
50
{
51
    tpCliOut('Usage: php scripts/restore.php --file "/path/to/backup.sql" --auth-token "TOKEN" [--force-disconnect]');
52
}
53
54
function tpCliIsInteractive(): bool
55
{
56
    try {
57
        if (function_exists('stream_isatty')) {
58
            return @stream_isatty(STDERR);
59
        }
60
        if (function_exists('posix_isatty')) {
61
            return @posix_isatty(STDERR);
62
        }
63
    } catch (Throwable $e) {
64
        // ignore
65
    }
66
    return false;
67
}
68
69
/**
70
 * Render a single-line progress bar on STDERR.
71
 * - Interactive terminals: updates in place (\r)
72
 * - Non-interactive: prints a line every 5% (and at 100%)
73
 */
74
function tpCliProgressBar(int $pct, int $totalExecuted, bool $interactive): void
75
{
76
    static $lastPct = -1;
77
    static $lastLen = 0;
78
    static $lastTs = 0.0;
79
80
    $pct = max(0, min(100, $pct));
81
82
    if (!$interactive) {
83
        if ($pct !== $lastPct && ($pct % 5 === 0 || $pct === 100)) {
84
            fwrite(STDERR, 'Progress: ' . $pct . '% (statements executed: ' . $totalExecuted . ')' . PHP_EOL);
85
            $lastPct = $pct;
86
        }
87
        return;
88
    }
89
90
    $now = microtime(true);
91
    if ($pct === $lastPct && ($now - $lastTs) < 0.20) {
92
        return;
93
    }
94
95
    $lastTs = $now;
96
    $lastPct = $pct;
97
98
    $width = 30;
99
    $filled = (int) round(($pct / 100) * $width);
100
    $bar = str_repeat('#', $filled) . str_repeat('-', $width - $filled);
101
    $msg = sprintf("\r[%s] %3d%% | statements: %d", $bar, $pct, $totalExecuted);
102
103
    $pad = max(0, $lastLen - strlen($msg));
104
    fwrite(STDERR, $msg . str_repeat(' ', $pad));
105
    $lastLen = strlen($msg);
106
107
    if ($pct === 100) {
108
        fwrite(STDERR, PHP_EOL);
109
    }
110
}
111
112
if (PHP_SAPI !== 'cli') {
113
    tpCliErr('ERROR: This script must be executed in CLI.');
114
    exit(2);
115
}
116
117
// Parse CLI options
118
$options = getopt('', ['file:', 'auth-token:', 'force-disconnect', 'help']);
119
if (isset($options['help']) || empty($options['file']) || empty($options['auth-token'])) {
120
    tpCliHelp();
121
    exit(isset($options['help']) ? 0 : 2);
122
}
123
124
$file = (string) $options['file'];
125
$authToken = (string) $options['auth-token'];
126
$forceDisconnect = array_key_exists('force-disconnect', $options);
127
128
$interactive = tpCliIsInteractive();
129
130
// Load settings
131
$configManager = new ConfigManager();
132
$SETTINGS = $configManager->getAllSettings();
133
134
// Logging
135
$filesDir = rtrim((string) ($SETTINGS['path_to_files_folder'] ?? sys_get_temp_dir()), '/');
136
$logDir = $filesDir . '/restore_cli_logs';
137
if (!is_dir($logDir)) {
138
    @mkdir($logDir, 0700, true);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for mkdir(). 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

138
    /** @scrutinizer ignore-unhandled */ @mkdir($logDir, 0700, true);

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...
139
}
140
$logFile = $logDir . '/restore_' . date('Ymd_His') . '.log';
141
142
$log = function (string $level, string $message) use ($logFile): void {
143
    $line = '[' . date('c') . '][' . $level . '] ' . $message . PHP_EOL;
144
    @file_put_contents($logFile, $line, FILE_APPEND);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_put_contents(). 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

144
    /** @scrutinizer ignore-unhandled */ @file_put_contents($logFile, $line, FILE_APPEND);

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...
145
    if ($level === 'ERROR') {
146
        tpCliErr($message);
147
    } else {
148
        tpCliOut($message);
149
    }
150
};
151
152
// Lock (prevent concurrent restores)
153
// Prefer the TeamPass files folder, but fallback to system temp if not writable for the current user.
154
$lockFile = '';
155
$lockHandle = false;
156
$lockCandidates = [
157
    $filesDir . '/restore_cli.lock',
158
    rtrim((string) sys_get_temp_dir(), '/\\') . '/teampass_restore_cli_' . substr(md5($filesDir), 0, 12) . '.lock',
159
];
160
161
foreach ($lockCandidates as $candidate) {
162
    $h = @fopen($candidate, 'c');
163
    if ($h !== false) {
164
        $lockFile = $candidate;
165
        $lockHandle = $h;
166
        break;
167
    }
168
}
169
170
if ($lockHandle === false) {
171
    $log('ERROR', 'Unable to open any lock file. Tried: ' . implode(', ', $lockCandidates));
172
    exit(20);
173
}
174
175
if (!flock($lockHandle, LOCK_EX | LOCK_NB)) {
176
    $log('ERROR', 'Another restore seems to be in progress (lock: ' . $lockFile . ').');
177
    exit(20);
178
}
179
180
@ftruncate($lockHandle, 0);
181
@fwrite($lockHandle, (string) getmypid() . ' ' . date('c') . PHP_EOL);
182
183
// Normalize file path
184
$realFile = realpath($file);
185
if ($realFile !== false) {
186
    $file = $realFile;
187
}
188
189
if (!is_file($file)) {
190
    $log('ERROR', 'Backup file not found: ' . $file);
191
    exit(3);
192
}
193
194
$tokenHash = hash('sha256', $authToken);
195
196
// Token verification (DB)
197
$fetch = tpRestoreAuthorizationFetchByHash($tokenHash);
198
if (empty($fetch['success'])) {
199
    $log('ERROR', 'Authorization token not found or invalid.');
200
    exit(30);
201
}
202
203
$authId = (int) ($fetch['id'] ?? 0);
204
$payload = $fetch['payload'] ?? [];
205
if (!is_array($payload)) {
206
    $log('ERROR', 'Authorization payload is invalid.');
207
    exit(30);
208
}
209
210
$now = time();
211
$status = (string) ($payload['status'] ?? '');
212
$expiresAt = (int) ($payload['expires_at'] ?? 0);
213
$payloadFilePath = (string) (($payload['file']['path'] ?? ''));
214
215
if ($status !== 'pending') {
216
    $log('ERROR', 'Authorization token is not pending (status=' . $status . ').');
217
    exit(31);
218
}
219
220
if ($expiresAt > 0 && $expiresAt < $now) {
221
    $log('ERROR', 'Authorization token is expired.');
222
    exit(32);
223
}
224
225
// Ensure the token matches the file we are about to restore
226
$cmp1 = $payloadFilePath;
227
$cmp2 = $file;
228
$cmp1Real = ($cmp1 !== '') ? realpath($cmp1) : false;
229
if ($cmp1Real !== false) {
230
    $cmp1 = $cmp1Real;
231
}
232
$cmp2Real = realpath($cmp2);
233
if ($cmp2Real !== false) {
234
    $cmp2 = $cmp2Real;
235
}
236
237
if ($cmp1 === '' || $cmp1 !== $cmp2) {
238
    $log('ERROR', 'Authorization token does not match the provided --file path.');
239
    $log('ERROR', 'Expected: ' . $payloadFilePath);
240
    $log('ERROR', 'Provided: ' . $file);
241
    exit(33);
242
}
243
244
// Always disconnect the restore initiator right away (so the admin can safely run the CLI).
245
$initiatorId = (int) (($payload['initiator']['id'] ?? 0));
246
if ($initiatorId > 0) {
247
    try {
248
        DB::update(
249
            prefixTable('users'),
250
            [
251
                'timestamp' => '',
252
                'key_tempo' => '',
253
                'session_end' => '',
254
            ],
255
            'id = %i',
256
            $initiatorId
257
        );
258
        $log('INFO', 'Disconnected restore initiator (user id=' . $initiatorId . ').');
259
    } catch (Throwable $e) {
260
        $log('WARN', 'Unable to disconnect restore initiator: ' . $e->getMessage());
261
    }
262
}
263
264
// Connected users detection (web + API if possible)
265
$webUsers = [];
266
$apiUsers = [];
267
try {
268
    if ($initiatorId > 0) {
269
        $webUsers = DB::query(
270
            'SELECT id, login, name, lastname, session_end
271
             FROM ' . prefixTable('users') . '
272
             WHERE session_end >= %i AND id != %i',
273
            $now,
274
            $initiatorId
275
        );
276
    } else {
277
        $webUsers = DB::query(
278
            'SELECT id, login, name, lastname, session_end
279
             FROM ' . prefixTable('users') . '
280
             WHERE session_end >= %i',
281
            $now
282
        );
283
    }
284
} catch (Throwable $e) {
285
	// Ensure any in-place progress bar line is terminated before printing errors
286
	if ($interactive) {
287
	    @fwrite(STDERR, PHP_EOL);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fwrite(). 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

287
	    /** @scrutinizer ignore-unhandled */ @fwrite(STDERR, PHP_EOL);

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...
288
	}
289
    $log('WARN', 'Unable to check web sessions: ' . $e->getMessage());
290
}
291
292
try {
293
    $apiTokenDuration = 3600;
294
    if (isset($SETTINGS['api_token_duration']) && is_numeric($SETTINGS['api_token_duration'])) {
295
        $apiTokenDuration = (int) $SETTINGS['api_token_duration'];
296
    }
297
    if ($apiTokenDuration <= 0) {
298
        $apiTokenDuration = 3600;
299
    }
300
    $apiConnectedAfter = $now - ($apiTokenDuration + 600);
301
302
    $apiUsers = DB::query(
303
        'SELECT u.id, u.login, u.name, u.lastname, api_conn.last_api_date AS last_api
304
         FROM ' . prefixTable('users') . ' u
305
         INNER JOIN (
306
            SELECT qui, MAX(date) AS last_api_date
307
            FROM ' . prefixTable('log_system') . '
308
            WHERE type = %s AND label = %s AND date >= %i
309
            GROUP BY qui
310
         ) api_conn ON api_conn.qui = u.id',
311
        'api',
312
        'user_connection',
313
        $apiConnectedAfter
314
    );
315
} catch (Throwable $e) {
316
    // Optional: ignore API checks if unavailable
317
    $log('WARN', 'Unable to check API activity: ' . $e->getMessage());
318
}
319
320
// Enforce disconnect policy for web sessions
321
if (!empty($webUsers) && $forceDisconnect === false) {
322
    $log('ERROR', 'Active web sessions detected. Re-run with --force-disconnect or disconnect users first.');
323
    foreach ($webUsers as $u) {
324
        $log('ERROR', 'WEB user connected: ' . ($u['login'] ?? '') . ' (' . ($u['name'] ?? '') . ' ' . ($u['lastname'] ?? '') . ')');
325
    }
326
    if (!empty($apiUsers)) {
327
        $log('WARN', 'API activity detected as well (best-effort detection):');
328
        foreach ($apiUsers as $u) {
329
            $log('WARN', 'API activity: ' . ($u['login'] ?? '') . ' (' . ($u['name'] ?? '') . ' ' . ($u['lastname'] ?? '') . ')');
330
        }
331
    }
332
    // IMPORTANT: keep token pending so the operator can retry after disconnecting users.
333
    $payload['last_error'] = 'active_web_sessions';
334
    $payload['last_error_at'] = time();
335
    tpRestoreAuthorizationUpdatePayload($authId, $payload);
336
337
    exit(10);
338
}
339
340
if (!empty($webUsers) && $forceDisconnect === true) {
341
    $log('INFO', 'Active web sessions detected: forcing disconnection.');
342
    try {
343
        DB::update(
344
            prefixTable('users'),
345
            [
346
                'timestamp' => '',
347
                'key_tempo' => '',
348
                'session_end' => '',
349
            ],
350
            'session_end >= %i',
351
            $now
352
        );
353
    } catch (Throwable $e) {
354
        $log('WARN', 'Failed to force-disconnect web users: ' . $e->getMessage());
355
    }
356
}
357
358
// From this point, we consider the restore authorized to start.
359
$payload['status'] = 'in_progress';
360
$payload['started_at'] = time();
361
tpRestoreAuthorizationUpdatePayload($authId, $payload);
362
363
if (!empty($apiUsers)) {
364
    $log('WARN', 'API activity detected (best-effort): those sessions may not be revocable. Proceeding.');
365
    foreach ($apiUsers as $u) {
366
        $log('WARN', 'API activity: ' . ($u['login'] ?? '') . ' (' . ($u['name'] ?? '') . ' ' . ($u['lastname'] ?? '') . ')');
367
    }
368
}
369
370
// Maintenance mode toggle (best-effort)
371
// Keep maintenance enabled at the end of the restore (same behavior as the legacy web restore),
372
// because the SQL dump may restore maintenance_mode to 0.
373
try {
374
    $affected = DB::update(
375
        prefixTable('misc'),
376
        [
377
            'valeur' => '1',
378
            'updated_at' => time(),
379
        ],
380
        'intitule = %s AND type = %s',
381
        'maintenance_mode',
382
        'admin'
383
    );
384
    if ($affected === 0) {
385
        DB::insert(
386
            prefixTable('misc'),
387
            [
388
                'intitule' => 'maintenance_mode',
389
                'type' => 'admin',
390
                'valeur' => '1',
391
                'updated_at' => time(),
392
            ]
393
        );
394
    }
395
} catch (Throwable $e) {
396
    // Fallback without updated_at
397
    try {
398
        $affected = DB::update(
399
            prefixTable('misc'),
400
            [
401
                'valeur' => '1',
402
            ],
403
            'intitule = %s AND type = %s',
404
            'maintenance_mode',
405
            'admin'
406
        );
407
        if ($affected === 0) {
408
            DB::insert(
409
                prefixTable('misc'),
410
                [
411
                    'intitule' => 'maintenance_mode',
412
                    'type' => 'admin',
413
                    'valeur' => '1',
414
                ]
415
            );
416
        }
417
    } catch (Throwable $e2) {
418
        $log('WARN', 'Unable to enable maintenance mode: ' . $e2->getMessage());
419
    }
420
}
421
422
$log('INFO', 'Starting restore from: ' . $file);
423
$log('INFO', 'Log file: ' . $logFile);
424
425
// Build decryption keys to try
426
$keysToTry = [];
427
428
// Secrets from payload (encrypted in DB)
429
try {
430
    $secrets = $payload['secrets'] ?? [];
431
    if (is_array($secrets)) {
432
        if (!empty($secrets['encryptionKey'])) {
433
            $tmp = cryption((string) $secrets['encryptionKey'], '', 'decrypt', $SETTINGS);
434
            $k = isset($tmp['string']) ? (string) $tmp['string'] : '';
435
            if ($k !== '') $keysToTry[] = $k;
436
        }
437
        if (!empty($secrets['overrideKey'])) {
438
            $tmp = cryption((string) $secrets['overrideKey'], '', 'decrypt', $SETTINGS);
439
            $k = isset($tmp['string']) ? (string) $tmp['string'] : '';
440
            if ($k !== '') $keysToTry[] = $k;
441
        }
442
    }
443
} catch (Throwable $e) {
444
    $log('WARN', 'Unable to decrypt stored secrets: ' . $e->getMessage());
445
}
446
447
// Instance key (scheduled backups)
448
try {
449
    if (!empty($SETTINGS['bck_script_passkey'] ?? '') === true) {
450
        $rawInstanceKey = (string) $SETTINGS['bck_script_passkey'];
451
        $tmp = cryption($rawInstanceKey, '', 'decrypt', $SETTINGS);
452
        $decInstanceKey = isset($tmp['string']) ? (string) $tmp['string'] : '';
453
454
        if ($decInstanceKey !== '') {
455
            $keysToTry[] = $decInstanceKey;
456
        }
457
        // Some environments may store bck_script_passkey already in clear
458
        if ($rawInstanceKey !== '' && $rawInstanceKey !== $decInstanceKey) {
459
            $keysToTry[] = $rawInstanceKey;
460
        }
461
    }
462
} catch (Throwable $e) {
463
    $log('WARN', 'Unable to decrypt instance backup key: ' . $e->getMessage());
464
}
465
466
$keysToTry = array_values(array_unique(array_filter($keysToTry, function ($v) { return $v !== ''; })));
467
468
// Decrypt backup file to temp SQL
469
// IMPORTANT: do NOT use tempnam() here. It creates the file and can break Defuse file decrypt.
470
$tmpRand = '';
471
try {
472
    $tmpRand = bin2hex(random_bytes(4));
473
} catch (Throwable $ignored) {
474
    $tmpRand = uniqid('', true);
475
}
476
477
$tmpSql = rtrim((string) sys_get_temp_dir(), '/\\') . '/defuse_temp_restore_' . getmypid() . '_' . time() . '_' . $tmpRand . '.sql';
478
479
$dec = tpDefuseDecryptWithCandidates($file, $tmpSql, $keysToTry, $SETTINGS);
480
if (empty($dec['success'])) {
481
    @unlink($tmpSql);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for unlink(). 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

481
    /** @scrutinizer ignore-unhandled */ @unlink($tmpSql);

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...
482
    $log('ERROR', 'Decrypt failed: ' . (string) ($dec['message'] ?? ''));
483
    $payload['status'] = 'failed';
484
    $payload['finished_at'] = time();
485
    $payload['error'] = 'decrypt_failed';
486
    tpRestoreAuthorizationUpdatePayload($authId, $payload);
487
    exit(41);
488
}
489
490
$log('INFO', 'Decrypt OK. Importing SQL...');
491
492
// Restore SQL (chunked)
493
$totalSize = filesize($tmpSql);
494
if ($totalSize === false) {
495
    $totalSize = 0;
496
}
497
$offset = 0;
498
$batchSize = 500;
499
$totalExecuted = 0;
500
501
$handle = fopen($tmpSql, 'r');
502
if ($handle === false) {
503
    @unlink($tmpSql);
504
    $log('ERROR', 'Unable to open decrypted SQL file.');
505
    exit(42);
506
}
507
508
try {
509
    while (true) {
510
        if ($offset > 0) {
511
            fseek($handle, $offset);
512
        }
513
514
        $query = '';
515
        $executed = 0;
516
        $inMultiLineComment = false;
517
        $errors = [];
518
519
        try {
520
            DB::startTransaction();
521
            DB::query("SET FOREIGN_KEY_CHECKS = 0");
522
            DB::query("SET UNIQUE_CHECKS = 0");
523
524
            while (!feof($handle) && $executed < $batchSize) {
525
                $line = fgets($handle);
526
                if ($line === false) {
527
                    break;
528
                }
529
530
                $trimmedLine = trim($line);
531
532
                // Skip empty lines or comments
533
                if (
534
                    $trimmedLine === ''
535
                    || strpos($trimmedLine, '--') === 0
536
                    || strpos($trimmedLine, '#') === 0
537
                ) {
538
                    continue;
539
                }
540
541
                // Handle multi-line comments
542
                if (strpos($trimmedLine, '/*') === 0 && strpos($trimmedLine, '*/') === false) {
543
                    $inMultiLineComment = true;
544
                    continue;
545
                }
546
                if ($inMultiLineComment) {
547
                    if (strpos($trimmedLine, '*/') !== false) {
548
                        $inMultiLineComment = false;
549
                    }
550
                    continue;
551
                }
552
553
                $query .= $line;
554
555
                // Execute query if it ends with semicolon
556
                if (substr(rtrim($query), -1) === ';') {
557
                    try {
558
                        DB::query($query);
559
                        $executed++;
560
                        $totalExecuted++;
561
                    } catch (Throwable $e) {
562
                        $errors[] = $e->getMessage();
563
                        $log('ERROR', 'SQL error: ' . $e->getMessage());
564
                        // Stop early on first error
565
                        break;
566
                    }
567
                    $query = '';
568
                }
569
            }
570
571
            if (empty($errors)) {
572
                DB::commit();
573
            } else {
574
                DB::rollback();
575
            }
576
        } catch (Throwable $e) {
577
            try {
578
                DB::query("SET FOREIGN_KEY_CHECKS = 1");
579
                DB::query("SET UNIQUE_CHECKS = 1");
580
            } catch (Throwable $ignored) {
581
                // ignore
582
            }
583
            DB::rollback();
584
            $errors[] = $e->getMessage();
585
            $log('ERROR', 'Restore chunk failed: ' . $e->getMessage());
586
        } finally {
587
            try {
588
                DB::query("SET FOREIGN_KEY_CHECKS = 1");
589
                DB::query("SET UNIQUE_CHECKS = 1");
590
            } catch (Throwable $ignored) {
591
                // ignore
592
            }
593
        }
594
595
        $newOffset = ftell($handle);
596
        if ($newOffset === false) {
597
            $newOffset = $offset;
598
        }
599
600
601
	    // Progress (single-line bar on STDERR to avoid flooding the terminal)
602
	    if ($totalSize > 0) {
603
	        $pct = (int) floor(($newOffset / $totalSize) * 100);
604
	        if ($pct > 100) {
605
	            $pct = 100;
606
	        }
607
	        tpCliProgressBar($pct, $totalExecuted, $interactive);
608
609
	        // Log progress in file every 10%
610
	        static $lastLoggedPct = -1;
611
	        if ($pct !== $lastLoggedPct && ($pct % 10 === 0 || $pct === 100)) {
612
	            $lastLoggedPct = $pct;
613
	            @file_put_contents(
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for file_put_contents(). 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

613
	            /** @scrutinizer ignore-unhandled */ @file_put_contents(

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...
614
	                $logFile,
615
	                '[' . date('c') . '][INFO] Progress: ' . $pct . '% (statements executed: ' . $totalExecuted . ')' . PHP_EOL,
616
	                FILE_APPEND
617
	            );
618
	        }
619
	    }
620
621
        if (!empty($errors)) {
622
            throw new RuntimeException('SQL restore failed: ' . implode(' | ', $errors));
623
        }
624
625
        // End conditions
626
        if (feof($handle) || $newOffset >= $totalSize) {
627
            break;
628
        }
629
630
        // Continue with next offset
631
        $offset = $newOffset;
632
    }
633
634
    $log('INFO', 'SQL import completed. Total statements executed: ' . $totalExecuted);
635
636
    $payload['status'] = 'success';
637
    $payload['finished_at'] = time();
638
    tpRestoreAuthorizationUpdatePayload($authId, $payload);
639
640
    $exitCode = 0;
641
} catch (Throwable $e) {
642
    $payload['status'] = 'failed';
643
    $payload['finished_at'] = time();
644
    $payload['error'] = $e->getMessage();
645
    tpRestoreAuthorizationUpdatePayload($authId, $payload);
646
647
    $log('ERROR', 'Restore failed: ' . $e->getMessage());
648
    $exitCode = 50;
649
} finally {
650
    if (is_resource($handle)) {
651
        fclose($handle);
652
    }
653
    @unlink($tmpSql);
654
655
    // Re-force maintenance mode ON at the end (dump may have restored it to 0).
656
    try {
657
        DB::update(
658
            prefixTable('misc'),
659
            [
660
                'valeur' => '1',
661
                'updated_at' => time(),
662
            ],
663
            'intitule = %s AND type = %s',
664
            'maintenance_mode',
665
            'admin'
666
        );
667
    } catch (Throwable $e) {
668
        try {
669
            DB::update(
670
                prefixTable('misc'),
671
                [
672
                    'valeur' => '1',
673
                ],
674
                'intitule = %s AND type = %s',
675
                'maintenance_mode',
676
                'admin'
677
            );
678
        } catch (Throwable $ignored) {
679
            // ignore
680
        }
681
    }
682
683
    // Release lock
684
    try {
685
        flock($lockHandle, LOCK_UN);
686
    } catch (Throwable $ignored) {
687
        // ignore
688
    }
689
    @fclose($lockHandle);
0 ignored issues
show
Security Best Practice introduced by
It seems like you do not handle an error condition for fclose(). 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

689
    /** @scrutinizer ignore-unhandled */ @fclose($lockHandle);

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...
690
}
691
692
exit($exitCode);
693