Passed
Push — master ( 0f3664...743a3e )
by Nils
06:44 queued 16s
created

detectFileEncoding()   A

Complexity

Conditions 2
Paths 1

Size

Total Lines 7
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

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