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

tpRestoreAuthorizationCreate()   F

Complexity

Conditions 13
Paths 1154

Size

Total Lines 106
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 13
eloc 64
c 1
b 0
f 0
nc 1154
nop 11
dl 0
loc 106
rs 2.7454

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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      backup.functions.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
/**
33
 * Teampass - Backup helper functions
34
 * This file provides reusable functions for database backup creation
35
 * (manual UI + scheduled/background tasks).
36
 */
37
38
if (!function_exists('tpCreateDatabaseBackup')) {
39
    /**
40
     * Create a Teampass database backup file (optionally encrypted) in files folder.
41
     *
42
     * @param array  $SETTINGS      Teampass settings array (must include path_to_files_folder)
43
     * @param string $encryptionKey Encryption key (Defuse). If empty => no encryption
44
     * @param array  $options       Optional:
45
     *                              - output_dir (string) default: $SETTINGS['path_to_files_folder']
46
     *                              - filename_prefix (string) default: '' (ex: 'scheduled-')
47
     *                              - chunk_rows (int) default: 1000
48
     *                              - flush_every_inserts (int) default: 200
49
     *                              - include_tables (array<string>) default: [] (empty => all)
50
     *                              - exclude_tables (array<string>) default: [] (empty => none)
51
     * @return array
52
     * @psalm-return array{success: bool, filename: string, filepath: string, encrypted: bool, size_bytes: int, message: string}
53
     */
54
    function tpCreateDatabaseBackup(array $SETTINGS, string $encryptionKey = '', array $options = []): array
55
    {
56
        // Ensure required dependencies are loaded
57
        $mainFunctionsPath = __DIR__ . '/main.functions.php';
58
        if ((!function_exists('GenerateCryptKey') || !function_exists('prefixTable')) && is_file($mainFunctionsPath)) {
59
            require_once $mainFunctionsPath;
60
        }
61
        if (function_exists('loadClasses') && !class_exists('DB')) {
62
            loadClasses('DB');
63
        }
64
65
        // Enable maintenance mode for the whole backup operation, then restore previous value at the end.
66
        // This is best-effort: a failure to toggle maintenance must not break the backup itself.
67
        /** @scrutinizer ignore-unused */
68
        $__tpMaintenanceGuard = new class() {
0 ignored issues
show
Unused Code introduced by
The assignment to $__tpMaintenanceGuard is dead and can be removed.
Loading history...
69
            private $prev = null;
70
            private $changed = false;
71
72
            public function __construct()
73
            {
74
                try {
75
                    $row = DB::queryFirstRow(
76
                        'SELECT valeur FROM ' . prefixTable('misc') . ' WHERE intitule=%s AND type=%s',
77
                        'maintenance_mode',
78
                        'admin'
79
                    );
80
81
                    if (is_array($row) && array_key_exists('valeur', $row)) {
82
                        $this->prev = (string) $row['valeur'];
83
                    }
84
85
                    // Only toggle if it was not already enabled
86
                    if ($this->prev !== '1') {
87
                        DB::update(
88
                            prefixTable('misc'),
89
                            array(
90
                                'valeur' => '1',
91
                                'updated_at' => time(),
92
                            ),
93
                            'intitule = %s AND type= %s',
94
                            'maintenance_mode',
95
                            'admin'
96
                        );
97
                        $this->changed = true;
98
                    }
99
                } catch (Throwable $ignored) {
100
                    // ignore
101
                }
102
            }
103
104
            public function __destruct()
105
            {
106
                if ($this->changed !== true) {
107
                    return;
108
                }
109
110
                try {
111
                    DB::update(
112
                        prefixTable('misc'),
113
                        array(
114
                            'valeur' => (string) ($this->prev ?? '0'),
115
                            'updated_at' => time(),
116
                        ),
117
                        'intitule = %s AND type= %s',
118
                        'maintenance_mode',
119
                        'admin'
120
                    );
121
                } catch (Throwable $ignored) {
122
                    // ignore
123
                }
124
            }
125
        };
126
127
        $outputDir = $options['output_dir'] ?? ($SETTINGS['path_to_files_folder'] ?? '');
128
        $prefix = (string)($options['filename_prefix'] ?? '');
129
        $chunkRows = (int)($options['chunk_rows'] ?? 1000);
130
        $flushEvery = (int)($options['flush_every_inserts'] ?? 200);
131
        $includeTables = $options['include_tables'] ?? [];
132
        $excludeTables = $options['exclude_tables'] ?? [];
133
134
        if ($outputDir === '' || !is_dir($outputDir) || !is_writable($outputDir)) {
135
            return [
136
                'success' => false,
137
                'filename' => '',
138
                'filepath' => '',
139
                'encrypted' => false,
140
                'size_bytes' => 0,
141
                'message' => 'Backup folder is not writable or not found: ' . $outputDir,
142
            ];
143
        }
144
145
        // Generate filename
146
        $token = function_exists('GenerateCryptKey')
147
            ? GenerateCryptKey(20, false, true, true, false, true)
148
            : bin2hex(random_bytes(10));
149
150
        // Schema level token in filename (used for compatibility checks during migrations)
151
$schemaLevel = '';
152
if (defined('UPGRADE_MIN_DATE')) {
153
    $schemaLevel = (string) UPGRADE_MIN_DATE;
154
}
155
if ($schemaLevel !== '' && preg_match('/^\d+$/', $schemaLevel) !== 1) {
156
    $schemaLevel = '';
157
}
158
$schemaSuffix = ($schemaLevel !== '') ? ('-sl' . $schemaLevel) : '';
159
$filename = $prefix . time() . '-' . $token . $schemaSuffix . '.sql';
160
        $filepath = rtrim($outputDir, '/') . '/' . $filename;
161
162
        $handle = @fopen($filepath, 'w+');
163
        if ($handle === false) {
164
            return [
165
                'success' => false,
166
                'filename' => $filename,
167
                'filepath' => $filepath,
168
                'encrypted' => false,
169
                'size_bytes' => 0,
170
                'message' => 'Could not create backup file: ' . $filepath,
171
            ];
172
        }
173
174
        $insertCount = 0;
175
176
        try {
177
            // Get all tables
178
            $tables = [];
179
            $result = DB::query('SHOW TABLES');
180
            foreach ($result as $row) {
181
                // SHOW TABLES returns key like 'Tables_in_<DB_NAME>'
182
                foreach ($row as $v) {
183
                    $tables[] = (string) $v;
184
                    break;
185
                }
186
            }
187
188
            // Filter tables if requested
189
            if (!empty($includeTables) && is_array($includeTables)) {
190
                $tables = array_values(array_intersect($tables, $includeTables));
191
            }
192
            if (!empty($excludeTables) && is_array($excludeTables)) {
193
                $tables = array_values(array_diff($tables, $excludeTables));
194
            }
195
196
            foreach ($tables as $tableName) {
197
                // Safety: only allow typical MySQL table identifiers
198
                if (!preg_match('/^[a-zA-Z0-9_]+$/', $tableName)) {
199
                    continue;
200
                }
201
202
                // Write drop and creation
203
                fwrite($handle, 'DROP TABLE IF EXISTS `' . $tableName . "`;\n");
204
205
                $row2 = DB::queryFirstRow('SHOW CREATE TABLE `' . $tableName . '`');
206
                if (!is_array($row2) || empty($row2['Create Table'])) {
207
                    // Skip table if structure cannot be fetched
208
                    fwrite($handle, "\n");
209
                    continue;
210
                }
211
212
                fwrite($handle, $row2['Create Table'] . ";\n\n");
213
214
                // Process table data in chunks to reduce memory usage
215
                $offset = 0;
216
                while (true) {
217
                    $rows = DB::query(
218
                        'SELECT * FROM `' . $tableName . '` LIMIT %i OFFSET %i',
219
                        $chunkRows,
220
                        $offset
221
                    );
222
223
                    if (empty($rows)) {
224
                        break;
225
                    }
226
227
                    foreach ($rows as $record) {
228
                        $values = [];
229
                        foreach ($record as $value) {
230
                            if ($value === null) {
231
                                $values[] = 'NULL';
232
                                continue;
233
                            }
234
235
                            // Force scalar/string
236
                            if (is_bool($value)) {
237
                                $value = $value ? '1' : '0';
238
                            } elseif (is_numeric($value)) {
239
                                // keep numeric as string but quoted (safe & consistent)
240
                                $value = (string) $value;
241
                            } else {
242
                                $value = (string) $value;
243
                            }
244
245
                            // Escape and keep newlines
246
                            $value = addslashes(preg_replace("/\n/", '\\n', $value));
247
                            $values[] = '"' . $value . '"';
248
                        }
249
250
                        $insertQuery = 'INSERT INTO `' . $tableName . '` VALUES(' . implode(',', $values) . ");\n";
251
                        fwrite($handle, $insertQuery);
252
253
                        $insertCount++;
254
                        if ($flushEvery > 0 && ($insertCount % $flushEvery) === 0) {
255
                            fflush($handle);
256
                        }
257
                    }
258
259
                    $offset += $chunkRows;
260
                    fflush($handle);
261
                }
262
263
                fwrite($handle, "\n\n");
264
                fflush($handle);
265
            }
266
        } catch (Throwable $e) {
267
            if (is_resource($handle)) {
268
                fclose($handle);
269
            }
270
271
            $errorMessage = 'Backup failed: ' . $e->getMessage();
272
273
            // Suppression sécurisée sans @
274
            if (file_exists($filepath)) {
275
                $deleted = unlink($filepath);
276
                if ($deleted === false) {
277
                    $errorMessage .= ' (Note: Temporary backup file could not be deleted from disk)';
278
                }
279
            }
280
281
            return [
282
                'success' => false,
283
                'filename' => $filename,
284
                'filepath' => $filepath,
285
                'encrypted' => false,
286
                'size_bytes' => 0,
287
                'message' => $errorMessage,
288
            ];
289
        }
290
291
        fclose($handle);
292
293
        // Encrypt the file if key provided
294
        $encrypted = false;
295
        if ($encryptionKey !== '') {
296
            $tmpPath = rtrim($outputDir, '/') . '/defuse_temp_' . $filename;
297
298
            if (!function_exists('prepareFileWithDefuse')) {
299
                if (file_exists($filepath)) {
300
                    unlink($filepath);
301
                }
302
                return [
303
                    'success' => false,
304
                    'filename' => $filename,
305
                    'filepath' => $filepath,
306
                    'encrypted' => false,
307
                    'size_bytes' => 0,
308
                    'message' => 'Missing prepareFileWithDefuse() dependency (main.functions.php not loaded?)',
309
                ];
310
            }
311
312
            $ret = prepareFileWithDefuse('encrypt', $filepath, $tmpPath, $encryptionKey);
313
314
            if ($ret !== true) {
315
                if (file_exists($filepath)) {
316
                    unlink($filepath);
317
                }
318
                if (file_exists($tmpPath)) {
319
                    unlink($tmpPath);
320
                }
321
                return [
322
                    'success' => false,
323
                    'filename' => $filename,
324
                    'filepath' => $filepath,
325
                    'encrypted' => false,
326
                    'size_bytes' => 0,
327
                    'message' => 'Encryption failed: ' . (is_string($ret) ? $ret : 'unknown error'),
0 ignored issues
show
introduced by
The condition is_string($ret) is always true.
Loading history...
328
                ];
329
            }
330
331
            // Replace original with encrypted version
332
            if (file_exists($filepath)) {
333
                unlink($filepath);
334
            }
335
            
336
            // On vérifie le succès de rename() sans @
337
            if (is_file($tmpPath) && !rename($tmpPath, $filepath)) {
338
                if (file_exists($tmpPath)) {
339
                    unlink($tmpPath);
340
                }
341
                return [
342
                    'success' => false,
343
                    'filename' => $filename,
344
                    'filepath' => $filepath,
345
                    'encrypted' => false,
346
                    'size_bytes' => 0,
347
                    'message' => 'Encryption succeeded but could not finalize file (rename failed)',
348
                ];
349
            }
350
351
            $encrypted = true;
352
        }
353
354
        // Gestion de filesize sans @
355
        $size = 0;
356
        if (is_file($filepath)) {
357
            $size = filesize($filepath);
358
            if ($size === false) {
359
                $size = 0;
360
            }
361
        }
362
363
        return [
364
            'success' => true,
365
            'filename' => $filename,
366
            'filepath' => $filepath,
367
            'encrypted' => $encrypted,
368
            'size_bytes' => (int) $size,
369
            'message' => '',
370
        ];
371
    }
372
}
373
374
375
// -----------------------------------------------------------------------------
376
// Helpers for restore logic (used by backups.queries.php)
377
// -----------------------------------------------------------------------------
378
379
if (function_exists('tpSafeUtf8String') === false) {
380
    /**
381
     * Ensure the returned string is valid UTF-8 and JSON-safe.
382
     *
383
     * Some crypto libraries can return messages containing non-UTF8 bytes.
384
     * Those would break json_encode() / prepareExchangedData().
385
     */
386
    function tpSafeUtf8String($value): string
387
    {
388
        if ($value === null) {
389
            return '';
390
        }
391
        if (is_bool($value)) {
392
            return $value ? '1' : '0';
393
        }
394
        if (is_scalar($value) === false) {
395
            $value = print_r($value, true);
396
        }
397
398
        $str = (string) $value;
399
400
        $isUtf8 = false;
401
        if (function_exists('mb_check_encoding')) {
402
            $isUtf8 = mb_check_encoding($str, 'UTF-8');
403
        } else {
404
            $isUtf8 = (@preg_match('//u', $str) === 1);
405
        }
406
407
        if ($isUtf8 === false) {
408
            // ASCII safe fallback
409
            return '[hex]' . bin2hex($str);
410
        }
411
412
        // Strip ASCII control chars
413
        $str = preg_replace("/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/", '', $str) ?? $str;
414
        return $str;
415
    }
416
}
417
if (function_exists('tpPrepareFileWithDefuseNormalized') === false) {
418
    /**
419
     * Wrapper around prepareFileWithDefuse() that normalizes return values across TeamPass versions.
420
     *
421
     * @return array{success: bool, message: string}
422
     */
423
    function tpPrepareFileWithDefuseNormalized(
424
        string $mode,
425
        string $sourceFile,
426
        string $destFile,
427
        string $encryptionKey
428
    ): array {
429
        if (function_exists('prepareFileWithDefuse') === false) {
430
            return ['success' => false, 'message' => 'prepareFileWithDefuse() is not available'];
431
        }
432
433
        try {
434
            $ret = prepareFileWithDefuse($mode, $sourceFile, $destFile, $encryptionKey);
435
436
            if ($ret === true) {
437
                return ['success' => true, 'message' => ''];
438
            }
439
440
            if (is_string($ret)) {
0 ignored issues
show
introduced by
The condition is_string($ret) is always true.
Loading history...
441
                return ['success' => false, 'message' => tpSafeUtf8String($ret)];
442
            }
443
444
            return ['success' => false, 'message' => 'Unknown error'];
445
        } catch (\Throwable $e) {
446
            return ['success' => false, 'message' => tpSafeUtf8String($e->getMessage())];
447
        }
448
    }
449
}
450
451
if (function_exists('tpDefuseDecryptWithCandidates') === false) {
452
    /**
453
     * Try to decrypt a file using Defuse with multiple candidate keys.
454
     *
455
     * @param array<int,string> $candidateKeys
456
     * @return array{success: bool, message: string, key_used?: string}
457
     */
458
    function tpDefuseDecryptWithCandidates(
459
        string $encryptedFile,
460
        string $decryptedFile,
461
        array $candidateKeys,
462
        array $SETTINGS = []
0 ignored issues
show
Unused Code introduced by
The parameter $SETTINGS is not used and could be removed. ( Ignorable by Annotation )

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

462
        /** @scrutinizer ignore-unused */ array $SETTINGS = []

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
463
    ): array {
464
        $lastMsg = '';
465
        foreach ($candidateKeys as $k) {
466
            $k = (string)$k;
467
            if ($k === '') {
468
                continue;
469
            }
470
471
            // Ensure we start from a clean slate.
472
            if (is_file($decryptedFile) && !unlink($decryptedFile)) {
473
                // Nothing to do, try next key
474
            }
475
476
            $r = tpPrepareFileWithDefuseNormalized('decrypt', $encryptedFile, $decryptedFile, $k);
477
            if (!empty($r['success'])) {
478
                return ['success' => true, 'message' => '', 'key_used' => $k];
479
            }
480
            $lastMsg = tpSafeUtf8String((string)($r['message'] ?? ''));
481
        }
482
483
        return ['success' => false, 'message' => ($lastMsg !== '' ? $lastMsg : 'Unable to decrypt')];
484
    }
485
}
486
487
// -----------------------------------------------------------------------------
488
// Backup metadata helpers (.meta.json sidecar) and schema token parsing
489
// -----------------------------------------------------------------------------
490
// NOTE: schema_level is stored for internal checks only. UI must never display schema_level.
491
492
if (function_exists('tpGetTpFilesVersion') === false) {
493
    function tpGetTpFilesVersion(): string
494
    {
495
        if (defined('TP_VERSION') && defined('TP_VERSION_MINOR')) {
496
            return (string) TP_VERSION . '.' . (string) TP_VERSION_MINOR;
497
        }
498
        return '';
499
    }
500
}
501
502
if (function_exists('tpGetSchemaLevel') === false) {
503
    function tpGetSchemaLevel(): string
504
    {
505
        if (defined('UPGRADE_MIN_DATE')) {
506
            $v = (string) UPGRADE_MIN_DATE;
507
            if ($v !== '' && preg_match('/^\d+$/', $v) === 1) {
508
                return $v;
509
            }
510
        }
511
        return '';
512
    }
513
}
514
515
if (function_exists('tpGetBackupMetadataPath') === false) {
516
    function tpGetBackupMetadataPath(string $backupFilePath): string
517
    {
518
        return $backupFilePath . '.meta.json';
519
    }
520
}
521
522
if (function_exists('tpWriteBackupMetadata') === false) {
523
    /**
524
     * Write backup metadata sidecar file (<backup>.meta.json).
525
     *
526
     * @return array{success: bool, message: string, meta_path: string}
527
     */
528
    function tpWriteBackupMetadata(string $backupFilePath, string $tpFilesVersion = '', string $schemaLevel = '', array $extra = []): array
529
    {
530
        $metaPath = tpGetBackupMetadataPath($backupFilePath);
531
532
        if ($tpFilesVersion === '') {
533
            $tpFilesVersion = tpGetTpFilesVersion();
534
        }
535
        if ($schemaLevel === '') {
536
            $schemaLevel = tpGetSchemaLevel();
537
        }
538
539
        $payload = array_merge(
540
            [
541
                'tp_files_version' => $tpFilesVersion !== '' ? $tpFilesVersion : null,
542
                'schema_level' => $schemaLevel !== '' ? $schemaLevel : null,
543
                'created_at' => gmdate('c'),
544
            ],
545
            $extra
546
        );
547
548
        $json = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT);
549
        if ($json === false) {
550
            return ['success' => false, 'message' => 'Unable to encode metadata as JSON', 'meta_path' => $metaPath];
551
        }
552
553
        $ok = @file_put_contents($metaPath, $json, LOCK_EX);
554
        if ($ok === false) {
555
            return ['success' => false, 'message' => 'Unable to write metadata file', 'meta_path' => $metaPath];
556
        }
557
558
        return ['success' => true, 'message' => '', 'meta_path' => $metaPath];
559
    }
