Passed
Push — master ( b9d9d6...11abf5 )
by Nils
08:06 queued 01:32
created

stripEmojis()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 6
c 1
b 0
f 0
nc 1
nop 1
dl 0
loc 15
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      import.queries.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
33
use Goodby\CSV\Import\Standard\Lexer;
34
use Goodby\CSV\Import\Standard\Interpreter;
35
use Goodby\CSV\Import\Standard\LexerConfig;
36
use voku\helper\AntiXSS;
37
use TeampassClasses\NestedTree\NestedTree;
38
use TeampassClasses\SessionManager\SessionManager;
39
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
40
use TeampassClasses\Language\Language;
41
use TeampassClasses\PerformChecks\PerformChecks;
42
use TeampassClasses\ConfigManager\ConfigManager;
43
44
// Load functions
45
require_once 'main.functions.php';
46
47
// init
48
loadClasses('DB');
49
$session = SessionManager::getSession();
50
$request = SymfonyRequest::createFromGlobals();
51
$lang = new Language($session->get('user-language') ?? 'english');
52
$antiXss = new AntiXSS();
53
54
// Load config
55
$configManager = new ConfigManager();
56
$SETTINGS = $configManager->getAllSettings();
57
58
// Do checks
59
// Instantiate the class with posted data
60
$checkUserAccess = new PerformChecks(
61
    dataSanitizer(
62
        [
63
            'type' => htmlspecialchars($request->request->get('type', ''), ENT_QUOTES, 'UTF-8'),
64
        ],
65
        [
66
            'type' => 'trim|escape',
67
        ],
68
    ),
69
    [
70
        'user_id' => returnIfSet($session->get('user-id'), null),
71
        'user_key' => returnIfSet($session->get('key'), null),
72
    ]
73
);
74
// Handle the case
75
echo $checkUserAccess->caseHandler();
76
if (
77
    $checkUserAccess->userAccessPage('import') === false ||
78
    $checkUserAccess->checkSession() === false
79
) {
80
    // Not allowed page
81
    $session->set('system-error_code', ERR_NOT_ALLOWED);
82
    include $SETTINGS['cpassman_dir'] . '/error.php';
83
    exit;
84
}
85
86
// Define Timezone
87
date_default_timezone_set($SETTINGS['timezone'] ?? 'UTC');
88
89
// Set header properties
90
header('Content-type: text/html; charset=utf-8');
91
header('Cache-Control: no-cache, no-store, must-revalidate');
92
error_reporting(E_ERROR);
93
set_time_limit(0);
94
95
// --------------------------------- //
96
97
// Set some constants for program readability
98
define('KP_PATH', 0);
99
define('KP_GROUP', 1);
100
define('KP_TITLE', 2);
101
define('KP_PASSWORD', 3);
102
define('KP_USERNAME', 4);
103
define('KP_URL', 5);
104
define('KP_UUID', 6);
105
define('KP_NOTES', 7);
106
107
108
// Prepare POST variables
109
$data = [
110
    'type' => $request->request->filter('type', '', FILTER_SANITIZE_SPECIAL_CHARS),
111
    'data' => $request->request->filter('data', '', FILTER_SANITIZE_SPECIAL_CHARS),
112
    'key' => $request->request->filter('key', '', FILTER_SANITIZE_SPECIAL_CHARS),
113
    'file' => $request->request->filter('file', '', FILTER_SANITIZE_SPECIAL_CHARS),
114
];
115
116
$filters = [
117
    'type' => 'trim|escape',
118
    'data' => 'trim|escape',
119
    'key' => 'trim|escape',
120
    'file' => 'cast:integer',
121
];
122
123
$inputData = dataSanitizer(
124
    $data,
125
    $filters
126
);
127
128
129
$tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
130
131
// Build query
132
switch ($inputData['type']) {
133
    //Check if import CSV file format is what expected
134
    case 'import_file_format_csv':
135
        // Check KEY and rights
136
        if ($inputData['key'] !== $session->get('key')) {
137
            echo prepareExchangedData(
138
                array(
139
                    'error' => true,
140
                    'message' => $lang->get('key_is_not_correct'),
141
                ),
142
                'encode'
143
            );
144
            break;
145
        }
146
147
        //load full tree
148
        $tree->rebuild();
149
        $tree = $tree->getDescendants();
150
151
        // Init post variable
152
        $post_operation_id = $inputData['file'];
153
154
        // Get filename from database
155
        $data = DB::queryFirstRow(
156
            'SELECT valeur
157
            FROM '.prefixTable('misc').'
158
            WHERE increment_id = %i AND type = "temp_file"',
159
            $post_operation_id
160
        );
161
        
162
        // Delete operation id
163
        DB::delete(
164
            prefixTable('misc'),
165
            "increment_id = %i AND type = 'temp_file'",
166
            $post_operation_id
167
        );
168
169
        // Initialisation
170
        $file = $SETTINGS['path_to_files_folder'] . '/' . $data['valeur'];
171
        $importation_possible = true;
172
        $valuesToImport = [];
173
        $items_number = 0;
174
        $batchInsert = [];
175
        $uniqueFolders = [];
176
177
        // Vérifier si le fichier est accessible
178
        if (!file_exists($file) || !is_readable($file)) {
179
            echo prepareExchangedData(
180
                array('error' => true, 'message' => $lang->get('cannot_open_file')),
181
                'encode'
182
            );
183
            unlink($file);
184
            break;
185
        }
186
187
        // Ouvrir le fichier pour récupérer l'en-tête
188
        $fp = fopen($file, 'r');
189
        $header = fgetcsv($fp);
190
        fclose($fp);
191
192
        // Vérifier si l'en-tête est valide
193
        if ($header === false || empty($header)) {
194
            echo prepareExchangedData(
195
                array('error' => true, 'message' => $lang->get('import_error_no_read_possible')),
196
                'encode'
197
            );
198
            unlink($file);
199
            break;
200
        }
201
202
        // Initialisation du tableau de stockage des valeurs
203
        $valuesToImport = [];
204
205
        // Configuration du parser CSV
206
        $config = new LexerConfig();
207
        $lexer = new Lexer($config);
208
        $config->setIgnoreHeaderLine(true);
209
210
        // Observer pour récupérer les données et respecter l'ordre des colonnes
211
        $interpreter = new Interpreter();
212
        $interpreter->addObserver(function (array $row) use (&$valuesToImport, $header) {
213
            $rowData = array_combine($header, $row);
214
215
            if ($rowData !== false) {
216
                $valuesToImport[] = array(
217
                    'Label' => isset($rowData['label']) ? trim($rowData['label']) : '',
218
                    'Login' => isset($rowData['login']) ? trim($rowData['login']) : '',
219
                    'Password' => isset($rowData['password']) ? trim($rowData['password']) : '',
220
                    'url' => isset($rowData['url']) ? trim($rowData['url']) : '',
221
                    'Comments' => isset($rowData['description']) ? trim($rowData['description']) : '',
222
                    'Folder' => isset($rowData['folder']) ? trim($rowData['folder']) : '',
223
                );
224
            }
225
        });
226
227
        // Launch parsing with `Lexer`
228
        $lexer->parse($file, $interpreter);
229
230
        // Check if some date were imported
231
        if (empty($valuesToImport)) {
232
            echo prepareExchangedData(
233
                array('error' => true, 'message' => $lang->get('import_error_no_read_possible')),
234
                'encode'
235
            );
236
            unlink($file);
237
            break;
238
        }
239
        
240
        // Process lines
241
        foreach ($valuesToImport as $row) {
242
            // Check that each line has 6 columns
243
            if (count($row) != 6) {
244
                echo prepareExchangedData(
245
                    array('error' => true, 'message' => $lang->get('import_error_invalid_structure')),
246
                    'encode'
247
                );
248
                unlink($file);
249
                break;
250
            }
251
        
252
            // CLean data
253
            $label    = cleanInput($row['Label']);
254
            $login    = cleanInput($row['Login']);
255
            $pwd      = cleanInput($row['Password']);
256
            $url      = cleanInput($row['url']);
257
            $folder   = cleanInput($row['Folder']);
258
            $comments = cleanInput($row['Comments']);
259
        
260
            // Handle multiple lignes description
261
            if (strpos($comments, '<br>') !== false || strpos($label, '<br>') !== false) {
262
                $continue_on_next_line = true;
263
                $comment .= " " . $label;
264
            } else {
265
                // Insert previous line if changing line
266
                if (!empty($account)) {
267
                    $items_number++;
268
269
                    // Insert in batch
270
                    $batchInsert[] = array(
271
                        'label'        => $account,
272
                        'description'  => $comment,
273
                        'pwd'          => $pwd,
274
                        'url'          => $url,
275
                        'folder'       => ((int) $session->get('user-admin') === 1 || (int) $session->get('user-manager') === 1 || (int) $session->get('user-can_manage_all_users') === 1) ? $folder : '',
276
                        'login'        => $login,
277
                        'operation_id' => $post_operation_id,
278
                    );
279
280
                    // Store unique folders
281
                    // Each folder is the unique element of the path located inside $folder and delimited by '/' or '\'
282
                    $folders = preg_split('/[\/\\\\]/', $folder);
283
                    foreach ($folders as $folder) {
284
                        if (!empty($folder)) {
285
                            $uniqueFolders[$folder] = $folder;
286
                        }
287
                    }
288
                }
289
                // Update current variables
290
                $account = $label;
291
                $comment = $comments;
292
                $continue_on_next_line = false;
293
            }
294
        }
295
296
        // Insert last line
297
        if (!empty($account)) {
298
            $items_number++;
299
300
            // Insert in batch
301
            $batchInsert[] = array(
302
                'label'        => $account,
303
                'description'  => $comment,
304
                'pwd'          => $pwd,
305
                'url'          => $url,
306
                'folder'       => ((int) $session->get('user-admin') === 1 || (int) $session->get('user-manager') === 1 || (int) $session->get('user-can_manage_all_users') === 1) ? $folder : '',
307
                'login'        => $login,
308
                'operation_id' => $post_operation_id,
309
            );
310
311
            // Store unique folders
312
            // Each folder is the unique element of the path located inside $folder and delimited by '/' or '\'
313
            $folders = preg_split('/[\/\\\\]/', $folder);
314
            foreach ($folders as $folder) {
315
                if (!empty($folder)) {
316
                    $uniqueFolders[$folder] = $folder;
317
                }
318
            }
319
        }
320
        
321
        // Insert in database (with batch optimisation)
322
        if (!empty($batchInsert)) {
323
            $tableName = prefixTable('items_importations');
324
            $values = [];
325
        
326
            foreach ($batchInsert as $data) {
327
                $values[] = "('" . implode("','", array_map('addslashes', $data)) . "')";
328
            }
329
        
330
            $sql = "INSERT INTO `$tableName` (`label`, `description`, `pwd`, `url`, `folder`, `login`, `operation_id`) VALUES " . implode(',', $values);
331
            
332
            DB::query($sql);
333
        }
334
335
        // Display results
336
        echo prepareExchangedData(
337
            array('error' => false,
338
                'operation_id' => $post_operation_id,
339
                'items_number' => $items_number,
340
                'folders_number' => count($uniqueFolders),        
341
                'userCanManageFolders' => ((int) $session->get('user-admin') === 1 || (int) $session->get('user-manager') === 1 || (int) $session->get('user-can_manage_all_users') === 1) ? 1 : 0
342
            ),
343
            'encode'
344
        );
345
        
346
        // Delete file after processing	
347
        unlink($file);
348
        break;
349
350
    // Create new folders
351
    case 'import_csv_folders':
352
        // Check KEY and rights
353
        if ($inputData['key'] !== $session->get('key')) {
354
            echo prepareExchangedData(
355
                array(
356
                    'error' => true,
357
                    'message' => $lang->get('key_is_not_correct'),
358
                ),
359
                'encode'
360
            );
361
            break;
362
        }
363
364
        // Check if user is manager at least.
365
        if ((int) $session->get('user-admin') === 0 && (int) $session->get('user-manager') === 0 && (int) $session->get('user-can_manage_all_users') === 0) {
366
            echo prepareExchangedData(
367
                array(
368
                    'error' => true,
369
                    'message' => $lang->get('import_error_no_rights'),
370
                ),
371
                'encode'
372
            );
373
            break;
374
        }
375
376
        // Decrypt and retreive data in JSON format
377
        $dataReceived = prepareExchangedData(
378
            $inputData['data'],
379
            'decode'
380
        );
381
382
        // Is this a personal folder?
383
        $personalFolder = in_array($dataReceived['folderId'], $session->get('user-personal_folders')) ? 1 : 0;
384
385
        // Get all folders from objects in DB
386
        $itemsPath = DB::query(
387
            'SELECT folder, increment_id
388
            FROM '.prefixTable('items_importations').'
389
            WHERE operation_id = %i
390
            LIMIT %i, %i',
391
            $dataReceived['csvOperationId'],
392
            $dataReceived['offset'],
393
            $dataReceived['limit']
394
        );
395
396
397
        // Save matches "path -> ID" to prevent against multiple insertions
398
        $folderIdMap = $dataReceived['folderIdMap'] ?? [];
399
        
400
        require_once 'folders.class.php';
401
        $folderManager = new FolderManager($lang);
402
403
        // Get all folders from objects in DB
404
        foreach ($itemsPath as $item) {
405
            $path = $item['folder'];
406
            $importId = $item['increment_id']; // Entry ID in items_importations
407
408
            $parts = explode("\\", $path); // Décomposer le chemin en sous-dossiers
409
            $currentPath = "";
410
            $parentId = $dataReceived['folderId']; // Strating with provided folder
411
412
            foreach ($parts as $part) {
413
                $currentPath = trim($currentPath . "/" . $part, "/");
414
                $currentFolder = $part;
415
416
                // Check if this folder has already been created
417
                if (isset($folderIdMap[$currentPath]) || empty($currentFolder)) {
418
                    $parentId = $folderIdMap[$currentPath];
419
                    continue; // Jump to next iteration
420
                }
421
422
                // Vérifier si le dossier existe déjà en base
423
                $existingId = DB::queryFirstField(
424
                    'SELECT id FROM '.prefixTable('nested_tree').' WHERE title = %s AND parent_id = %i',
425
                    $currentFolder, // Searching only by name
426
                    $parentId // Ensure we search the correct parent
427
                );
428
429
                if ($existingId) {
430
                    $folderIdMap[$currentPath] = $existingId;
431
                    $parentId = $existingId;
432
                } else {
433
                    // Insert folder and get its ID
434
                    $params = [
435
                        'title' => (string) $currentFolder,
436
                        'parent_id' => (int) $parentId,
437
                        'personal_folder' => (int) $personalFolder,
438
                        'complexity' => $dataReceived['folderPasswordComplexity'] ?? 0,
439
                        'duration' => 0,
440
                        'access_rights' => $dataReceived['folderAccessRight'] ?? '',
441
                        'user_is_admin' => (int) $session->get('user-admin'),
442
                        'user_accessible_folders' => (array) $session->get('user-accessible_folders'),
443
                        'user_is_manager' => (int) $session->get('user-manager'),
444
                        'user_can_create_root_folder' => (int) $session->get('user-can_create_root_folder'),
445
                        'user_can_manage_all_users' => (int) $session->get('user-can_manage_all_users'),
446
                        'user_id' => (int) $session->get('user-id'),
447
                        'user_roles' => (string) $session->get('user-roles')
448
                    ];
449
                    $options = [
450
                        'rebuildFolderTree' => false,
451
                        'setFolderCategories' => false,
452
                        'manageFolderPermissions' => true,
453
                        'copyCustomFieldsCategories' => false,
454
                        'refreshCacheForUsersWithSimilarRoles' => true,
455
                    ];
456
                    $creationStatus = $folderManager->createNewFolder($params, $options);
457
                    $folderCreationDone = $creationStatus['error'];
458
459
                    if ((int) $folderCreationDone === 0) {
460
                        $newFolderId = $creationStatus['newId'];
461
                        // User created the folder
462
                        // Add new ID to list of visible ones
463
                        if ((int) $session->get('user-admin') === 0 && $creationStatus['error'] === false && $newFolderId !== 0) {
464
                            SessionManager::addRemoveFromSessionArray('user-accessible_folders', [$newFolderId], 'add');
465
                        }
466
467
                        // Save ID in map to avoid recreating it
468
                        $folderIdMap[$currentPath] = $newFolderId;
469
                        $parentId = $newFolderId;
470
                    } else {
471
                        // Get ID of existing folder
472
                        $ret = DB::queryFirstRow(
473
                            'SELECT *
474
                            FROM ' . prefixTable('nested_tree') . '
475
                            WHERE title = %s',
476
                            $currentFolder
477
                        );
478
                        $newFolderId = $ret['id'];
479
                        $parentId = $newFolderId;
480
                    }
481
                }
482
            }
483
484
            // Update the importation entry with the new folder ID
485
            DB::update(prefixTable('items_importations'), [
486
                'folder_id' => $parentId
487
            ], "increment_id=%i", $importId);
488
        }
489
490
        echo prepareExchangedData(
491
            array(
492
                'error' => false,
493
                'message' => '',
494
                'processedCount' => count($itemsPath),
495
                'folderIdMap' => $folderIdMap,
496
            ),
497
            'encode'
498
        );
499
        
500
        break;
501
502
503
    //Insert into DB the items the user has selected
504
    case 'import_csv_items':
505
        // Check KEY and rights
506
        if ($inputData['key'] !== $session->get('key')) {
507
            echo prepareExchangedData(
508
                array(
509
                    'error' => true,
510
                    'message' => $lang->get('key_is_not_correct'),
511
                ),
512
                'encode'
513
            );
514
            break;
515
        }
516
        
517
518
        // Decrypt and retreive data in JSON format
519
        $dataReceived = prepareExchangedData(
520
            $inputData['data'],
521
            'decode'
522
        );
523
524
        //Get some info about personal folder
525
        $personalFolder = in_array($dataReceived['folderId'], $session->get('user-personal_folders')) ? 1 : 0;        
526
527
        // Get all folders from objects in DB
528
        if ($dataReceived['foldersNumber'] > 0) {
529
            $items = DB::query(
530
                'SELECT ii.label, ii.login, ii.pwd, ii.url, ii.description, ii.folder_id, ii.increment_id, nt.title
531
                FROM '.prefixTable('items_importations').' AS ii
532
                INNER JOIN '.prefixTable('nested_tree').' AS nt ON ii.folder_id = nt.id
533
                WHERE ii.operation_id = %i
534
                LIMIT %i, %i',
535
                $dataReceived['csvOperationId'],
536
                $dataReceived['offset'],
537
                $dataReceived['limit']
538
            );
539
        } else {
540
            $items = DB::query(
541
                'SELECT ii.label, ii.login, ii.pwd, ii.url, ii.description, ii.folder_id, ii.increment_id
542
                FROM '.prefixTable('items_importations').' AS ii
543
                WHERE ii.operation_id = %i
544
                LIMIT %i, %i',
545
                $dataReceived['csvOperationId'],
546
                $dataReceived['offset'],
547
                $dataReceived['limit']
548
            );
549
            $targetFolderId = $dataReceived['folderId'];
550
            $targetFolderName = DB::queryFirstField(
551
                'SELECT title
552
                FROM '.prefixTable('nested_tree').'
553
                WHERE id = %i',
554
                $targetFolderId
555
            );
556
        }
557
558
        // Init some variables
559
        $insertedItems = $dataReceived['insertedItems'] ?? 0;
560
        $failedItems = [];
561
562
        // Loop on items
563
        foreach ($items as $item) {         
564
            try {
565
                // Handle case where password is empty
566
                if (($session->has('user-create_item_without_password') && 
567
                    null !== $session->get('user-create_item_without_password') &&
568
                    (int) $session->get('user-create_item_without_password') !== 1) ||
569
                    !empty($item['pwd'])) {
570
                    // Encrypt password
571
                    $cryptedStuff = doDataEncryption($item['pwd']);
572
                } else {
573
                    $cryptedStuff['encrypted'] = '';
574
                    $cryptedStuff['objectKey'] = '';
575
                }
576
                $itemPassword = $cryptedStuff['encrypted'];
577
            
578
                // Insert new item in table ITEMS
579
                DB::insert(
580
                    prefixTable('items'),
581
                    array(
582
                        'label' => substr($item['label'], 0, 500),
583
                        'description' => empty($item['comment']) === true ? '' : $item['comment'],
584
                        'pw' => $itemPassword,
585
                        'pw_iv' => '',
586
                        'url' => empty($item['url']) === true ? '' : substr($item['url'], 0, 500),
587
                        'id_tree' => is_null($item['folder_id']) === true ? $targetFolderId : (int) $item['folder_id'],
588
                        'login' => empty($item['login']) === true ? '' : substr($item['login'], 0, 200),
589
                        'anyone_can_modify' => $dataReceived['editAll'],
590
                        'encryption_type' => 'teampass_aes',
591
                        'item_key' => uniqidReal(50),
592
                        'created_at' => time(),
593
                    )
594
                );
595
                $newId = DB::insertId();
596
597
                // Create new task for the new item
598
                // If it is not a personnal one
599
                if ((int) $personalFolder === 0) {
600
                    if ($dataReceived['keysGenerationWithTasksHandler' === 'tasksHandler']) {
601
                        // Create task for the new item
602
                        storeTask(
603
                            'new_item',
604
                            $session->get('user-id'),
605
                            0,
606
                            (int) $item['folder_id'],
607
                            (int) $newId,
608
                            $cryptedStuff['objectKey'],
609
                        );
610
                    } else {
611
                        // Create sharekeys for current user
612
                        storeUsersShareKey(
613
                            prefixTable('sharekeys_items'),
614
                            (int) $item['folder_id'],
615
                            (int) $newId,
616
                            $cryptedStuff['objectKey'],
617
                            false
618
                        );
619
                    }
620
                } else {
621
                    // Create sharekeys for current user
622
                    storeUsersShareKey(
623
                        prefixTable('sharekeys_items'),
624
                        (int) $personalFolder,
625
                        (int) $newId,
626
                        $cryptedStuff['objectKey'],
627
                        true
628
                    );
629
                }
630
631
                //if asked, anyone in role can modify
632
                if ((int) $dataReceived['editRole'] === 1) {
633
                    foreach ($session->get('system-array_roles') as $role) {
634
                        DB::insert(
635
                            prefixTable('restriction_to_roles'),
636
                            array(
637
                                'role_id' => $role['id'],
638
                                'item_id' => $newId,
639
                            )
640
                        );
641
                    }
642
                }
643
644
                // Insert new item in table LOGS_ITEMS
645
                DB::insert(
646
                    prefixTable('log_items'),
647
                    array(
648
                        'id_item' => $newId,
649
                        'date' => time(),
650
                        'id_user' => $session->get('user-id'),
651
                        'action' => 'at_creation',
652
                    )
653
                );
654
655
                //Add entry to cache table
656
                DB::insert(
657
                    prefixTable('cache'),
658
                    array(
659
                        'id' => $newId,
660
                        'label' => substr($item['label'], 0, 500),
661
                        'description' => empty($item['comment']) ? '' : $item['comment'],
662
                        'id_tree' => is_null($item['folder_id']) === true ? $targetFolderId : (int) $item['folder_id'],
663
                        'url' => '0',
664
                        'perso' => $personalFolder === 0 ? 0 : 1,
665
                        'login' => empty($item['login']) ? '' : substr($item['login'], 0, 500),
666
                        'folder' => is_null($item['title']) === true ? $targetFolderName : $item['title'],
667
                        'author' => $session->get('user-id'),
668
                        'timestamp' => time(),
669
                        'tags' => '',
670
                        'restricted_to' => '0',
671
                        'renewal_period' => '0',
672
                        'timestamp' => time(),
673
                    )
674
                );
675
676
                // Update items_importation table
677
                DB::update(
678
                    prefixTable('items_importations'),
679
                    array(
680
                        'increment_id' => $newId,
681
                        'imported_at'=> time(),
682
                    ),
683
                    "increment_id=%i",
684
                    $item['increment_id']
685
                );
686
687
                $insertedItems++;
688
689
            } catch (Exception $e) {
690
                // Log the error and store the failed item
691
                $failedItems[] = [
692
                    'increment_id' => $item['increment_id'],
693
                    'error' => $e->getMessage(),
694
                ];
695
            }
696
        }
697
698
        echo prepareExchangedData(
699
            array(
700
                'error' => false,
701
                'message' => '',
702
                'insertedItems' => $insertedItems,
703
                'failedItems' => $failedItems,
704
            ),
705
            'encode'
706
        );
707
        break;
708
709
710
    //Check if import KEEPASS file format is what expected
711
    case 'import_file_format_keepass':
712
        // Check KEY and rights
713
        if ($inputData['key'] !== $session->get('key')) {
714
            echo prepareExchangedData(
715
                array(
716
                    'error' => true,
717
                    'message' => $lang->get('key_is_not_correct'),
718
                ),
719
                'encode'
720
            );
721
            break;
722
        }
723
724
        // Decrypt and retreive data in JSON format
725
        $receivedParameters = prepareExchangedData(
726
            $inputData['data'],
727
            'decode'
728
        );
729
        $post_operation_id = filter_var($receivedParameters['file'], FILTER_SANITIZE_FULL_SPECIAL_CHARS);
730
        $post_folder_id = filter_var($receivedParameters['folder-id'], FILTER_SANITIZE_NUMBER_INT);
731
732
        // Get filename from database
733
        $data = DB::queryFirstRow(
734
            'SELECT valeur
735
            FROM '.prefixTable('misc').'
736
            WHERE increment_id = %i AND type = "temp_file"',
737
            $post_operation_id
738
        );
739
740
        // Delete operation id
741
        DB::delete(
742
            prefixTable('misc'),
743
            'increment_id = %i AND type = "temp_file"',
744
            $post_operation_id
745
        );
746
747
        // do some initializations
748
        $file = $data['valeur'];
749
750
        //read xml file
751
        if (file_exists($SETTINGS['path_to_files_folder'].'/'.$file)) {
752
            $xml = simplexml_load_file(
753
                $SETTINGS['path_to_files_folder'].'/'.$file
754
            );
755
        }
756
757
        // Convert XML to associative array
758
        $xmlfile = file_get_contents($SETTINGS['path_to_files_folder'].'/'.$file);
759
        $new = simplexml_load_string($xmlfile);
760
        $con = json_encode($new);
761
        $newArr = json_decode($con, true);
762
763
764
        /**
765
         * Recursive function to process the Keepass XML structure.
766
         * 
767
         * @param array $array The current array to process.
768
         * @param string $previousFolder The parent folder ID.
769
         * @param array $newItemsToAdd The new items to add to the database.
770
         * @param int $level The current level of the recursion.
771
         * 
772
         * @return array The new items to add to the database.
773
         */
774
        function recursive($array, $previousFolder, $newItemsToAdd, $level) : array
775
        {
776
            // Handle entries (items)
777
            if (isset($array['Entry'])) {
778
                $newItemsToAdd = handleEntries($array['Entry'], $previousFolder, $newItemsToAdd);
779
            }
780
781
            // Handle groups (folders)
782
            if (isset($array['Group']) && is_array($array['Group'])) {
783
                $newItemsToAdd = handleGroups($array['Group'], $previousFolder, $newItemsToAdd, $level);
784
            }
785
786
            return $newItemsToAdd;
787
        }
788
789
        /**
790
         * Handle entries (items) within the structure.
791
         * It processes each entry and adds it to the new items list.
792
         * 
793
         * @param array $entries The entries to process.
794
         * @param string $previousFolder The parent folder ID.
795
         * @param array $newItemsToAdd The new items to add to the database.
796
         * 
797
         * @return array The new items to add to the database.
798
         */
799
        function handleEntries(array $entries, string $previousFolder, array $newItemsToAdd) : array
800
        {
801
            foreach ($entries as $key => $value) {
802
                // Check if the entry has a 'String' field and process it
803
                if (isset($value['String'])) {
804
                    $newItemsToAdd['items'][] = buildItemDefinition($value['String'], $previousFolder);
805
                } 
806
                // If it's a direct 'String' item, build a simple item
807
                elseif ($key === 'String') {
808
                    $newItemsToAdd['items'][] = buildSimpleItem($value, $previousFolder);
809
                }
810
            }
811
812
            return $newItemsToAdd;
813
        }
814
815
        /**
816
         * Build an item definition from the 'String' fields.
817
         * Converts the key-value pairs into a usable item format.
818
         * 
819
         * @param array $strings The 'String' fields to process.
820
         * @param string $previousFolder The parent folder ID.
821
         * 
822
         * @return array The item definition.
823
         */
824
        function buildItemDefinition(array $strings, string $previousFolder) : array
825
        {
826
            $itemDefinition = [];
827
            // Loop through each 'String' entry and map keys and values
828
            foreach ($strings as $entry) {
829
                $itemDefinition[$entry['Key']] = is_array($entry['Value']) ? '' : $entry['Value'];
830
            }
831
832
            // Set the parent folder and ensure default values for certain fields
833
            $itemDefinition['parentFolderId'] = $previousFolder;
834
            $itemDefinition['Notes'] = $itemDefinition['Notes'] ?? '';
835
            $itemDefinition['URL'] = $itemDefinition['URL'] ?? '';
836
            $itemDefinition['Password'] = $itemDefinition['Password'] ?? '';
837
838
            return $itemDefinition;
839
        }
840
841
        /**
842
         * Build a simple item with predefined fields.
843
         * This is used when there is no associated key, just ordered values.
844
         * 
845
         * @param array $value The ordered values to process.
846
         * @param string $previousFolder The parent folder ID.
847
         * 
848
         * @return array The simple item definition.
849
         */
850
        function buildSimpleItem(array $value, string $previousFolder) : array
851
        {
852
            return [
853
                'Notes' => is_array($value[0]['Value']) ? '' : $value[0]['Value'],
854
                'Title' => is_array($value[2]['Value']) ? '' : $value[2]['Value'],
855
                'Password' => is_array($value[1]['Value']) ? '' : $value[1]['Value'],
856
                'URL' => is_array($value[3]['Value']) ? '' : $value[3]['Value'],
857
                'UserName' => is_array($value[4]['Value']) ? '' : $value[4]['Value'],
858
                'parentFolderId' => $previousFolder,
859
            ];
860
        }
861
862
        /**
863
         * Handle groups (folders) within the structure.
864
         * It processes each group and recursively goes deeper into subgroups and subentries.
865
         * 
866
         * @param array $groups The groups to process.
867
         * @param string $previousFolder The parent folder ID.
868
         * @param array $newItemsToAdd The new items to add to the database.
869
         * 
870
         * @return array The new items to add to the database.
871
         */
872
        function handleGroups($groups, string $previousFolder, array $newItemsToAdd, int $level) : array
873
        {
874
            // If a single group is found, wrap it into an array
875
            if (isset($groups['UUID'])) {
876
                $groups = [$groups];
877
            }
878
879
            foreach ($groups as $group) {
880
                // Add the current group (folder) to the list
881
                $newItemsToAdd['folders'][] = [
882
                    'folderName' => $group['Name'],
883
                    'uuid' => $group['UUID'],
884
                    'parentFolderId' => $previousFolder,
885
                    'level' => $level,
886
                ];
887
888
                // Recursively process entries and subgroups inside this group
889
                $newItemsToAdd = recursive(
890
                    [
891
                        'Entry' => $group['Entry'] ?? '',
892
                        'Group' => $group['Group'] ?? '',
893
                    ],
894
                    $group['UUID'],
895
                    $newItemsToAdd,
896
                    $level + 1
897
                );
898
            }
899
900
            return $newItemsToAdd;
901
        }
902
903
        // Start the recursive processing
904
        $ret = recursive(
905
            array_merge(
906
                ['Entry' => $newArr['Root']['Group']['Entry']],
907
                ['Group' => $newArr['Root']['Group']['Group']],
908
            ),
909
            $post_folder_id,
910
            [
911
                'folders' => [],
912
                'items' => []
913
            ],
914
            1,
915
        );
916
917
        
918
        echo prepareExchangedData(
919
            array(
920
                'error' => false,
921
                'message' => '',
922
                'data' => $ret,
923
            ),
924
            'encode'
925
        );
926
927
        break;
928
929
    // KEEPASS - CREATE FOLDERS
930
    case 'keepass_create_folders':
931
        // Check KEY and rights
932
        if ($inputData['key'] !== $session->get('key')) {
933
            echo prepareExchangedData(
934
                array(
935
                    'error' => true,
936
                    'message' => $lang->get('key_is_not_correct'),
937
                ),
938
                'encode'
939
            );
940
            break;
941
        }
942
943
        // Decrypt and retreive data in JSON format
944
        $receivedParameters = prepareExchangedData(
945
            $inputData['data'],
946
            'decode'
947
        );
948
949
        $post_folder_id = filter_var($receivedParameters['folder-id'], FILTER_SANITIZE_NUMBER_INT);
950
        $$inputData['editAll'] = filter_var($receivedParameters['edit-all'], FILTER_SANITIZE_NUMBER_INT);
951
        $$inputData['editRole'] = filter_var($receivedParameters['edit-role'], FILTER_SANITIZE_NUMBER_INT);
952
        $post_folders = filter_var_array(
953
            $receivedParameters['folders'],
954
            FILTER_SANITIZE_FULL_SPECIAL_CHARS
955
        );
956
957
        // get destination folder informations
958
        $destinationFolderInfos = getFolderComplexity($post_folder_id, $session->get('user-personal_folders'));
959
        $arrFolders[$post_folder_id] = [
960
            'id' => (int) $post_folder_id,
961
            'level' => 1,
962
            'isPF' => false,
963
        ];
964
        $startPathLevel = 1;
965
966
        foreach($post_folders as $folder) {
967
            // get parent id
968
            $parentId = $arrFolders[$folder['parentFolderId']];
969
970
            // create folder in DB
971
            $folderId = createFolder(
972
                $folder['folderName'],
973
                $parentId['id'],
974
                $folder['level'],
975
                $startPathLevel,
976
                $destinationFolderInfos['levelPwComplexity']
977
            );
978
979
            // manage parent
980
            $arrFolders[$folder['uuid']] = [
981
                'id' => (int) $folderId,
982
                'level' => (int) ($folder['level'] + $startPathLevel),
983
                'isPF' => $destinationFolderInfos['importPF'],
984
            ];
985
        }
986
        //rebuild full tree
987
        $tree->rebuild();
988
989
990
        echo prepareExchangedData(
991
            array(
992
                'error' => false,
993
                'message' => '',
994
                'folders' => $arrFolders,
995
            ),
996
            'encode'
997
        );
998
999
        break;
1000
1001
    // KEEPASS - CREATE ITEMS
1002
    case 'keepass_create_items':
1003
        // Check KEY and rights
1004
        if ($inputData['key'] !== $session->get('key')) {
1005
            echo prepareExchangedData(
1006
                array(
1007
                    'error' => true,
1008
                    'message' => $lang->get('key_is_not_correct'),
1009
                ),
1010
                'encode'
1011
            );
1012
            break;
1013
        }
1014
1015
        // Decrypt and retreive data in JSON format
1016
        $receivedParameters = prepareExchangedData(
1017
            $inputData['data'],
1018
            'decode'
1019
        );
1020
1021
        $$inputData['editAll'] = filter_var($receivedParameters['edit-all'], FILTER_SANITIZE_NUMBER_INT);
1022
        $$inputData['editRole'] = filter_var($receivedParameters['edit-role'], FILTER_SANITIZE_NUMBER_INT);
1023
        $post_folders = filter_var_array(
1024
            $receivedParameters['folders'],
1025
            FILTER_SANITIZE_FULL_SPECIAL_CHARS
1026
        );
1027
        $post_items = filter_var_array(
1028
            $receivedParameters['items'],
1029
            FILTER_SANITIZE_FULL_SPECIAL_CHARS
1030
        );
1031
        $ret = '';
1032
1033
        // Start transaction for better performance
1034
        DB::startTransaction();
1035
1036
        // Import all items
1037
        foreach($post_items as $item) {
1038
            // get info about this folder
1039
            $destinationFolderMore = DB::queryFirstRow(
1040
                'SELECT title FROM '.prefixTable('nested_tree').' WHERE id = %i',
1041
                (int) $post_folders[$item['parentFolderId']]['id']
1042
            );
1043
1044
            // Handle case where pw is empty
1045
            // if not allowed then warn user
1046
            if (($session->has('user-create_item_without_password') && null !== $session->get('user-create_item_without_password')
1047
                && (int) $session->get('user-create_item_without_password') !== 1
1048
                ) ||
1049
                empty($item['Password']) === false
1050
            ) {
1051
                // NEW ENCRYPTION
1052
                $cryptedStuff = doDataEncryption($item['Password']);
1053
            } else {
1054
                $cryptedStuff['encrypted'] = '';
1055
                $cryptedStuff['objectKey'] = '';
1056
            }
1057
            $post_password = $cryptedStuff['encrypted'];
1058
1059
            //ADD item
1060
            DB::insert(
1061
                prefixTable('items'),
1062
                array(
1063
                    'label' => substr($item['Title'], 0, 500),
1064
                    'description' => $item['Notes'],
1065
                    'pw' => $cryptedStuff['encrypted'],
1066
                    'pw_iv' => '',
1067
                    'url' => substr($item['URL'], 0, 500),
1068
                    'id_tree' => $post_folders[$item['parentFolderId']]['id'],
1069
                    'login' => substr($item['UserName'], 0, 500),
1070
                    'anyone_can_modify' => $$inputData['editAll'],
1071
                    'encryption_type' => 'teampass_aes',
1072
                    'inactif' => 0,
1073
                    'restricted_to' => '',
1074
                    'perso' => $post_folders[$item['parentFolderId']]['isPF'] === true ? 1 : 0,
1075
                    'item_key' => uniqidReal(50),
1076
                    'created_at' => time(),
1077
                )
1078
            );
1079
            $newId = DB::insertId();
1080
1081
            // Create sharekeys for users
1082
            storeUsersShareKey(
1083
                prefixTable('sharekeys_items'),
1084
                $post_folders[$item['parentFolderId']]['isPF'] === true ? 1 : 0,
1085
                (int) $newId,
1086
                $cryptedStuff['objectKey'],
1087
            );
1088
1089
            //if asked, anyone in role can modify
1090
            if ($$inputData['editRole'] === 1) {
1091
                foreach ($session->get('system-array_roles') as $role) {
1092
                    DB::insert(
1093
                        prefixTable('restriction_to_roles'),
1094
                        array(
1095
                            'role_id' => $role['id'],
1096
                            'item_id' => $newId,
1097
                        )
1098
                    );
1099
                }
1100
            }
1101
1102
            //Add log
1103
            DB::insert(
1104
                prefixTable('log_items'),
1105
                array(
1106
                    'id_item' => $newId,
1107
                    'date' => time(),
1108
                    'id_user' => $session->get('user-id'),
1109
                    'action' => 'at_creation',
1110
                    'raison' => 'at_import',
1111
                )
1112
            );
1113
1114
            //Add entry to cache table
1115
            DB::insert(
1116
                prefixTable('cache'),
1117
                array(
1118
                    'id' => $newId,
1119
                    'label' => substr(stripslashes($item['Title']), 0, 500),
1120
                    'description' => stripslashes($item['Notes']),
1121
                    'url' => substr(stripslashes($item['URL']), 0, 500),
1122
                    'tags' => '',
1123
                    'id_tree' => $post_folders[$item['parentFolderId']]['id'],
1124
                    'perso' => $post_folders[$item['parentFolderId']]['isPF'] === 0 ? 0 : 1,
1125
                    'login' => substr(stripslashes($item['UserName']), 0, 500),
1126
                    'restricted_to' => '0',
1127
                    'folder' => $destinationFolderMore['title'],
1128
                    'author' => $session->get('user-id'),
1129
                    'renewal_period' => '0',
1130
                    'timestamp' => time(),
1131
                )
1132
            );
1133
1134
            // prepare return
1135
            $ret .= "<li>".substr(stripslashes($item['Title']), 0, 500)." [".$destinationFolderMore['title']."]</li>";
1136
        }
1137
1138
        // Commit transaction.
1139
        DB::commit();
1140
1141
        echo prepareExchangedData(
1142
            array(
1143
                'error' => false,
1144
                'message' => '',
1145
                'info' => "<ul>".$ret."</ul>",
1146
            ),
1147
            'encode'
1148
        );
1149
1150
        break;
1151
    }
1152
1153
1154
1155
/**
1156
 * Create folders during importation
1157
 *
1158
 * @param string $folderTitle
1159
 * @param integer $parentId
1160
 * @param integer $folderLevel
1161
 * @param integer $startPathLevel
1162
 * @param integer $levelPwComplexity
1163
 * @return integer
1164
 */
1165
function createFolder($folderTitle, $parentId, $folderLevel, $startPathLevel, $levelPwComplexity)
1166
{
1167
    $session = SessionManager::getSession();
1168
    //create folder - if not exists at the same level
1169
    DB::query(
1170
        'SELECT * FROM '.prefixTable('nested_tree').'
1171
        WHERE nlevel = %i AND title = %s AND parent_id = %i LIMIT 1',
1172
        intval($folderLevel + $startPathLevel),
1173
        $folderTitle,
1174
        $parentId
1175
    );
1176
    if (DB::count() === 0) {
1177
        //do query
1178
        DB::insert(
1179
            prefixTable('nested_tree'),
1180
            array(
1181
                'parent_id' => $parentId,
1182
                'title' => stripslashes($folderTitle),
1183
                'nlevel' => $folderLevel,
1184
                'categories' => '',
1185
            )
1186
        );
1187
        $id = DB::insertId();
1188
        //Add complexity level => level is set to "medium" by default.
1189
        DB::insert(
1190
            prefixTable('misc'),
1191
            array(
1192
                'type' => 'complex',
1193
                'intitule' => $id,
1194
                'valeur' => $levelPwComplexity,
1195
                'created_at' => time(),
1196
            )
1197
        );
1198
1199
        // Indicate that a change has been done to force tree user reload
1200
        DB::update(
1201
            prefixTable('misc'),
1202
            array(
1203
                'valeur' => time(),
1204
                'updated_at' => time(),
1205
            ),
1206
            'type = %s AND intitule = %s',
1207
            'timestamp',
1208
            'last_folder_change'
1209
        );
1210
1211
        //For each role to which the user depends on, add the folder just created.
1212
        foreach ($session->get('system-array_roles') as $role) {
1213
            DB::insert(
1214
                prefixTable('roles_values'),
1215
                array(
1216
                    'role_id' => $role['id'],
1217
                    'folder_id' => $id,
1218
                    'type' => 'W',
1219
                )
1220
            );
1221
        }
1222
1223
        //Add this new folder to the list of visible folders for the user.
1224
        $session->set('user-accessible_folders', array_unique(array_merge($session->get('user-accessible_folders'), [$id]), SORT_NUMERIC));
1225
1226
        return $id;
1227
    }
1228
    
1229
    //get folder actual ID
1230
    $data = DB::queryFirstRow(
1231
        'SELECT id FROM '.prefixTable('nested_tree').'
1232
        WHERE nlevel = %i AND title = %s AND parent_id = %i',
1233
        intval($folderLevel + $startPathLevel),
1234
        $folderTitle,
1235
        $parentId
1236
    );
1237
    return $data['id'];
1238
}
1239
1240
/** 
1241
 * getFolderComplexity
1242
 * 
1243
 * @param int $folderId
1244
 * @param boolean $isFolderPF
1245
 * 
1246
 * @return array
1247
*/
1248
function getFolderComplexity($folderId, $isFolderPF)
1249
{
1250
    // If destination is not ROOT then get the complexity level
1251
    if ($isFolderPF === true) {
1252
        return [
1253
            'levelPwComplexity' => 50,
1254
            'startPathLevel' => 1,
1255
            'importPF' => true
1256
        ];
1257
    } elseif ($folderId > 0) {
1258
        $data = DB::queryFirstRow(
1259
            'SELECT m.valeur as value, t.nlevel as nlevel
1260
            FROM '.prefixTable('misc').' as m
1261
            INNER JOIN '.prefixTable('nested_tree').' as t ON (m.intitule = t.id)
1262
            WHERE m.type = %s AND m.intitule = %s',
1263
            'complex',
1264
            $folderId
1265
        );
1266
        return [
1267
            'levelPwComplexity' => $data['value'],
1268
            'startPathLevel' => $data['nlevel'],
1269
            'importPF' => false
1270
        ];
1271
    }
1272
    return [
1273
        'levelPwComplexity' => 50,
1274
        'startPathLevel' => 0,
1275
        'importPF' => false
1276
    ];
1277
}
1278
1279
spl_autoload_register(function ($class) {
1280
    $prefix = 'League\\Csv\\';
1281
    $base_dir = __DIR__.'/src/';
1282
    $len = strlen($prefix);
1283
    if (strncmp($prefix, $class, $len) !== 0) {
1284
        // no, move to the next registered autoloader
1285
        return;
1286
    }
1287
    $relative_class = substr($class, $len);
1288
    $file = $base_dir.str_replace('\\', '/', $relative_class).'.php';
1289
    if (file_exists($file)) {
1290
        require $file;
1291
    }
1292
});
1293
1294
/**
1295
 * Used to format the string ready for insertion in to the database.
1296
 *
1297
 * @param string $str             String to clean
1298
 * @param string $crLFReplacement Replacement
1299
 *
1300
 * @return string
1301
 */