560
}
561
562
if (function_exists('tpReadBackupMetadata') === false) {
563
    function tpReadBackupMetadata(string $backupFilePath): array
564
    {
565
        $metaPath = tpGetBackupMetadataPath($backupFilePath);
566
        if (!is_file($metaPath)) {
567
            return [];
568
        }
569
        $raw = @file_get_contents($metaPath);
570
        if ($raw === false || trim($raw) === '') {
571
            return [];
572
        }
573
        $data = json_decode($raw, true);
574
        return is_array($data) ? $data : [];
575
    }
576
}
577
578
if (function_exists('tpParseSchemaLevelFromBackupFilename') === false) {
579
    function tpParseSchemaLevelFromBackupFilename(string $filename): string
580
    {
581
        $bn = basename($filename);
582
        if (preg_match('/-sl(\d+)(?:\D|$)/', $bn, $m) === 1) {
583
            return (string) $m[1];
584
        }
585
        return '';
586
    }
587
}
588
589
if (function_exists('tpGetBackupSchemaLevelFromMetaOrFilename') === false) {
590
    function tpGetBackupSchemaLevelFromMetaOrFilename(string $backupFilePath): string
591
    {
592
        $meta = tpReadBackupMetadata($backupFilePath);
593
        if (!empty($meta['schema_level']) && is_scalar($meta['schema_level'])) {
594
            $v = (string) $meta['schema_level'];
595
            if ($v !== '' && preg_match('/^\d+$/', $v) === 1) {
596
                return $v;
597
            }
598
        }
599
        $v = tpParseSchemaLevelFromBackupFilename($backupFilePath);
600
        return ($v !== '' && preg_match('/^\d+$/', $v) === 1) ? $v : '';
601
    }
602
}
603
604
if (function_exists('tpGetBackupTpFilesVersionFromMeta') === false) {
605
    function tpGetBackupTpFilesVersionFromMeta(string $backupFilePath): string
606
    {
607
        $meta = tpReadBackupMetadata($backupFilePath);
608
        if (!empty($meta['tp_files_version']) && is_scalar($meta['tp_files_version'])) {
609
            return (string) $meta['tp_files_version'];
610
        }
611
        return '';
612
    }
613
}
614
615
/**
616
 * -----------------------------------------------------------------------------
617
 * CLI Restore Authorization (one-shot token stored in teampass_misc)
618
 * -----------------------------------------------------------------------------
619
 * type    : restore_authorization_cli
620
 * intitule: sha256(token)
621
 * valeur  : JSON payload (status, initiator, file info, encrypted secrets)
622
 *
623
 * TTL fixed at 3600s (1 hour) for now.
624
 */