1302
function sanitiseString($str, $crLFReplacement)
1303
{
1304
    $str = preg_replace('#[\r\n]#', $crLFReplacement, (string) $str);
1305
    $str = str_replace('\\', '&#92;', $str);
1306
    $str = str_replace('"', '&quot;', $str);
1307
    if (!empty($str)) {
1308
        addslashes($str);
1309
    }
1310
1311
    return $str;
1312
}
1313
1314
/**
1315
 * Clean array values.
1316
 *
1317
 * @param string $value String to clean
1318
 *
1319
 * @return string
1320
 */
1321
function cleanOutput(&$value)
1322
{
1323
    return htmlspecialchars_decode($value);
1324
}
1325
1326
/**
1327
 * Clean a string.
1328
 * 
1329
 * @param string $value The string to clean.
1330
 * 
1331
 * @return string The cleaned string.
1332
 */
1333
function cleanInput($value): string
1334
{
1335
    return stripEmojis(
1336
        cleanString(
1337
            html_entity_decode($value, ENT_QUOTES | ENT_XHTML, 'UTF-8'),
1338
            true
1339
        )
1340
    );
1341
}
1342
1343
/**
1344
 * Strip any emoji icons from the string.
1345
 *
1346
 * @param string $string
1347
 *   The string to remove emojis from.
1348
 *
1349
 * @return string
1350
 *   The string with emojis removed.
1351
 */
1352
function stripEmojis($string): string
1353
{
1354
    // Convert question marks to a special thing so that we can remove
1355
    // question marks later without any problems.
1356
    $string = str_replace("?", "{%}", $string);
1357
    // Convert the text into UTF-8.
1358
    $string = mb_convert_encoding($string, "ISO-8859-1", "UTF-8");
1359
    // Convert the text to ASCII.
1360
    $string = mb_convert_encoding($string, "UTF-8", "ISO-8859-1");
1361
    // Replace anything that is a question mark (left over from the conversion.
1362
    $string = preg_replace('/(\s?\?\s?)/', ' ', $string);
1363
    // Put back the .
1364
    $string = str_replace("{%}", "?", $string);
1365
    // Trim and return.
1366
    return trim($string);
1367
  }