625
626
if (function_exists('tpRestoreAuthorizationCreate') === false) {
627
    function tpRestoreAuthorizationCreate(
628
        int $userId,
629
        string $userLogin,
630
        string $filePath,
631
        string $serverScope,
632
        string $serverFile,
633
        int $operationId,
634
        string $encryptionKey,
635
        string $overrideKey,
636
        array $compat,
637
        int $ttl,
638
        array $SETTINGS
639
    ): array {
640
        $ttl = (int) $ttl;
641
        if ($ttl <= 0) {
642
            $ttl = 3600;
643
        }
644
645
        $now = time();
646
647
        try {
648
            $token = rtrim(strtr(base64_encode(random_bytes(32)), '+/', '-_'), '=');
649
        } catch (Throwable $e) {
650
            return ['success' => false, 'message' => $e->getMessage()];
651
        }
652
653
        $tokenHash = hash('sha256', $token);
654
655
        $real = realpath($filePath);
656
        $resolvedPath = ($real !== false) ? $real : $filePath;
657
658
        $payload = [
659
            'status' => 'pending',
660
            'created_at' => $now,
661
            'expires_at' => $now + $ttl,
662
            'ttl' => $ttl,
663
            'initiator' => [
664
                'id' => $userId,
665
                'login' => $userLogin,
666
            ],
667
            'file' => [
668
                'path' => $resolvedPath,
669
                'basename' => basename($resolvedPath),
670
                'size' => is_file($resolvedPath) ? filesize($resolvedPath) : null,
671
                'mtime' => is_file($resolvedPath) ? filemtime($resolvedPath) : null,
672
            ],
673
            'source' => [
674
                'serverScope' => $serverScope,
675
                'serverFile' => $serverFile,
676
                'operation_id' => $operationId,
677
            ],
678
            'compat' => $compat,
679
            'secrets' => [],
680
        ];
681
682
        if ($encryptionKey !== '') {
683
            $enc = cryption($encryptionKey, '', 'encrypt', $SETTINGS);
684
            $payload['secrets']['encryptionKey'] = isset($enc['string']) ? (string) $enc['string'] : '';
685
        }
686
687
        if ($overrideKey !== '') {
688
            $enc = cryption($overrideKey, '', 'encrypt', $SETTINGS);
689
            $payload['secrets']['overrideKey'] = isset($enc['string']) ? (string) $enc['string'] : '';
690
        }
691
692
        $json = json_encode($payload, JSON_UNESCAPED_SLASHES);
693
        if ($json === false) {
694
            return ['success' => false, 'message' => 'json_encode_failed'];
695
        }
696
697
        // Insert token entry
698
        try {
699
            DB::insert(
700
                prefixTable('misc'),
701
                [
702
                    'type' => 'restore_authorization_cli',
703
                    'intitule' => $tokenHash,
704
                    'valeur' => $json,
705
                    'updated_at' => $now,
706
                ]
707
            );
708
            $id = (int) DB::insertId();
709
        } catch (Throwable $e) {
710
            // Fallback if updated_at column does not exist
711
            try {
712
                DB::insert(
713
                    prefixTable('misc'),
714
                    [
715
                        'type' => 'restore_authorization_cli',
716
                        'intitule' => $tokenHash,
717
                        'valeur' => $json,
718
                    ]
719
                );
720
                $id = (int) DB::insertId();
721
            } catch (Throwable $e2) {
722
                return ['success' => false, 'message' => $e2->getMessage()];
723
            }
724
        }
725
726
        return [
727
            'success' => true,
728
            'id' => $id,
729
            'token' => $token,
730
            'token_hash' => $tokenHash,
731
            'expires_at' => (int) $payload['expires_at'],
732
            'ttl' => $ttl,
733
        ];
734
    }
735
}
736
737
if (function_exists('tpRestoreAuthorizationFetchByHash') === false) {
738
    function tpRestoreAuthorizationFetchByHash(string $tokenHash): array
739
    {
740
        $row = DB::queryFirstRow(
741
            'SELECT increment_id, valeur FROM ' . prefixTable('misc') . ' WHERE type = %s AND intitule = %s LIMIT 1',
742
            'restore_authorization_cli',
743
            $tokenHash
744
        );
745
746
        if (empty($row)) {
747
            return ['success' => false, 'message' => 'not_found'];
748
        }
749
750
        $payload = json_decode((string) ($row['valeur'] ?? ''), true);
751
        if (!is_array($payload)) {
752
            return ['success' => false, 'message' => 'invalid_payload'];
753
        }
754
755
        return [
756
            'success' => true,
757
            'id' => (int) ($row['increment_id'] ?? 0),
758
            'payload' => $payload,
759
        ];
760
    }
761
}
762
763
if (function_exists('tpRestoreAuthorizationUpdatePayload') === false) {
764
    function tpRestoreAuthorizationUpdatePayload(int $id, array $payload): bool
765
    {
766
        $payload['updated_at'] = time();
767
        $json = json_encode($payload, JSON_UNESCAPED_SLASHES);
768
        if ($json === false) {
769
            return false;
770
        }
771
772
        try {
773
            DB::update(
774
                prefixTable('misc'),
775
                [
776
                    'valeur' => $json,
777
                    'updated_at' => time(),
778
                ],
779
                'increment_id=%i AND type=%s',
780
                $id,
781
                'restore_authorization_cli'
782
            );
783
            return true;
784
        } catch (Throwable $e) {
785
            // Fallback if updated_at column does not exist
786
            try {
787
                DB::update(
788
                    prefixTable('misc'),
789
                    [
790
                        'valeur' => $json,
791
                    ],
792
                    'increment_id=%i AND type=%s',
793
                    $id,
794
                    'restore_authorization_cli'
795
                );
796
                return true;
797
            } catch (Throwable $e2) {
798
                return false;
799
            }
800
        }
801
    }
802
}
803
804