Passed
Push — master ( 97d9b0...f5b400 )
by Nils
06:48 queued 15s
created

toggleUserFavorite()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 10
c 1
b 0
f 0
nc 2
nop 2
dl 0
loc 15
rs 9.9332
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      main.functions.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
use LdapRecord\Connection;
33
use Elegant\Sanitizer\Sanitizer;
34
use voku\helper\AntiXSS;
35
use Hackzilla\PasswordGenerator\Generator\ComputerPasswordGenerator;
36
use Hackzilla\PasswordGenerator\RandomGenerator\Php7RandomGenerator;
37
use TeampassClasses\SessionManager\SessionManager;
38
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
39
use TeampassClasses\Language\Language;
40
use TeampassClasses\NestedTree\NestedTree;
41
use Defuse\Crypto\Key;
42
use Defuse\Crypto\Crypto;
43
use Defuse\Crypto\KeyProtectedByPassword;
44
use Defuse\Crypto\File as CryptoFile;
45
use Defuse\Crypto\Exception as CryptoException;
46
use TeampassClasses\PasswordManager\PasswordManager;
47
use Symfony\Component\Process\PhpExecutableFinder;
48
use TeampassClasses\Encryption\Encryption;
49
use TeampassClasses\ConfigManager\ConfigManager;
50
use TeampassClasses\EmailService\EmailService;
51
use TeampassClasses\EmailService\EmailSettings;
52
53
header('Content-type: text/html; charset=utf-8');
54
header('Cache-Control: no-cache, must-revalidate');
55
56
loadClasses('DB');
57
$session = SessionManager::getSession();
58
59
// Load config if $SETTINGS not defined
60
$configManager = new ConfigManager();
61
$SETTINGS = $configManager->getAllSettings();
62
63
/**
64
 * Checks if a string is hex encoded
65
 *
66
 * @param string $str
67
 * @return boolean
68
 */
69
function isHex(string $str): bool
70
{
71
    if (str_starts_with(strtolower($str), '0x')) {
72
        $str = substr($str, 2);
73
    }
74
75
    return ctype_xdigit($str);
76
}
77
78
/**
79
 * Defuse cryption function.
80
 *
81
 * @param string $message   what to de/crypt
82
 * @param string $ascii_key key to use
83
 * @param string $type      operation to perform
84
 * @param array  $SETTINGS  Teampass settings
85
 *
86
 * @return array
87
 */
88
function cryption(string $message, string $ascii_key, string $type, ?array $SETTINGS = []): array
89
{
90
    $ascii_key = empty($ascii_key) === true ? file_get_contents(SECUREPATH.'/'.SECUREFILE) : $ascii_key;
91
    $err = false;
92
    
93
    // convert KEY
94
    $key = Key::loadFromAsciiSafeString($ascii_key);
95
    try {
96
        if ($type === 'encrypt') {
97
            $text = Crypto::encrypt($message, $key);
98
        } elseif ($type === 'decrypt') {
99
            $text = Crypto::decrypt($message, $key);
100
        }
101
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
102
        error_log('TEAMPASS-Error-Wrong key or modified ciphertext: ' . $ex->getMessage());
103
        $err = 'wrong_key_or_modified_ciphertext';
104
    } catch (CryptoException\BadFormatException $ex) {
105
        error_log('TEAMPASS-Error-Bad format exception: ' . $ex->getMessage());
106
        $err = 'bad_format';
107
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
108
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
109
        $err = 'environment_error';
110
    } catch (CryptoException\IOException $ex) {
111
        error_log('TEAMPASS-Error-IO: ' . $ex->getMessage());
112
        $err = 'io_error';
113
    } catch (Exception $ex) {
114
        error_log('TEAMPASS-Error-Unexpected exception: ' . $ex->getMessage());
115
        $err = 'unexpected_error';
116
    }
117
118
    return [
119
        'string' => $text ?? '',
120
        'error' => $err,
121
    ];
122
}
123
124
/**
125
 * Generating a defuse key.
126
 *
127
 * @return string
128
 */
129
function defuse_generate_key()
130
{
131
    $key = Key::createNewRandomKey();
132
    $key = $key->saveToAsciiSafeString();
133
    return $key;
134
}
135
136
/**
137
 * Generate a Defuse personal key.
138
 *
139
 * @param string $psk psk used
140
 *
141
 * @return string
142
 */
143
function defuse_generate_personal_key(string $psk): string
144
{
145
    $protected_key = KeyProtectedByPassword::createRandomPasswordProtectedKey($psk);
146
    return $protected_key->saveToAsciiSafeString(); // save this in user table
147
}
148
149
/**
150
 * Validate persoanl key with defuse.
151
 *
152
 * @param string $psk                   the user's psk
153
 * @param string $protected_key_encoded special key
154
 *
155
 * @return string
156
 */
157
function defuse_validate_personal_key(string $psk, string $protected_key_encoded): string
158
{
159
    try {
160
        $protected_key_encoded = KeyProtectedByPassword::loadFromAsciiSafeString($protected_key_encoded);
161
        $user_key = $protected_key_encoded->unlockKey($psk);
162
        $user_key_encoded = $user_key->saveToAsciiSafeString();
163
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
164
        return 'Error - Major issue as the encryption is broken.';
165
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
166
        return 'Error - The saltkey is not the correct one.';
167
    }
168
169
    return $user_key_encoded;
170
    // store it in session once user has entered his psk
171
}
172
173
/**
174
 * Decrypt a defuse string if encrypted.
175
 *
176
 * @param string $value Encrypted string
177
 *
178
 * @return string Decrypted string
179
 */
180
function defuseReturnDecrypted(string $value): string
181
{
182
    if (substr($value, 0, 3) === 'def') {
183
        $value = cryption($value, '', 'decrypt')['string'];
184
    }
185
186
    return $value;
187
}
188
189
/**
190
 * Trims a string depending on a specific string.
191
 *
192
 * @param string|array $chaine  what to trim
193
 * @param string       $element trim on what
194
 *
195
 * @return string
196
 */
197
function trimElement($chaine, string $element): string
198
{
199
    if (! empty($chaine)) {
200
        if (is_array($chaine) === true) {
201
            $chaine = implode(';', $chaine);
202
        }
203
        $chaine = trim($chaine);
204
        if (substr($chaine, 0, 1) === $element) {
205
            $chaine = substr($chaine, 1);
206
        }
207
        if (substr($chaine, strlen($chaine) - 1, 1) === $element) {
208
            $chaine = substr($chaine, 0, strlen($chaine) - 1);
209
        }
210
    }
211
212
    return $chaine;
213
}
214
215
/**
216
 * Permits to suppress all "special" characters from string.
217
 *
218
 * @param string $string  what to clean
219
 * @param bool   $special use of special chars?
220
 *
221
 * @return string
222
 */
223
function cleanString(string $string, bool $special = false): string
224
{
225
    // Create temporary table for special characters escape
226
    $tabSpecialChar = [];
227
    for ($i = 0; $i <= 31; ++$i) {
228
        $tabSpecialChar[] = chr($i);
229
    }
230
    array_push($tabSpecialChar, '<br />');
231
    if ((int) $special === 1) {
232
        $tabSpecialChar = array_merge($tabSpecialChar, ['</li>', '<ul>', '<ol>']);
233
    }
234
235
    return str_replace($tabSpecialChar, "\n", $string);
236
}
237
238
/**
239
 * Erro manager for DB.
240
 *
241
 * @param array $params output from query
242
 *
243
 * @return void
244
 */
245
function db_error_handler(array $params): void
246
{
247
    echo 'Error: ' . $params['error'] . "<br>\n";
248
    echo 'Query: ' . $params['query'] . "<br>\n";
249
    throw new Exception('Error - Query', 1);
250
}
251
252
/**
253
 * Identify user's rights
254
 *
255
 * @param string|array $groupesVisiblesUser  [description]
256
 * @param string|array $groupesInterditsUser [description]
257
 * @param string       $isAdmin              [description]
258
 * @param string|null       $idFonctions          [description]
259
 *
260
 * @return bool
261
 */
262
function identifyUserRights(
263
    $groupesVisiblesUser,
264
    $groupesInterditsUser,
265
    $isAdmin,
266
    ?string $idFonctions,
267
    $SETTINGS
268
) {
269
    $session = SessionManager::getSession();
270
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
271
272
    // Check if user is ADMINISTRATOR    
273
    (int) $isAdmin === 1 ?
274
        identAdmin(
275
            $idFonctions ?? ''
276
        )
277
        :
278
        identUser(
279
            $groupesVisiblesUser,
280
            $groupesInterditsUser,
281
            $idFonctions,
282
            $SETTINGS, /** @scrutinizer ignore-type */
283
            $tree
284
        );
285
286
    // update user's timestamp
287
    DB::update(
288
        prefixTable('users'),
289
        [
290
            'timestamp' => time(),
291
        ],
292
        'id=%i',
293
        $session->get('user-id')
294
    );
295
296
    return true;
297
}
298
299
/**
300
 * Identify administrator.
301
 *
302
 * @param string $idFonctions Roles of user
303
 *
304
 * @return bool
305
 */
306
function identAdmin(string $idFonctions)
307
{
308
    
309
    $session = SessionManager::getSession();
310
    $groupesVisibles = [];
311
    $session->set('user-personal_folders', []);
312
    $session->set('user-accessible_folders', []);
313
    $session->set('user-no_access_folders', []);
314
    $session->set('user-personal_visible_folders', []);
315
    $session->set('user-read_only_folders', []);
316
    $session->set('system-list_restricted_folders_for_items', []);
317
    $session->set('system-list_folders_editable_by_role', []);
318
    $session->set('user-list_folders_limited', []);
319
    $session->set('user-forbiden_personal_folders', []);
320
    
321
    // Get list of Folders
322
    $rows = DB::query('SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i', 0);
323
    foreach ($rows as $record) {
324
        array_push($groupesVisibles, $record['id']);
325
    }
326
    $session->set('user-accessible_folders', $groupesVisibles);
327
328
    // get complete list of ROLES
329
    $tmp = array_filter(explode(';', $idFonctions !== null ? $idFonctions : ''));
0 ignored issues
show
introduced by
The condition $idFonctions !== null is always true.
Loading history...
330
    $rows = DB::query(
331
        'SELECT * FROM ' . prefixTable('roles_title') . '
332
        ORDER BY title ASC'
333
    );
334
    foreach ($rows as $record) {
335
        if (! empty($record['id']) && ! in_array($record['id'], $tmp)) {
336
            array_push($tmp, $record['id']);
337
        }
338
    }
339
    $session->set('user-roles', implode(';', $tmp));
340
    $session->set('user-admin', 1);
341
    // Check if admin has created Folders and Roles
342
    DB::query('SELECT * FROM ' . prefixTable('nested_tree') . '');
343
    $session->set('user-nb_folders', DB::count());
344
    DB::query('SELECT * FROM ' . prefixTable('roles_title'));
345
    $session->set('user-nb_roles', DB::count());
346
347
    return true;
348
}
349
350
/**
351
 * Permits to convert an element to array.
352
 *
353
 * @param string|array $element Any value to be returned as array
354
 *
355
 * @return array
356
 */
357
function convertToArray($element): ?array
358
{
359
    if (is_string($element) === true) {
360
        if (empty($element) === true) {
361
            return [];
362
        }
363
        return explode(
364
            ';',
365
            trimElement($element, ';')
366
        );
367
    }
368
    return $element;
369
}
370
371
/**
372
 * Defines the rights the user has.
373
 *
374
 * @param string|array $allowedFolders  Allowed folders
375
 * @param string|array $noAccessFolders Not allowed folders
376
 * @param string|array $userRoles       Roles of user
377
 * @param array        $SETTINGS        Teampass settings
378
 * @param object       $tree            Tree of folders
379
 * 
380
 * @return bool
381
 */
382
function identUser(
383
    $allowedFolders,
384
    $noAccessFolders,
385
    $userRoles,
386
    array $SETTINGS,
387
    object $tree
388
) {
389
    $session = SessionManager::getSession();
390
    // Init
391
    $session->set('user-accessible_folders', []);
392
    $session->set('user-personal_folders', []);
393
    $session->set('user-no_access_folders', []);
394
    $session->set('user-personal_visible_folders', []);
395
    $session->set('user-read_only_folders', []);
396
    $session->set('user-user-roles', $userRoles);
397
    $session->set('user-admin', 0);
398
    // init
399
    $personalFolders = [];
400
    $readOnlyFolders = [];
401
    $noAccessPersonalFolders = [];
402
    $restrictedFoldersForItems = [];
403
    $foldersLimited = [];
404
    $foldersLimitedFull = [];
405
    $allowedFoldersByRoles = [];
406
    $globalsUserId = $session->get('user-id');
407
    $globalsPersonalFolders = $session->get('user-personal_folder_enabled');
408
    // Ensure consistency in array format
409
    $noAccessFolders = convertToArray($noAccessFolders);
410
    $userRoles = convertToArray($userRoles);
411
    $allowedFolders = convertToArray($allowedFolders);
412
    $session->set('user-allowed_folders_by_definition', $allowedFolders);
413
    
414
    // Get list of folders depending on Roles
415
    $arrays = identUserGetFoldersFromRoles(
416
        $userRoles,
417
        $allowedFoldersByRoles,
418
        $readOnlyFolders,
419
        $allowedFolders
420
    );
421
    $allowedFoldersByRoles = $arrays['allowedFoldersByRoles'];
422
    $readOnlyFolders = $arrays['readOnlyFolders'];
423
424
    // Does this user is allowed to see other items
425
    $inc = 0;
426
    $rows = DB::query(
427
        'SELECT id, id_tree FROM ' . prefixTable('items') . '
428
            WHERE restricted_to LIKE %ss AND inactif = %s'.
429
            (count($allowedFolders) > 0 ? ' AND id_tree NOT IN ('.implode(',', $allowedFolders).')' : ''),
430
        $globalsUserId,
431
        '0'
432
    );
433
    foreach ($rows as $record) {
434
        // Exclude restriction on item if folder is fully accessible
435
        //if (in_array($record['id_tree'], $allowedFolders) === false) {
436
            $restrictedFoldersForItems[$record['id_tree']][$inc] = $record['id'];
437
            ++$inc;
438
        //}
439
    }
440
441
    // Check for the users roles if some specific rights exist on items
442
    $rows = DB::query(
443
        'SELECT i.id_tree, r.item_id
444
        FROM ' . prefixTable('items') . ' as i
445
        INNER JOIN ' . prefixTable('restriction_to_roles') . ' as r ON (r.item_id=i.id)
446
        WHERE i.id_tree <> "" '.
447
        (count($userRoles) > 0 ? 'AND r.role_id IN %li ' : '').
448
        'ORDER BY i.id_tree ASC',
449
        $userRoles
450
    );
451
    $inc = 0;
452
    foreach ($rows as $record) {
453
        //if (isset($record['id_tree'])) {
454
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
455
            array_push($foldersLimitedFull, $record['id_tree']);
456
            ++$inc;
457
        //}
458
    }
459
460
    // Get list of Personal Folders
461
    $arrays = identUserGetPFList(
462
        $globalsPersonalFolders,
463
        $allowedFolders,
464
        $globalsUserId,
465
        $personalFolders,
466
        $noAccessPersonalFolders,
467
        $foldersLimitedFull,
468
        $allowedFoldersByRoles,
469
        array_keys($restrictedFoldersForItems),
470
        $readOnlyFolders,
471
        $noAccessFolders,
472
        isset($SETTINGS['enable_pf_feature']) === true ? $SETTINGS['enable_pf_feature'] : 0,
473
        $tree
474
    );
475
    $allowedFolders = $arrays['allowedFolders'];
476
    $personalFolders = $arrays['personalFolders'];
477
    $noAccessPersonalFolders = $arrays['noAccessPersonalFolders'];
478
479
    // Return data
480
    $session->set('user-accessible_folders', array_unique(array_merge($allowedFolders, $personalFolders), SORT_NUMERIC));
481
    $session->set('user-read_only_folders', $readOnlyFolders);
482
    $session->set('user-no_access_folders', $noAccessFolders);
483
    $session->set('user-personal_folders', $personalFolders);
484
    $session->set('user-list_folders_limited', $foldersLimited);
485
    $session->set('system-list_folders_editable_by_role', $allowedFoldersByRoles, 'SESSION');
486
    $session->set('system-list_restricted_folders_for_items', $restrictedFoldersForItems);
487
    $session->set('user-forbiden_personal_folders', $noAccessPersonalFolders);
488
    // Folders and Roles numbers
489
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('nested_tree') . '');
490
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('nested_tree') . '');
491
    $session->set('user-nb_folders', DB::count());
492
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('roles_title'));
493
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('roles_title'));
494
    $session->set('user-nb_roles', DB::count());
495
    // check if change proposals on User's items
496
    if (isset($SETTINGS['enable_suggestion']) === true && (int) $SETTINGS['enable_suggestion'] === 1) {
497
        $countNewItems = DB::query(
498
            'SELECT COUNT(*)
499
            FROM ' . prefixTable('items_change') . ' AS c
500
            LEFT JOIN ' . prefixTable('log_items') . ' AS i ON (c.item_id = i.id_item)
501
            WHERE i.action = %s AND i.id_user = %i',
502
            'at_creation',
503
            $globalsUserId
504
        );
505
        $session->set('user-nb_item_change_proposals', $countNewItems);
506
    } else {
507
        $session->set('user-nb_item_change_proposals', 0);
508
    }
509
510
    return true;
511
}
512
513
/**
514
 * Get list of folders depending on Roles
515
 * 
516
 * @param array $userRoles
517
 * @param array $allowedFoldersByRoles
518
 * @param array $readOnlyFolders
519
 * @param array $allowedFolders
520
 * 
521
 * @return array
522
 */
523
function identUserGetFoldersFromRoles(array $userRoles, array $allowedFoldersByRoles = [], array $readOnlyFolders = [], array $allowedFolders = []) : array
524
{
525
    $rows = DB::query(
526
        'SELECT *
527
        FROM ' . prefixTable('roles_values') . '
528
        WHERE type IN %ls'.(count($userRoles) > 0 ? ' AND role_id IN %li' : ''),
529
        ['W', 'ND', 'NE', 'NDNE', 'R'],
530
        $userRoles,
531
    );
532
    foreach ($rows as $record) {
533
        if ($record['type'] === 'R') {
534
            array_push($readOnlyFolders, $record['folder_id']);
535
        } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
536
            array_push($allowedFoldersByRoles, $record['folder_id']);
537
        }
538
    }
539
    $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
540
    $readOnlyFolders = array_unique($readOnlyFolders);
541
    
542
    // Clean arrays
543
    foreach ($allowedFoldersByRoles as $value) {
544
        $key = array_search($value, $readOnlyFolders);
545
        if ($key !== false) {
546
            unset($readOnlyFolders[$key]);
547
        }
548
    }
549
    return [
550
        'readOnlyFolders' => $readOnlyFolders,
551
        'allowedFoldersByRoles' => $allowedFoldersByRoles
552
    ];
553
}
554
555
/**
556
 * Get list of Personal Folders
557
 * 
558
 * @param int $globalsPersonalFolders
559
 * @param array $allowedFolders
560
 * @param int $globalsUserId
561
 * @param array $personalFolders
562
 * @param array $noAccessPersonalFolders
563
 * @param array $foldersLimitedFull
564
 * @param array $allowedFoldersByRoles
565
 * @param array $restrictedFoldersForItems
566
 * @param array $readOnlyFolders
567
 * @param array $noAccessFolders
568
 * @param int $enablePfFeature
569
 * @param object $tree
570
 * 
571
 * @return array
572
 */
573
function identUserGetPFList(
574
    $globalsPersonalFolders,
575
    $allowedFolders,
576
    $globalsUserId,
577
    $personalFolders,
578
    $noAccessPersonalFolders,
579
    $foldersLimitedFull,
580
    $allowedFoldersByRoles,
581
    $restrictedFoldersForItems,
582
    $readOnlyFolders,
583
    $noAccessFolders,
584
    $enablePfFeature,
585
    $tree
586
)
587
{
588
    if (
589
        (int) $enablePfFeature === 1
590
        && (int) $globalsPersonalFolders === 1
591
    ) {
592
        $persoFld = DB::queryFirstRow(
593
            'SELECT id
594
            FROM ' . prefixTable('nested_tree') . '
595
            WHERE title = %s AND personal_folder = %i'.
596
            (count($allowedFolders) > 0 ? ' AND id NOT IN ('.implode(',', $allowedFolders).')' : ''),
597
            $globalsUserId,
598
            1
599
        );
600
        if (empty($persoFld['id']) === false) {
601
            array_push($personalFolders, $persoFld['id']);
602
            array_push($allowedFolders, $persoFld['id']);
603
            // get all descendants
604
            $ids = $tree->getDescendants($persoFld['id'], false, false, true);
605
            foreach ($ids as $id) {
606
                //array_push($allowedFolders, $id);
607
                array_push($personalFolders, $id);
608
            }
609
        }
610
    }
611
    
612
    // Exclude all other PF
613
    $where = new WhereClause('and');
614
    $where->add('personal_folder=%i', 1);
615
    if (count($personalFolders) > 0) {
616
        $where->add('id NOT IN ('.implode(',', $personalFolders).')');
617
    }
618
    if (
619
        (int) $enablePfFeature === 1
620
        && (int) $globalsPersonalFolders === 1
621
    ) {
622
        $where->add('title=%s', $globalsUserId);
623
        $where->negateLast();
624
    }
625
    $persoFlds = DB::query(
626
        'SELECT id
627
        FROM ' . prefixTable('nested_tree') . '
628
        WHERE %l',
629
        $where
630
    );
631
    foreach ($persoFlds as $persoFldId) {
632
        array_push($noAccessPersonalFolders, $persoFldId['id']);
633
    }
634
635
    // All folders visibles
636
    $allowedFolders = array_unique(array_merge(
637
        $allowedFolders,
638
        $foldersLimitedFull,
639
        $allowedFoldersByRoles,
640
        $restrictedFoldersForItems,
641
        $readOnlyFolders
642
    ), SORT_NUMERIC);
643
    // Exclude from allowed folders all the specific user forbidden folders
644
    if (count($noAccessFolders) > 0) {
645
        $allowedFolders = array_diff($allowedFolders, $noAccessFolders);
646
    }
647
648
    return [
649
        'allowedFolders' => array_diff(array_diff($allowedFolders, $noAccessPersonalFolders), $personalFolders),
650
        'personalFolders' => $personalFolders,
651
        'noAccessPersonalFolders' => $noAccessPersonalFolders
652
    ];
653
}
654
655
656
/**
657
 * Update the CACHE table.
658
 *
659
 * @param string $action   What to do
660
 * @param array  $SETTINGS Teampass settings
661
 * @param int    $ident    Ident format
662
 * 
663
 * @return void
664
 */
665
function updateCacheTable(string $action, ?int $ident = null): void
666
{
667
    if ($action === 'reload') {
668
        // Rebuild full cache table
669
        cacheTableRefresh();
670
    } elseif ($action === 'update_value' && is_null($ident) === false) {
671
        // UPDATE an item
672
        cacheTableUpdate($ident);
673
    } elseif ($action === 'add_value' && is_null($ident) === false) {
674
        // ADD an item
675
        cacheTableAdd($ident);
676
    } elseif ($action === 'delete_value' && is_null($ident) === false) {
677
        // DELETE an item
678
        DB::delete(prefixTable('cache'), 'id = %i', $ident);
679
    }
680
}
681
682
/**
683
 * Cache table - refresh.
684
 *
685
 * @return void
686
 */
687
function cacheTableRefresh(): void
688
{
689
    // Load class DB
690
    loadClasses('DB');
691
692
    //Load Tree
693
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
694
    // truncate table
695
    DB::query('TRUNCATE TABLE ' . prefixTable('cache'));
696
    // reload date
697
    $rows = DB::query(
698
        'SELECT *
699
        FROM ' . prefixTable('items') . ' as i
700
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
701
        AND l.action = %s
702
        AND i.inactif = %i',
703
        'at_creation',
704
        0
705
    );
706
    foreach ($rows as $record) {
707
        if (empty($record['id_tree']) === false) {
708
            // Get all TAGS
709
            $tags = '';
710
            $itemTags = DB::query(
711
                'SELECT tag
712
                FROM ' . prefixTable('tags') . '
713
                WHERE item_id = %i AND tag != ""',
714
                $record['id']
715
            );
716
            foreach ($itemTags as $itemTag) {
717
                $tags .= $itemTag['tag'] . ' ';
718
            }
719
720
            // Get renewal period
721
            $resNT = DB::queryFirstRow(
722
                'SELECT renewal_period
723
                FROM ' . prefixTable('nested_tree') . '
724
                WHERE id = %i',
725
                $record['id_tree']
726
            );
727
            // form id_tree to full foldername
728
            $folder = [];
729
            $arbo = $tree->getPath($record['id_tree'], true);
730
            foreach ($arbo as $elem) {
731
                // Check if title is the ID of a user
732
                if (is_numeric($elem->title) === true) {
733
                    // Is this a User id?
734
                    $user = DB::queryFirstRow(
735
                        'SELECT login
736
                        FROM ' . prefixTable('users') . '
737
                        WHERE id = %i',
738
                        $elem->title
739
                    );
740
                    if ($user !== null) {
741
                        $elem->title = $user['login'];
742
                    }
743
                }
744
                // Build path
745
                array_push($folder, stripslashes($elem->title));
746
            }
747
            // store data
748
            DB::insert(
749
                prefixTable('cache'),
750
                [
751
                    'id' => $record['id'],
752
                    'label' => $record['label'],
753
                    'description' => $record['description'] ?? '',
754
                    'url' => isset($record['url']) && ! empty($record['url']) ? $record['url'] : '0',
755
                    'tags' => $tags,
756
                    'id_tree' => $record['id_tree'],
757
                    'perso' => $record['perso'],
758
                    'restricted_to' => isset($record['restricted_to']) && ! empty($record['restricted_to']) ? $record['restricted_to'] : '0',
759
                    'login' => $record['login'] ?? '',
760
                    'folder' => implode(' » ', $folder),
761
                    'author' => $record['id_user'],
762
                    'renewal_period' => $resNT['renewal_period'] ?? '0',
763
                    'timestamp' => $record['date'],
764
                ]
765
            );
766
        }
767
    }
768
}
769
770
/**
771
 * Cache table - update existing value.
772
 *
773
 * @param int    $ident    Ident format
774
 * 
775
 * @return void
776
 */
777
function cacheTableUpdate(?int $ident = null): void
778
{
779
    $session = SessionManager::getSession();
780
    loadClasses('DB');
781
782
    //Load Tree
783
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
784
    // get new value from db
785
    $data = DB::queryFirstRow(
786
        'SELECT label, description, id_tree, perso, restricted_to, login, url
787
        FROM ' . prefixTable('items') . '
788
        WHERE id=%i',
789
        $ident
790
    );
791
    // Get all TAGS
792
    $tags = '';
793
    $itemTags = DB::query(
794
        'SELECT tag
795
            FROM ' . prefixTable('tags') . '
796
            WHERE item_id = %i AND tag != ""',
797
        $ident
798
    );
799
    foreach ($itemTags as $itemTag) {
800
        $tags .= $itemTag['tag'] . ' ';
801
    }
802
    // form id_tree to full foldername
803
    $folder = [];
804
    $arbo = $tree->getPath($data['id_tree'], true);
805
    foreach ($arbo as $elem) {
806
        // Check if title is the ID of a user
807
        if (is_numeric($elem->title) === true) {
808
            // Is this a User id?
809
            $user = DB::queryFirstRow(
810
                'SELECT id, login
811
                FROM ' . prefixTable('users') . '
812
                WHERE id = %i',
813
                $elem->title
814
            );
815
            if ($user !== null) {
816
                $elem->title = $user['login'];
817
            }
818
        }
819
        // Build path
820
        array_push($folder, stripslashes($elem->title));
821
    }
822
    // finaly update
823
    DB::update(
824
        prefixTable('cache'),
825
        [
826
            'label' => $data['label'],
827
            'description' => $data['description'],
828
            'tags' => $tags,
829
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
830
            'id_tree' => $data['id_tree'],
831
            'perso' => $data['perso'],
832
            'restricted_to' => isset($data['restricted_to']) && ! empty($data['restricted_to']) ? $data['restricted_to'] : '0',
833
            'login' => $data['login'] ?? '',
834
            'folder' => implode(' » ', $folder),
835
            'author' => $session->get('user-id'),
836
        ],
837
        'id = %i',
838
        $ident
839
    );
840
}
841
842
/**
843
 * Cache table - add new value.
844
 *
845
 * @param int    $ident    Ident format
846
 * 
847
 * @return void
848
 */
849
function cacheTableAdd(?int $ident = null): void
850
{
851
    $session = SessionManager::getSession();
852
    $globalsUserId = $session->get('user-id');
853
854
    // Load class DB
855
    loadClasses('DB');
856
857
    //Load Tree
858
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
859
    // get new value from db
860
    $data = DB::queryFirstRow(
861
        'SELECT i.label, i.description, i.id_tree as id_tree, i.perso, i.restricted_to, i.id, i.login, i.url, l.date
862
        FROM ' . prefixTable('items') . ' as i
863
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
864
        WHERE i.id = %i
865
        AND l.action = %s',
866
        $ident,
867
        'at_creation'
868
    );
869
    // Get all TAGS
870
    $tags = '';
871
    $itemTags = DB::query(
872
        'SELECT tag
873
            FROM ' . prefixTable('tags') . '
874
            WHERE item_id = %i AND tag != ""',
875
        $ident
876
    );
877
    foreach ($itemTags as $itemTag) {
878
        $tags .= $itemTag['tag'] . ' ';
879
    }
880
    // form id_tree to full foldername
881
    $folder = [];
882
    $arbo = $tree->getPath($data['id_tree'], true);
883
    foreach ($arbo as $elem) {
884
        // Check if title is the ID of a user
885
        if (is_numeric($elem->title) === true) {
886
            // Is this a User id?
887
            $user = DB::queryFirstRow(
888
                'SELECT id, login
889
                FROM ' . prefixTable('users') . '
890
                WHERE id = %i',
891
                $elem->title
892
            );
893
            if ($user !== null) {
894
                $elem->title = $user['login'];
895
            }
896
        }
897
        // Build path
898
        array_push($folder, stripslashes($elem->title));
899
    }
900
    // finaly update
901
    DB::insert(
902
        prefixTable('cache'),
903
        [
904
            'id' => $data['id'],
905
            'label' => $data['label'],
906
            'description' => $data['description'],
907
            'tags' => empty($tags) === false ? $tags : 'None',
908
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
909
            'id_tree' => $data['id_tree'],
910
            'perso' => isset($data['perso']) && empty($data['perso']) === false && $data['perso'] !== 'None' ? $data['perso'] : '0',
911
            'restricted_to' => isset($data['restricted_to']) && empty($data['restricted_to']) === false ? $data['restricted_to'] : '0',
912
            'login' => $data['login'] ?? '',
913
            'folder' => implode(' » ', $folder),
914
            'author' => $globalsUserId,
915
            'timestamp' => $data['date'],
916
        ]
917
    );
918
}
919
920
/**
921
 * Do statistics.
922
 *
923
 * @param array $SETTINGS Teampass settings
924
 *
925
 * @return array
926
 */
927
function getStatisticsData(array $SETTINGS): array
928
{
929
    DB::query(
930
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
931
        0
932
    );
933
    $counter_folders = DB::count();
934
    DB::query(
935
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
936
        1
937
    );
938
    $counter_folders_perso = DB::count();
939
    DB::query(
940
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
941
        0
942
    );
943
    $counter_items = DB::count();
944
        DB::query(
945
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
946
        1
947
    );
948
    $counter_items_perso = DB::count();
949
        DB::query(
950
        'SELECT id FROM ' . prefixTable('users') . ' WHERE login NOT IN (%s, %s, %s)',
951
        'OTV', 'TP', 'API'
952
    );
953
    $counter_users = DB::count();
954
        DB::query(
955
        'SELECT id FROM ' . prefixTable('users') . ' WHERE admin = %i',
956
        1
957
    );
958
    $admins = DB::count();
959
    DB::query(
960
        'SELECT id FROM ' . prefixTable('users') . ' WHERE gestionnaire = %i',
961
        1
962
    );
963
    $managers = DB::count();
964
    DB::query(
965
        'SELECT id FROM ' . prefixTable('users') . ' WHERE read_only = %i',
966
        1
967
    );
968
    $readOnly = DB::count();
969
    // list the languages
970
    $usedLang = [];
971
    $tp_languages = DB::query(
972
        'SELECT name FROM ' . prefixTable('languages')
973
    );
974
    foreach ($tp_languages as $tp_language) {
975
        DB::query(
976
            'SELECT * FROM ' . prefixTable('users') . ' WHERE user_language = %s',
977
            $tp_language['name']
978
        );
979
        $usedLang[$tp_language['name']] = round((DB::count() * 100 / $counter_users), 0);
980
    }
981
982
    // get list of ips
983
    $usedIp = [];
984
    $tp_ips = DB::query(
985
        'SELECT user_ip FROM ' . prefixTable('users')
986
    );
987
    foreach ($tp_ips as $ip) {
988
        if (array_key_exists($ip['user_ip'], $usedIp)) {
989
            $usedIp[$ip['user_ip']] += $usedIp[$ip['user_ip']];
990
        } elseif (! empty($ip['user_ip']) && $ip['user_ip'] !== 'none') {
991
            $usedIp[$ip['user_ip']] = 1;
992
        }
993
    }
994
995
    return [
996
        'error' => '',
997
        'stat_phpversion' => phpversion(),
998
        'stat_folders' => $counter_folders,
999
        'stat_folders_shared' => intval($counter_folders) - intval($counter_folders_perso),
1000
        'stat_items' => $counter_items,
1001
        'stat_items_shared' => intval($counter_items) - intval($counter_items_perso),
1002
        'stat_users' => $counter_users,
1003
        'stat_admins' => $admins,
1004
        'stat_managers' => $managers,
1005
        'stat_ro' => $readOnly,
1006
        'stat_kb' => $SETTINGS['enable_kb'],
1007
        'stat_pf' => $SETTINGS['enable_pf_feature'],
1008
        'stat_fav' => $SETTINGS['enable_favourites'],
1009
        'stat_teampassversion' => TP_VERSION,
1010
        'stat_ldap' => $SETTINGS['ldap_mode'],
1011
        'stat_agses' => $SETTINGS['agses_authentication_enabled'],
1012
        'stat_duo' => $SETTINGS['duo'],
1013
        'stat_suggestion' => $SETTINGS['enable_suggestion'],
1014
        'stat_api' => $SETTINGS['api'],
1015
        'stat_customfields' => $SETTINGS['item_extra_fields'],
1016
        'stat_syslog' => $SETTINGS['syslog_enable'],
1017
        'stat_2fa' => $SETTINGS['google_authentication'],
1018
        'stat_stricthttps' => $SETTINGS['enable_sts'],
1019
        'stat_mysqlversion' => DB::serverVersion(),
1020
        'stat_languages' => $usedLang,
1021
        'stat_country' => $usedIp,
1022
    ];
1023
}
1024
1025
/**
1026
 * Permits to prepare the way to send the email
1027
 * 
1028
 * @param string $subject       email subject
1029
 * @param string $body          email message
1030
 * @param string $email         email
1031
 * @param string $receiverName  Receiver name
1032
 * @param string $encryptedUserPassword      encryptedUserPassword
1033
 *
1034
 * @return void
1035
 */
1036
function prepareSendingEmail(
1037
    $subject,
1038
    $body,
1039
    $email,
1040
    $receiverName = '',
1041
    $encryptedUserPassword = ''
1042
): void 
1043
{
1044
    DB::insert(
1045
        prefixTable('background_tasks'),
1046
        array(
1047
            'created_at' => time(),
1048
            'process_type' => 'send_email',
1049
            'arguments' => json_encode([
1050
                'subject' => html_entity_decode($subject, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
1051
                'receivers' => $email,
1052
                'body' => $body,
1053
                'receiver_name' => $receiverName,
1054
                'encryptedUserPassword' => $encryptedUserPassword,
1055
            ], JSON_HEX_QUOT | JSON_HEX_TAG),
1056
        )
1057
    );
1058
}
1059
1060
/**
1061
 * Returns the email body.
1062
 *
1063
 * @param string $textMail Text for the email
1064
 */
1065
function emailBody(string $textMail): string
1066
{
1067
    return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
1068
    w3.org/TR/html4/loose.dtd"><html>
1069
    <head><title>Email Template</title>
1070
    <style type="text/css">
1071
    body { background-color: #f0f0f0; padding: 10px 0; margin:0 0 10px =0; }
1072
    </style></head>
1073
    <body style="-ms-text-size-adjust: none; size-adjust: none; margin: 0; padding: 10px 0; background-color: #f0f0f0;" bgcolor="#f0f0f0" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
1074
    <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#f0f0f0" style="border-spacing: 0;">
1075
    <tr><td style="border-collapse: collapse;"><br>
1076
        <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#17357c" style="border-spacing: 0; margin-bottom: 25px;">
1077
        <tr><td style="border-collapse: collapse; padding: 11px 20px;">
1078
            <div style="max-width:150px; max-height:34px; color:#f0f0f0; font-weight:bold;">Teampass</div>
1079
        </td></tr></table></td>
1080
    </tr>
1081
    <tr><td align="center" valign="top" bgcolor="#f0f0f0" style="border-collapse: collapse; background-color: #f0f0f0;">
1082
        <table width="600" cellpadding="0" cellspacing="0" border="0" class="container" bgcolor="#ffffff" style="border-spacing: 0; border-bottom: 1px solid #e0e0e0; box-shadow: 0 0 3px #ddd; color: #434343; font-family: Helvetica, Verdana, sans-serif;">
1083
        <tr><td class="container-padding" bgcolor="#ffffff" style="border-collapse: collapse; border-left: 1px solid #e0e0e0; background-color: #ffffff; padding-left: 30px; padding-right: 30px;">
1084
        <br><div style="float:right;">' .
1085
        $textMail .
1086
        '<br><br></td></tr></table>
1087
    </td></tr></table>
1088
    <br></body></html>';
1089
}
1090
1091
/**
1092
 * Convert date to timestamp.
1093
 *
1094
 * @param string $date        The date
1095
 * @param string $date_format Date format
1096
 *
1097
 * @return int
1098
 */
1099
function dateToStamp(string $date, string $date_format): int
1100
{
1101
    $date = date_parse_from_format($date_format, $date);
1102
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1103
        return mktime(
1104
            empty($date['hour']) === false ? $date['hour'] : 23,
1105
            empty($date['minute']) === false ? $date['minute'] : 59,
1106
            empty($date['second']) === false ? $date['second'] : 59,
1107
            $date['month'],
1108
            $date['day'],
1109
            $date['year']
1110
        );
1111
    }
1112
    return 0;
1113
}
1114
1115
/**
1116
 * Is this a date.
1117
 *
1118
 * @param string $date Date
1119
 *
1120
 * @return bool
1121
 */
1122
function isDate(string $date): bool
1123
{
1124
    return strtotime($date) !== false;
1125
}
1126
1127
/**
1128
 * Check if isUTF8().
1129
 *
1130
 * @param string|array $string Is the string
1131
 *
1132
 * @return int is the string in UTF8 format
1133
 */
1134
function isUTF8($string): int
1135
{
1136
    if (is_array($string) === true) {
1137
        $string = $string['string'];
1138
    }
1139
1140
    return preg_match(
1141
        '%^(?:
1142
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1143
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1144
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1145
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1146
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1147
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1148
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1149
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1150
        )*$%xs',
1151
        $string
1152
    );
1153
}
1154
1155
/**
1156
 * Prepare an array to UTF8 format before JSON_encode.
1157
 *
1158
 * @param array $array Array of values
1159
 *
1160
 * @return array
1161
 */
1162
function utf8Converter(array $array): array
1163
{
1164
    array_walk_recursive(
1165
        $array,
1166
        static function (&$item): void {
1167
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1168
                $item = mb_convert_encoding($item, 'ISO-8859-1', 'UTF-8');
1169
            }
1170
        }
1171
    );
1172
    return $array;
1173
}
1174
1175
/**
1176
 * Permits to prepare data to be exchanged.
1177
 *
1178
 * @param array|string $data Text
1179
 * @param string       $type Parameter
1180
 * @param string       $key  Optional key
1181
 *
1182
 * @return string|array
1183
 */
1184
function prepareExchangedData($data, string $type, ?string $key = null)
1185
{
1186
    $session = SessionManager::getSession();
1187
    $key = empty($key) ? $session->get('key') : $key;
1188
    
1189
    // Perform
1190
    if ($type === 'encode' && is_array($data) === true) {
1191
        // json encoding
1192
        $data = json_encode(
1193
            $data,
1194
            JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1195
        );
1196
        
1197
        // Now encrypt
1198
        if ((int) $session->get('encryptClientServer') === 1) {
1199
            $data = Encryption::encrypt(
1200
                $data,
1201
                $key
1202
            );
1203
        }
1204
1205
        return $data;
1206
    }
1207
1208
    if ($type === 'decode' && is_array($data) === false) {
1209
        // Decrypt if needed
1210
        if ((int) $session->get('encryptClientServer') === 1) {
1211
            $data = (string) Encryption::decrypt(
1212
                (string) $data,
1213
                $key
1214
            );
1215
        } else {
1216
            // Double html encoding received
1217
            $data = html_entity_decode(html_entity_decode(/** @scrutinizer ignore-type */$data)); // @codeCoverageIgnore Is always a string (not an array)
1218
        }
1219
1220
        // Check if $data is a valid string before json_decode
1221
        if (is_string($data) && !empty($data)) {
1222
            // Return data array
1223
            return json_decode($data, true);
1224
        }
1225
    }
1226
1227
    return '';
1228
}
1229
1230
1231
/**
1232
 * Create a thumbnail.
1233
 *
1234
 * @param string  $src           Source
1235
 * @param string  $dest          Destination
1236
 * @param int $desired_width Size of width
1237
 * 
1238
 * @return void|string|bool
1239
 */
1240
function makeThumbnail(string $src, string $dest, int $desired_width)
1241
{
1242
    /* read the source image */
1243
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1244
        $source_image = imagecreatefrompng($src);
1245
        if ($source_image === false) {
1246
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1247
        }
1248
    } else {
1249
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1250
    }
1251
1252
    // Get height and width
1253
    $width = imagesx($source_image);
1254
    $height = imagesy($source_image);
1255
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1256
    $desired_height = (int) floor($height * $desired_width / $width);
1257
    /* create a new, "virtual" image */
1258
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1259
    if ($virtual_image === false) {
1260
        return false;
1261
    }
1262
    /* copy source image at a resized size */
1263
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1264
    /* create the physical thumbnail image to its destination */
1265
    imagejpeg($virtual_image, $dest);
1266
}
1267
1268
/**
1269
 * Check table prefix in SQL query.
1270
 *
1271
 * @param string $table Table name
1272
 * 
1273
 * @return string
1274
 */
1275
function prefixTable(string $table): string
1276
{
1277
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1278
    return $safeTable;
1279
}
1280
1281
/**
1282
 * GenerateCryptKey
1283
 *
1284
 * @param int     $size      Length
1285
 * @param bool $secure Secure
1286
 * @param bool $numerals Numerics
1287
 * @param bool $uppercase Uppercase letters
1288
 * @param bool $symbols Symbols
1289
 * @param bool $lowercase Lowercase
1290
 * 
1291
 * @return string
1292
 */
1293
function GenerateCryptKey(
1294
    int $size = 20,
1295
    bool $secure = false,
1296
    bool $numerals = false,
1297
    bool $uppercase = false,
1298
    bool $symbols = false,
1299
    bool $lowercase = false
1300
): string {
1301
    $generator = new ComputerPasswordGenerator();
1302
    $generator->setRandomGenerator(new Php7RandomGenerator());
1303
    
1304
    // Manage size
1305
    $generator->setLength((int) $size);
1306
    if ($secure === true) {
1307
        $generator->setSymbols(true);
1308
        $generator->setLowercase(true);
1309
        $generator->setUppercase(true);
1310
        $generator->setNumbers(true);
1311
    } else {
1312
        $generator->setLowercase($lowercase);
1313
        $generator->setUppercase($uppercase);
1314
        $generator->setNumbers($numerals);
1315
        $generator->setSymbols($symbols);
1316
    }
1317
1318
    return $generator->generatePasswords()[0];
1319
}
1320
1321
/**
1322
 * GenerateGenericPassword
1323
 *
1324
 * @param int     $size      Length
1325
 * @param bool $secure Secure
1326
 * @param bool $numerals Numerics
1327
 * @param bool $uppercase Uppercase letters
1328
 * @param bool $symbols Symbols
1329
 * @param bool $lowercase Lowercase
1330
 * @param array   $SETTINGS  SETTINGS
1331
 * 
1332
 * @return string
1333
 */
1334
function generateGenericPassword(
1335
    int $size,
1336
    bool $secure,
1337
    bool $lowercase,
1338
    bool $capitalize,
1339
    bool $numerals,
1340
    bool $symbols,
1341
    array $SETTINGS
1342
): string
1343
{
1344
    if ((int) $size > (int) $SETTINGS['pwd_maximum_length']) {
1345
        return prepareExchangedData(
1346
            array(
1347
                'error_msg' => 'Password length is too long! ',
1348
                'error' => 'true',
1349
            ),
1350
            'encode'
1351
        );
1352
    }
1353
    // Load libraries
1354
    $generator = new ComputerPasswordGenerator();
1355
    $generator->setRandomGenerator(new Php7RandomGenerator());
1356
1357
    // Manage size
1358
    $generator->setLength(($size <= 0) ? 10 : $size);
1359
1360
    if ($secure === true) {
1361
        $generator->setSymbols(true);
1362
        $generator->setLowercase(true);
1363
        $generator->setUppercase(true);
1364
        $generator->setNumbers(true);
1365
    } else {
1366
        $generator->setLowercase($lowercase);
1367
        $generator->setUppercase($capitalize);
1368
        $generator->setNumbers($numerals);
1369
        $generator->setSymbols($symbols);
1370
    }
1371
1372
    return prepareExchangedData(
1373
        array(
1374
            'key' => $generator->generatePasswords(),
1375
            'error' => '',
1376
        ),
1377
        'encode'
1378
    );
1379
}
1380
1381
/**
1382
 * Send sysLOG message
1383
 *
1384
 * @param string    $message
1385
 * @param string    $host
1386
 * @param int       $port
1387
 * @param string    $component
1388
 * 
1389
 * @return void
1390
*/
1391
function send_syslog($message, $host, $port, $component = 'teampass'): void
1392
{
1393
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1394
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1395
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1396
    socket_close($sock);
1397
}
1398
1399
/**
1400
 * Permits to log events into DB
1401
 *
1402
 * @param array  $SETTINGS Teampass settings
1403
 * @param string $type     Type
1404
 * @param string $label    Label
1405
 * @param string $who      Who
1406
 * @param string $login    Login
1407
 * @param string|int $field_1  Field
1408
 * 
1409
 * @return void
1410
 */
1411
function logEvents(
1412
    array $SETTINGS, 
1413
    string $type, 
1414
    string $label, 
1415
    string $who, 
1416
    ?string $login = null, 
1417
    $field_1 = null
1418
): void
1419
{
1420
    if (empty($who)) {
1421
        $who = getClientIpServer();
1422
    }
1423
1424
    // Load class DB
1425
    loadClasses('DB');
1426
1427
    DB::insert(
1428
        prefixTable('log_system'),
1429
        [
1430
            'type' => $type,
1431
            'date' => time(),
1432
            'label' => $label,
1433
            'qui' => $who,
1434
            'field_1' => $field_1 === null ? '' : $field_1,
1435
        ]
1436
    );
1437
    // If SYSLOG
1438
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1439
        if ($type === 'user_mngt') {
1440
            send_syslog(
1441
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1442
                $SETTINGS['syslog_host'],
1443
                $SETTINGS['syslog_port'],
1444
                'teampass'
1445
            );
1446
        } else {
1447
            send_syslog(
1448
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1449
                $SETTINGS['syslog_host'],
1450
                $SETTINGS['syslog_port'],
1451
                'teampass'
1452
            );
1453
        }
1454
    }
1455
}
1456
1457
/**
1458
 * Log events.
1459
 *
1460
 * @param array  $SETTINGS        Teampass settings
1461
 * @param int    $item_id         Item id
1462
 * @param string $item_label      Item label
1463
 * @param int    $id_user         User id
1464
 * @param string $action          Code for reason
1465
 * @param string $login           User login
1466
 * @param string $raison          Code for reason
1467
 * @param string $encryption_type Encryption on
1468
 * @param string $time Encryption Time
1469
 * @param string $old_value       Old value
1470
 * 
1471
 * @return void
1472
 */
1473
function logItems(
1474
    array $SETTINGS,
1475
    int $item_id,
1476
    string $item_label,
1477
    int $id_user,
1478
    string $action,
1479
    ?string $login = null,
1480
    ?string $raison = null,
1481
    ?string $encryption_type = null,
1482
    ?string $time = null,
1483
    ?string $old_value = null
1484
): void {
1485
    // Load class DB
1486
    loadClasses('DB');
1487
1488
    // Insert log in DB
1489
    DB::insert(
1490
        prefixTable('log_items'),
1491
        [
1492
            'id_item' => $item_id,
1493
            'date' => is_null($time) === true ? time() : $time,
1494
            'id_user' => $id_user,
1495
            'action' => $action,
1496
            'raison' => $raison,
1497
            'old_value' => $old_value,
1498
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1499
        ]
1500
    );
1501
    // Timestamp the last change
1502
    if (in_array($action, ['at_creation', 'at_modifiation', 'at_delete', 'at_import'], true)) {
1503
        DB::update(
1504
            prefixTable('misc'),
1505
            [
1506
                'valeur' => time(),
1507
                'updated_at' => time(),
1508
            ],
1509
            'type = %s AND intitule = %s',
1510
            'timestamp',
1511
            'last_item_change'
1512
        );
1513
    }
1514
1515
    // SYSLOG
1516
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1517
        // Extract reason
1518
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1519
        // Get item info if not known
1520
        if (empty($item_label) === true) {
1521
            $dataItem = DB::queryFirstRow(
1522
                'SELECT id, id_tree, label
1523
                FROM ' . prefixTable('items') . '
1524
                WHERE id = %i',
1525
                $item_id
1526
            );
1527
            $item_label = $dataItem['label'];
1528
        }
1529
1530
        send_syslog(
1531
            'action=' . str_replace('at_', '', $action) .
1532
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1533
                ' itemno=' . $item_id .
1534
                ' user=' . (is_null($login) === true ? '' : addslashes((string) $login)) .
1535
                ' itemname="' . addslashes($item_label) . '"',
1536
            $SETTINGS['syslog_host'],
1537
            $SETTINGS['syslog_port'],
1538
            'teampass'
1539
        );
1540
    }
1541
1542
    // send notification if enabled
1543
    //notifyOnChange($item_id, $action, $SETTINGS);
1544
}
1545
1546
/**
1547
 * Prepare notification email to subscribers.
1548
 *
1549
 * @param int    $item_id  Item id
1550
 * @param string $label    Item label
1551
 * @param array  $changes  List of changes
1552
 * @param array  $SETTINGS Teampass settings
1553
 * 
1554
 * @return void
1555
 */
1556
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1557
{
1558
    $session = SessionManager::getSession();
1559
    $lang = new Language($session->get('user-language') ?? 'english');
1560
    $globalsUserId = $session->get('user-id');
1561
    $globalsLastname = $session->get('user-lastname');
1562
    $globalsName = $session->get('user-name');
1563
    // send email to user that what to be notified
1564
    $notification = DB::queryFirstField(
1565
        'SELECT email
1566
        FROM ' . prefixTable('notification') . ' AS n
1567
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1568
        WHERE n.item_id = %i AND n.user_id != %i',
1569
        $item_id,
1570
        $globalsUserId
1571
    );
1572
    if (DB::count() > 0) {
1573
        // Prepare path
1574
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1575
        // Get list of changes
1576
        $htmlChanges = '<ul>';
1577
        foreach ($changes as $change) {
1578
            $htmlChanges .= '<li>' . $change . '</li>';
1579
        }
1580
        $htmlChanges .= '</ul>';
1581
        // send email
1582
        DB::insert(
1583
            prefixTable('emails'),
1584
            [
1585
                'timestamp' => time(),
1586
                'subject' => $lang->get('email_subject_item_updated'),
1587
                'body' => str_replace(
1588
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1589
                    [$label, $path, (string) $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1590
                    $lang->get('email_body_item_updated')
1591
                ),
1592
                'receivers' => implode(',', $notification),
1593
                'status' => '',
1594
            ]
1595
        );
1596
    }
1597
}
1598
1599
/**
1600
 * Returns the Item + path.
1601
 *
1602
 * @param int    $id_tree  Node id
1603
 * @param string $label    Label
1604
 * @param array  $SETTINGS TP settings
1605
 * 
1606
 * @return string
1607
 */
1608
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1609
{
1610
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1611
    $arbo = $tree->getPath($id_tree, true);
1612
    $path = '';
1613
    foreach ($arbo as $elem) {
1614
        if (empty($path) === true) {
1615
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1616
        } else {
1617
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1618
        }
1619
    }
1620
1621
    // Build text to show user
1622
    if (empty($label) === false) {
1623
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1624
    }
1625
    return empty($path) === true ? '' : $path;
1626
}
1627
1628
/**
1629
 * Get the client ip address.
1630
 *
1631
 * @return string IP address
1632
 */
1633
function getClientIpServer(): string
1634
{
1635
    if (getenv('HTTP_CLIENT_IP')) {
1636
        $ipaddress = getenv('HTTP_CLIENT_IP');
1637
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1638
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1639
    } elseif (getenv('HTTP_X_FORWARDED')) {
1640
        $ipaddress = getenv('HTTP_X_FORWARDED');
1641
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1642
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1643
    } elseif (getenv('HTTP_FORWARDED')) {
1644
        $ipaddress = getenv('HTTP_FORWARDED');
1645
    } elseif (getenv('REMOTE_ADDR')) {
1646
        $ipaddress = getenv('REMOTE_ADDR');
1647
    } else {
1648
        $ipaddress = 'UNKNOWN';
1649
    }
1650
1651
    return $ipaddress;
1652
}
1653
1654
/**
1655
 * Escape all HTML, JavaScript, and CSS.
1656
 *
1657
 * @param string $input    The input string
1658
 * @param string $encoding Which character encoding are we using?
1659
 * 
1660
 * @return string
1661
 */
1662
function noHTML(string $input, string $encoding = 'UTF-8'): string
1663
{
1664
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1665
}
1666
1667
/**
1668
 * Rebuilds the Teampass config file.
1669
 *
1670
 * @param string $configFilePath Path to the config file.
1671
 * @param array  $settings       Teampass settings.
1672
 *
1673
 * @return string|bool
1674
 */
1675
function rebuildConfigFile(string $configFilePath, array $settings)
1676
{
1677
    // Perform a copy if the file exists
1678
    if (file_exists($configFilePath)) {
1679
        $backupFilePath = $configFilePath . '.' . date('Y_m_d_His', time());
1680
        if (!copy($configFilePath, $backupFilePath)) {
1681
            return "ERROR: Could not copy file '$configFilePath'";
1682
        }
1683
    }
1684
1685
    // Regenerate the config file
1686
    $data = ["<?php\n", "global \$SETTINGS;\n", "\$SETTINGS = array (\n"];
1687
    $rows = DB::query('SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s', 'admin');
1688
    foreach ($rows as $record) {
1689
        $value = getEncryptedValue($record['valeur'], $record['is_encrypted']);
1690
        $data[] = "    '{$record['intitule']}' => '". htmlspecialchars_decode($value, ENT_COMPAT) . "',\n";
1691
    }
1692
    $data[] = ");\n";
1693
    $data = array_unique($data);
1694
1695
    // Update the file
1696
    file_put_contents($configFilePath, implode('', $data));
1697
1698
    return true;
1699
}
1700
1701
/**
1702
 * Returns the encrypted value if needed.
1703
 *
1704
 * @param string $value       Value to encrypt.
1705
 * @param int   $isEncrypted Is the value encrypted?
1706
 *
1707
 * @return string
1708
 */
1709
function getEncryptedValue(string $value, int $isEncrypted): string
1710
{
1711
    return $isEncrypted ? cryption($value, '', 'encrypt')['string'] : $value;
1712
}
1713
1714
/**
1715
 * Permits to replace &#92; to permit correct display
1716
 *
1717
 * @param string $input Some text
1718
 * 
1719
 * @return string
1720
 */
1721
function handleBackslash(string $input): string
1722
{
1723
    return str_replace('&amp;#92;', '&#92;', $input);
1724
}
1725
1726
/**
1727
 * Permits to load settings
1728
 * 
1729
 * @return void
1730
*/
1731
function loadSettings(): void
1732
{
1733
    global $SETTINGS;
1734
    /* LOAD CPASSMAN SETTINGS */
1735
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1736
        $SETTINGS = [];
1737
        $SETTINGS['duplicate_folder'] = 0;
1738
        //by default, this is set to 0;
1739
        $SETTINGS['duplicate_item'] = 0;
1740
        //by default, this is set to 0;
1741
        $SETTINGS['number_of_used_pw'] = 5;
1742
        //by default, this value is set to 5;
1743
        $settings = [];
1744
        $rows = DB::query(
1745
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1746
            [
1747
                'type' => 'admin',
1748
                'type2' => 'settings',
1749
            ]
1750
        );
1751
        foreach ($rows as $record) {
1752
            if ($record['type'] === 'admin') {
1753
                $SETTINGS[$record['intitule']] = $record['valeur'];
1754
            } else {
1755
                $settings[$record['intitule']] = $record['valeur'];
1756
            }
1757
        }
1758
        $SETTINGS['loaded'] = 1;
1759
        $SETTINGS['default_session_expiration_time'] = 5;
1760
    }
1761
}
1762
1763
/**
1764
 * check if folder has custom fields.
1765
 * Ensure that target one also has same custom fields
1766
 * 
1767
 * @param int $source_id
1768
 * @param int $target_id 
1769
 * 
1770
 * @return bool
1771
*/
1772
function checkCFconsistency(int $source_id, int $target_id): bool
1773
{
1774
    $source_cf = [];
1775
    $rows = DB::query(
1776
        'SELECT id_category
1777
            FROM ' . prefixTable('categories_folders') . '
1778
            WHERE id_folder = %i',
1779
        $source_id
1780
    );
1781
    foreach ($rows as $record) {
1782
        array_push($source_cf, $record['id_category']);
1783
    }
1784
1785
    $target_cf = [];
1786
    $rows = DB::query(
1787
        'SELECT id_category
1788
            FROM ' . prefixTable('categories_folders') . '
1789
            WHERE id_folder = %i',
1790
        $target_id
1791
    );
1792
    foreach ($rows as $record) {
1793
        array_push($target_cf, $record['id_category']);
1794
    }
1795
1796
    $cf_diff = array_diff($source_cf, $target_cf);
1797
    if (count($cf_diff) > 0) {
1798
        return false;
1799
    }
1800
1801
    return true;
1802
}
1803
1804
/**
1805
 * Will encrypte/decrypt a fil eusing Defuse.
1806
 *
1807
 * @param string $type        can be either encrypt or decrypt
1808
 * @param string $source_file path to source file
1809
 * @param string $target_file path to target file
1810
 * @param array  $SETTINGS    Settings
1811
 * @param string $password    A password
1812
 *
1813
 * @return string|bool
1814
 */
1815
function prepareFileWithDefuse(
1816
    string $type,
1817
    string $source_file,
1818
    string $target_file,
1819
    ?string $password = null
1820
) {
1821
    // Load AntiXSS
1822
    $antiXss = new AntiXSS();
1823
    // Protect against bad inputs
1824
    if (is_array($source_file) === true || is_array($target_file) === true) {
1825
        return 'error_cannot_be_array';
1826
    }
1827
1828
    // Sanitize
1829
    $source_file = $antiXss->xss_clean($source_file);
1830
    $target_file = $antiXss->xss_clean($target_file);
1831
    if (empty($password) === true || is_null($password) === true) {
1832
        // get KEY to define password
1833
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
1834
        $password = Key::loadFromAsciiSafeString($ascii_key);
1835
    }
1836
1837
    $err = '';
1838
    if ($type === 'decrypt') {
1839
        // Decrypt file
1840
        $err = defuseFileDecrypt(
1841
            $source_file,
1842
            $target_file,
1843
            $password
0 ignored issues
show
Bug introduced by
It seems like $password can also be of type Defuse\Crypto\Key; however, parameter $password of defuseFileDecrypt() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1843
            /** @scrutinizer ignore-type */ $password
Loading history...
1844
        );
1845
    } elseif ($type === 'encrypt') {
1846
        // Encrypt file
1847
        $err = defuseFileEncrypt(
1848
            $source_file,
1849
            $target_file,
1850
            $password
0 ignored issues
show
Bug introduced by
It seems like $password can also be of type Defuse\Crypto\Key; however, parameter $password of defuseFileEncrypt() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

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

1850
            /** @scrutinizer ignore-type */ $password
Loading history...
1851
        );
1852
    }
1853
1854
    // return error
1855
    return $err === true ? $err : '';
1856
}
1857
1858
/**
1859
 * Encrypt a file with Defuse.
1860
 *
1861
 * @param string $source_file path to source file
1862
 * @param string $target_file path to target file
1863
 * @param array  $SETTINGS    Settings
1864
 * @param string $password    A password
1865
 *
1866
 * @return string|bool
1867
 */
1868
function defuseFileEncrypt(
1869
    string $source_file,
1870
    string $target_file,
1871
    ?string $password = null
1872
) {
1873
    $err = '';
1874
    try {
1875
        CryptoFile::encryptFileWithPassword(
1876
            $source_file,
1877
            $target_file,
1878
            $password
1879
        );
1880
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
1881
        $err = 'wrong_key';
1882
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
1883
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
1884
        $err = 'environment_error';
1885
    } catch (CryptoException\IOException $ex) {
1886
        error_log('TEAMPASS-Error-General: ' . $ex->getMessage());
1887
        $err = 'general_error';
1888
    }
1889
1890
    // return error
1891
    return empty($err) === false ? $err : true;
1892
}
1893
1894
/**
1895
 * Decrypt a file with Defuse.
1896
 *
1897
 * @param string $source_file path to source file
1898
 * @param string $target_file path to target file
1899
 * @param string $password    A password
1900
 *
1901
 * @return string|bool
1902
 */
1903
function defuseFileDecrypt(
1904
    string $source_file,
1905
    string $target_file,
1906
    ?string $password = null
1907
) {
1908
    $err = '';
1909
    try {
1910
        CryptoFile::decryptFileWithPassword(
1911
            $source_file,
1912
            $target_file,
1913
            $password
1914
        );
1915
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
1916
        $err = 'wrong_key';
1917
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
1918
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
1919
        $err = 'environment_error';
1920
    } catch (CryptoException\IOException $ex) {
1921
        error_log('TEAMPASS-Error-General: ' . $ex->getMessage());
1922
        $err = 'general_error';
1923
    }
1924
1925
    // return error
1926
    return empty($err) === false ? $err : true;
1927
}
1928
1929
/*
1930
* NOT TO BE USED
1931
*/
1932
/**
1933
 * Undocumented function.
1934
 *
1935
 * @param string $text Text to debug
1936
 */
1937
function debugTeampass(string $text): void
1938
{
1939
    $debugFile = fopen('D:/wamp64/www/TeamPass/debug.txt', 'r+');
1940
    if ($debugFile !== false) {
1941
        fputs($debugFile, $text);
1942
        fclose($debugFile);
1943
    }
1944
}
1945
1946
/**
1947
 * DELETE the file with expected command depending on server type.
1948
 *
1949
 * @param string $file     Path to file
1950
 * @param array  $SETTINGS Teampass settings
1951
 *
1952
 * @return void
1953
 */
1954
function fileDelete(string $file, array $SETTINGS): void
1955
{
1956
    // Load AntiXSS
1957
    $antiXss = new AntiXSS();
1958
    $file = $antiXss->xss_clean($file);
1959
    if (is_file($file)) {
1960
        unlink($file);
1961
    }
1962
}
1963
1964
/**
1965
 * Permits to extract the file extension.
1966
 *
1967
 * @param string $file File name
1968
 *
1969
 * @return string
1970
 */
1971
function getFileExtension(string $file): string
1972
{
1973
    if (strpos($file, '.') === false) {
1974
        return $file;
1975
    }
1976
1977
    return substr($file, strrpos($file, '.') + 1);
1978
}
1979
1980
/**
1981
 * Chmods files and folders with different permissions.
1982
 *
1983
 * This is an all-PHP alternative to using: \n
1984
 * <tt>exec("find ".$path." -type f -exec chmod 644 {} \;");</tt> \n
1985
 * <tt>exec("find ".$path." -type d -exec chmod 755 {} \;");</tt>
1986
 *
1987
 * @author Jeppe Toustrup (tenzer at tenzer dot dk)
1988
  *
1989
 * @param string $path      An either relative or absolute path to a file or directory which should be processed.
1990
 * @param int    $filePerm The permissions any found files should get.
1991
 * @param int    $dirPerm  The permissions any found folder should get.
1992
 *
1993
 * @return bool Returns TRUE if the path if found and FALSE if not.
1994
 *
1995
 * @warning The permission levels has to be entered in octal format, which
1996
 * normally means adding a zero ("0") in front of the permission level. \n
1997
 * More info at: http://php.net/chmod.
1998
*/
1999
2000
function recursiveChmod(
2001
    string $path,
2002
    int $filePerm = 0644,
2003
    int  $dirPerm = 0755
2004
) {
2005
    // Check if the path exists
2006
    $path = basename($path);
2007
    if (! file_exists($path)) {
2008
        return false;
2009
    }
2010
2011
    // See whether this is a file
2012
    if (is_file($path)) {
2013
        // Chmod the file with our given filepermissions
2014
        try {
2015
            chmod($path, $filePerm);
2016
        } catch (Exception $e) {
2017
            return false;
2018
        }
2019
    // If this is a directory...
2020
    } elseif (is_dir($path)) {
2021
        // Then get an array of the contents
2022
        $foldersAndFiles = scandir($path);
2023
        // Remove "." and ".." from the list
2024
        $entries = array_slice($foldersAndFiles, 2);
2025
        // Parse every result...
2026
        foreach ($entries as $entry) {
2027
            // And call this function again recursively, with the same permissions
2028
            recursiveChmod($path.'/'.$entry, $filePerm, $dirPerm);
2029
        }
2030
2031
        // When we are done with the contents of the directory, we chmod the directory itself
2032
        try {
2033
            chmod($path, $filePerm);
2034
        } catch (Exception $e) {
2035
            return false;
2036
        }
2037
    }
2038
2039
    // Everything seemed to work out well, return true
2040
    return true;
2041
}
2042
2043
/**
2044
 * Check if user can access to this item.
2045
 *
2046
 * @param int   $item_id ID of item
2047
 * @param array $SETTINGS
2048
 *
2049
 * @return bool|string
2050
 */
2051
function accessToItemIsGranted(int $item_id, array $SETTINGS)
2052
{
2053
    
2054
    $session = SessionManager::getSession();
2055
    $session_groupes_visibles = $session->get('user-accessible_folders');
2056
    $session_list_restricted_folders_for_items = $session->get('system-list_restricted_folders_for_items');
2057
    // Load item data
2058
    $data = DB::queryFirstRow(
2059
        'SELECT id_tree
2060
        FROM ' . prefixTable('items') . '
2061
        WHERE id = %i',
2062
        $item_id
2063
    );
2064
    // Check if user can access this folder
2065
    if (in_array($data['id_tree'], $session_groupes_visibles) === false) {
2066
        // Now check if this folder is restricted to user
2067
        if (isset($session_list_restricted_folders_for_items[$data['id_tree']]) === true
2068
            && in_array($item_id, $session_list_restricted_folders_for_items[$data['id_tree']]) === false
2069
        ) {
2070
            return 'ERR_FOLDER_NOT_ALLOWED';
2071
        }
2072
    }
2073
2074
    return true;
2075
}
2076
2077
/**
2078
 * Creates a unique key.
2079
 *
2080
 * @param int $lenght Key lenght
2081
 *
2082
 * @return string
2083
 */
2084
function uniqidReal(int $lenght = 13): string
2085
{
2086
    if (function_exists('random_bytes')) {
2087
        $bytes = random_bytes(intval(ceil($lenght / 2)));
2088
    } elseif (function_exists('openssl_random_pseudo_bytes')) {
2089
        $bytes = openssl_random_pseudo_bytes(intval(ceil($lenght / 2)));
2090
    } else {
2091
        throw new Exception('no cryptographically secure random function available');
2092
    }
2093
2094
    return substr(bin2hex($bytes), 0, $lenght);
2095
}
2096
2097
/**
2098
 * Obfuscate an email.
2099
 *
2100
 * @param string $email Email address
2101
 *
2102
 * @return string
2103
 */
2104
function obfuscateEmail(string $email): string
2105
{
2106
    $email = explode("@", $email);
2107
    $name = $email[0];
2108
    if (strlen($name) > 3) {
2109
        $name = substr($name, 0, 2);
2110
        for ($i = 0; $i < strlen($email[0]) - 3; $i++) {
2111
            $name .= "*";
2112
        }
2113
        $name .= substr($email[0], -1, 1);
2114
    }
2115
    $host = explode(".", $email[1])[0];
2116
    if (strlen($host) > 3) {
2117
        $host = substr($host, 0, 1);
2118
        for ($i = 0; $i < strlen(explode(".", $email[1])[0]) - 2; $i++) {
2119
            $host .= "*";
2120
        }
2121
        $host .= substr(explode(".", $email[1])[0], -1, 1);
2122
    }
2123
    $email = $name . "@" . $host . "." . explode(".", $email[1])[1];
2124
    return $email;
2125
}
2126
2127
/**
2128
 * Get id and title from role_titles table.
2129
 *
2130
 * @return array
2131
 */
2132
function getRolesTitles(): array
2133
{
2134
    // Load class DB
2135
    loadClasses('DB');
2136
    
2137
    // Insert log in DB
2138
    return DB::query(
2139
        'SELECT id, title
2140
        FROM ' . prefixTable('roles_title')
2141
    );
2142
}
2143
2144
/**
2145
 * Undocumented function.
2146
 *
2147
 * @param int $bytes Size of file
2148
 *
2149
 * @return string
2150
 */
2151
function formatSizeUnits(int $bytes): string
2152
{
2153
    if ($bytes >= 1073741824) {
2154
        $bytes = number_format($bytes / 1073741824, 2) . ' GB';
2155
    } elseif ($bytes >= 1048576) {
2156
        $bytes = number_format($bytes / 1048576, 2) . ' MB';
2157
    } elseif ($bytes >= 1024) {
2158
        $bytes = number_format($bytes / 1024, 2) . ' KB';
2159
    } elseif ($bytes > 1) {
2160
        $bytes .= ' bytes';
2161
    } elseif ($bytes === 1) {
2162
        $bytes .= ' byte';
2163
    } else {
2164
        $bytes = '0 bytes';
2165
    }
2166
2167
    return $bytes;
2168
}
2169
2170
/**
2171
 * Generate user pair of keys.
2172
 *
2173
 * @param string $userPwd User password
2174
 *
2175
 * @return array
2176
 */
2177
function generateUserKeys(string $userPwd, ?array $SETTINGS = null): array
2178
{
2179
    // Sanitize
2180
    $antiXss = new AntiXSS();
2181
    $userPwd = $antiXss->xss_clean($userPwd);
2182
    // Load classes
2183
    $rsa = new Crypt_RSA();
2184
    $cipher = new Crypt_AES();
2185
    // Create the private and public key
2186
    $res = $rsa->createKey(4096);
2187
    // Encrypt the privatekey
2188
    $cipher->setPassword($userPwd);
2189
    $privatekey = $cipher->encrypt($res['privatekey']);
2190
2191
    $result = [
2192
        'private_key' => base64_encode($privatekey),
2193
        'public_key' => base64_encode($res['publickey']),
2194
        'private_key_clear' => base64_encode($res['privatekey']),
2195
    ];
2196
2197
    // Generate transparent recovery data
2198
    // Generate unique seed for this user
2199
    $userSeed = bin2hex(openssl_random_pseudo_bytes(32));
2200
2201
    // Derive backup encryption key
2202
    $derivedKey = deriveBackupKey($userSeed, $result['public_key'], $SETTINGS);
2203
2204
    // Encrypt private key with derived key (backup)
2205
    $cipherBackup = new Crypt_AES();
2206
    $cipherBackup->setPassword($derivedKey);
2207
    $privatekeyBackup = $cipherBackup->encrypt($res['privatekey']);
2208
2209
    // Generate integrity hash
2210
    $serverSecret = getServerSecret();
2211
    $integrityHash = generateKeyIntegrityHash($userSeed, $result['public_key'], $serverSecret);
2212
2213
    $result['user_seed'] = $userSeed;
2214
    $result['private_key_backup'] = base64_encode($privatekeyBackup);
2215
    $result['key_integrity_hash'] = $integrityHash;
2216
2217
    return $result;
2218
}
2219
2220
/**
2221
 * Permits to decrypt the user's privatekey.
2222
 *
2223
 * @param string $userPwd        User password
2224
 * @param string $userPrivateKey User private key
2225
 *
2226
 * @return string|object
2227
 */
2228
function decryptPrivateKey(string $userPwd, string $userPrivateKey)
2229
{
2230
    // Sanitize
2231
    $antiXss = new AntiXSS();
2232
    $userPwd = $antiXss->xss_clean($userPwd);
2233
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2234
2235
    if (empty($userPwd) === false) {
2236
        // Load classes
2237
        $cipher = new Crypt_AES();
2238
        // Encrypt the privatekey
2239
        $cipher->setPassword($userPwd);
2240
        try {
2241
            return base64_encode((string) $cipher->decrypt(base64_decode($userPrivateKey)));
2242
        } catch (Exception $e) {
2243
            return $e;
2244
        }
2245
    }
2246
    return '';
2247
}
2248
2249
/**
2250
 * Permits to encrypt the user's privatekey.
2251
 *
2252
 * @param string $userPwd        User password
2253
 * @param string $userPrivateKey User private key
2254
 *
2255
 * @return string
2256
 */
2257
function encryptPrivateKey(string $userPwd, string $userPrivateKey): string
2258
{
2259
    // Sanitize
2260
    $antiXss = new AntiXSS();
2261
    $userPwd = $antiXss->xss_clean($userPwd);
2262
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2263
2264
    if (empty($userPwd) === false) {
2265
        // Load classes
2266
        $cipher = new Crypt_AES();
2267
        // Encrypt the privatekey
2268
        $cipher->setPassword($userPwd);        
2269
        try {
2270
            return base64_encode($cipher->encrypt(base64_decode($userPrivateKey)));
2271
        } catch (Exception $e) {
2272
            return $e->getMessage();
2273
        }
2274
    }
2275
    return '';
2276
}
2277
2278
/**
2279
 * Derives a backup encryption key from user seed and public key.
2280
 * Uses PBKDF2 with 100k iterations for strong key derivation.
2281
 *
2282
 * @param string $userSeed User's unique derivation seed (64 hex chars)
2283
 * @param string $publicKey User's public RSA key (base64 encoded)
2284
 * @param array $SETTINGS Teampass settings
2285
 *
2286
 * @return string Derived key (32 bytes, raw binary)
2287
 */
2288
function deriveBackupKey(string $userSeed, string $publicKey, ?array $SETTINGS = null): string
2289
{
2290
    // Sanitize inputs
2291
    $antiXss = new AntiXSS();
2292
    $userSeed = $antiXss->xss_clean($userSeed);
2293
    $publicKey = $antiXss->xss_clean($publicKey);
2294
2295
    // Use public key hash as salt for key derivation
2296
    $salt = hash('sha256', $publicKey, true);
2297
2298
    // Get PBKDF2 iterations from settings (default 100000)
2299
    $iterations = isset($SETTINGS['transparent_key_recovery_pbkdf2_iterations'])
2300
        ? (int) $SETTINGS['transparent_key_recovery_pbkdf2_iterations']
2301
        : 100000;
2302
2303
    // PBKDF2 key derivation with SHA256
2304
    return hash_pbkdf2(
2305
        'sha256',
2306
        hex2bin($userSeed),
2307
        $salt,
2308
        $iterations,
2309
        32, // 256 bits key length
2310
        true // raw binary output
2311
    );
2312
}
2313
2314
/**
2315
 * Generates key integrity hash to detect tampering.
2316
 *
2317
 * @param string $userSeed User derivation seed
2318
 * @param string $publicKey User public key
2319
 * @param string $serverSecret Server-wide secret key
2320
 *
2321
 * @return string HMAC hash (64 hex chars)
2322
 */
2323
function generateKeyIntegrityHash(string $userSeed, string $publicKey, string $serverSecret): string
2324
{
2325
    return hash_hmac('sha256', $userSeed . $publicKey, $serverSecret);
2326
}
2327
2328
/**
2329
 * Verifies key integrity to detect SQL injection or tampering.
2330
 *
2331
 * @param array $userInfo User information from database
2332
 * @param string $serverSecret Server-wide secret key
2333
 *
2334
 * @return bool True if integrity is valid
2335
 */
2336
function verifyKeyIntegrity(array $userInfo, string $serverSecret): bool
2337
{
2338
    // Skip check if no integrity hash stored (legacy users)
2339
    if (empty($userInfo['key_integrity_hash'])) {
2340
        return true;
2341
    }
2342
2343
    if (empty($userInfo['user_derivation_seed']) || empty($userInfo['public_key'])) {
2344
        return false;
2345
    }
2346
2347
    $expectedHash = generateKeyIntegrityHash(
2348
        $userInfo['user_derivation_seed'],
2349
        $userInfo['public_key'],
2350
        $serverSecret
2351
    );
2352
2353
    return hash_equals($expectedHash, $userInfo['key_integrity_hash']);
2354
}
2355
2356
/**
2357
 * Gets server secret for integrity checks.
2358
 * Reads from file or generates if not exists.
2359
 * *
2360
 * @return string Server secret key
2361
 */
2362
function getServerSecret(): string
2363
{
2364
    $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
2365
    $key = Key::loadFromAsciiSafeString($ascii_key);
2366
    return $key->saveToAsciiSafeString();
2367
}
2368
2369
/**
2370
 * Attempts transparent recovery when password change is detected.
2371
 *
2372
 * @param array $userInfo User information from database
2373
 * @param string $newPassword New password (clear)
2374
 * @param array $SETTINGS Teampass settings
2375
 *
2376
 * @return array Result with private key or error
2377
 */
2378
function attemptTransparentRecovery(array $userInfo, string $newPassword, array $SETTINGS): array
2379
{
2380
    $session = SessionManager::getSession();
2381
    try {
2382
        // Check if user has recovery data
2383
        if (empty($userInfo['user_derivation_seed']) || empty($userInfo['private_key_backup'])) {
2384
            return [
2385
                'success' => false,
2386
                'error' => 'no_recovery_data',
2387
                'private_key_clear' => '',
2388
            ];
2389
        }
2390
2391
        // Verify key integrity
2392
        $serverSecret = getServerSecret();
2393
        if (!verifyKeyIntegrity($userInfo, $serverSecret)) {
2394
            // Critical security event - integrity check failed
2395
            logEvents(
2396
                $SETTINGS,
2397
                'security_alert',
2398
                'key_integrity_check_failed',
2399
                (string) $userInfo['id'],
2400
                'User: ' . $userInfo['login']
2401
            );
2402
            return [
2403
                'success' => false,
2404
                'error' => 'integrity_check_failed',
2405
                'private_key_clear' => '',
2406
            ];
2407
        }
2408
2409
        // Derive backup key
2410
        $derivedKey = deriveBackupKey(
2411
            $userInfo['user_derivation_seed'],
2412
            $userInfo['public_key'],
2413
            $SETTINGS
2414
        );
2415
2416
        // Decrypt private key using derived key
2417
        $cipher = new Crypt_AES();
2418
        $cipher->setPassword($derivedKey);
2419
        $privateKeyClear = base64_encode($cipher->decrypt(base64_decode($userInfo['private_key_backup'])));
2420
2421
        // Re-encrypt with new password
2422
        $newPrivateKeyEncrypted = encryptPrivateKey($newPassword, $privateKeyClear);
2423
2424
        // Re-encrypt backup with derived key (refresh)
2425
        $cipher->setPassword($derivedKey);
2426
        $newPrivateKeyBackup = base64_encode($cipher->encrypt(base64_decode($privateKeyClear)));
2427
        
2428
        // Update database
2429
        DB::update(
2430
            prefixTable('users'),
2431
            [
2432
                'private_key' => $newPrivateKeyEncrypted,
2433
                'private_key_backup' => $newPrivateKeyBackup,
2434
                'last_pw_change' => time(),
2435
                'special' => 'none',
2436
            ],
2437
            'id = %i',
2438
            $userInfo['id']
2439
        );
2440
2441
        // Log success
2442
        logEvents(
2443
            $SETTINGS,
2444
            'user_connection',
2445
            'auto_reencryption_success',
2446
            (string) $userInfo['id'],
2447
            'User: ' . $userInfo['login']
2448
        );
2449
2450
        // Store in session for immediate use
2451
        $session->set('user-private_key', $privateKeyClear);
2452
        $session->set('user-private_key_recovered', true);
2453
2454
        return [
2455
            'success' => true,
2456
            'error' => '',
2457
        ];
2458
2459
    } catch (Exception $e) {
2460
        // Log failure
2461
        logEvents(
2462
            $SETTINGS,
2463
            'security_alert',
2464
            'auto_reencryption_failed',
2465
            (string) $userInfo['id'],
2466
            'User: ' . $userInfo['login'] . ' - Error: ' . $e->getMessage()
2467
        );
2468
2469
        return [
2470
            'success' => false,
2471
            'error' => 'decryption_failed: ' . $e->getMessage(),
2472
            'private_key_clear' => '',
2473
        ];
2474
    }
2475
}
2476
2477
/**
2478
 * Handles external password change (LDAP/OAuth2) with automatic re-encryption.
2479
 *
2480
 * @param int $userId User ID
2481
 * @param string $newPassword New password (clear)
2482
 * @param array $userInfo User information from database
2483
 * @param array $SETTINGS Teampass settings
2484
 *
2485
 * @return bool True if  handled successfully
2486
 */
2487
function handleExternalPasswordChange(int $userId, string $newPassword, array $userInfo, array $SETTINGS): bool
2488
{
2489
    // Check if password was changed recently (< 30s) to avoid duplicate processing
2490
    if (!empty($userInfo['last_pw_change'])) {
2491
        $timeSinceChange = time() - (int) $userInfo['last_pw_change'];
2492
        if ($timeSinceChange < 30) { // 30 seconds
2493
            return true; // Already processed
2494
        }
2495
    }
2496
2497
    // Try to decrypt with new password first (maybe already updated)
2498
    try {
2499
        $testDecrypt = decryptPrivateKey($newPassword, $userInfo['private_key']);
2500
        if (!empty($testDecrypt)) {
2501
            // Password already works, just update timestamp
2502
            DB::update(
2503
                prefixTable('users'),
2504
                [
2505
                    'last_pw_change' => time(),
2506
                    'otp_provided' => 1,
2507
                    'special' => 'none',
2508
                ],
2509
                'id = %i',
2510
                $userId
2511
            );
2512
            return true;
2513
        }
2514
    } catch (Exception $e) {
2515
        // Expected - old password doesn't work, continue with recovery
2516
    }
2517
2518
    // Attempt transparent recovery
2519
    $result = attemptTransparentRecovery($userInfo, $newPassword, $SETTINGS);
2520
2521
    if ($result['success']) {
2522
        return true;
2523
    }
2524
2525
    // Recovery failed - disable user and alert admins
2526
    DB::update(
2527
        prefixTable('users'),
2528
        [
2529
            'disabled' => 1,
2530
            'special' => 'recrypt-private-key',
2531
        ],
2532
        'id = %i',
2533
        $userId
2534
    );
2535
2536
    // Log critical event
2537
    logEvents(
2538
        $SETTINGS,
2539
        'security_alert',
2540
        'auto_reencryption_critical_failure',
2541
        (string) $userId,
2542
        'User: ' . $userInfo['login'] . ' - disabled due to key recovery failure'
2543
    );
2544
2545
    return false;
2546
}
2547
2548
/**
2549
 * Encrypts a string using AES.
2550
 *
2551
 * @param string $data String to encrypt
2552
 * @param string $key
2553
 *
2554
 * @return array
2555
 */
2556
function doDataEncryption(string $data, ?string $key = null): array
2557
{
2558
    // Sanitize
2559
    $antiXss = new AntiXSS();
2560
    $data = $antiXss->xss_clean($data);
2561
    
2562
    // Load classes
2563
    $cipher = new Crypt_AES(CRYPT_AES_MODE_CBC);
2564
    // Generate an object key
2565
    $objectKey = is_null($key) === true ? uniqidReal(KEY_LENGTH) : $antiXss->xss_clean($key);
2566
    // Set it as password
2567
    $cipher->setPassword($objectKey);
2568
    return [
2569
        'encrypted' => base64_encode($cipher->encrypt($data)),
2570
        'objectKey' => base64_encode($objectKey),
2571
    ];
2572
}
2573
2574
/**
2575
 * Decrypts a string using AES.
2576
 *
2577
 * @param string $data Encrypted data
2578
 * @param string $key  Key to uncrypt
2579
 *
2580
 * @return string
2581
 */
2582
function doDataDecryption(string $data, string $key): string
2583
{
2584
    // Sanitize
2585
    $antiXss = new AntiXSS();
2586
    $data = $antiXss->xss_clean($data);
2587
    $key = $antiXss->xss_clean($key);
2588
2589
    // Load classes
2590
    $cipher = new Crypt_AES();
2591
    // Set the object key
2592
    $cipher->setPassword(base64_decode($key));
2593
    return base64_encode((string) $cipher->decrypt(base64_decode($data)));
2594
}
2595
2596
/**
2597
 * Encrypts using RSA a string using a public key.
2598
 *
2599
 * @param string $key       Key to be encrypted
2600
 * @param string $publicKey User public key
2601
 *
2602
 * @return string
2603
 */
2604
function encryptUserObjectKey(string $key, string $publicKey): string
2605
{
2606
    // Empty password
2607
    if (empty($key)) return '';
2608
2609
    // Sanitize
2610
    $antiXss = new AntiXSS();
2611
    $publicKey = $antiXss->xss_clean($publicKey);
2612
    // Load classes
2613
    $rsa = new Crypt_RSA();
2614
    // Load the public key
2615
    $decodedPublicKey = base64_decode($publicKey, true);
2616
    if ($decodedPublicKey === false) {
2617
        throw new InvalidArgumentException("Error while decoding key.");
2618
    }
2619
    $rsa->loadKey($decodedPublicKey);
2620
    // Encrypt
2621
    $encrypted = $rsa->encrypt(base64_decode($key));
2622
    if (empty($encrypted)) {  // Check if key is empty or null
2623
        throw new RuntimeException("Error while encrypting key.");
2624
    }
2625
    // Return
2626
    return base64_encode($encrypted);
2627
}
2628
2629
/**
2630
 * Decrypts using RSA an encrypted string using a private key.
2631
 *
2632
 * @param string $key        Encrypted key
2633
 * @param string $privateKey User private key
2634
 *
2635
 * @return string
2636
 */
2637
function decryptUserObjectKey(string $key, string $privateKey): string
2638
{
2639
    // Sanitize
2640
    $antiXss = new AntiXSS();
2641
    $privateKey = $antiXss->xss_clean($privateKey);
2642
2643
    // Load classes
2644
    $rsa = new Crypt_RSA();
2645
    // Load the private key
2646
    $decodedPrivateKey = base64_decode($privateKey, true);
2647
    if ($decodedPrivateKey === false) {
2648
        throw new InvalidArgumentException("Error while decoding private key.");
2649
    }
2650
2651
    $rsa->loadKey($decodedPrivateKey);
2652
2653
    // Decrypt
2654
    try {
2655
        $decodedKey = base64_decode($key, true);
2656
        if ($decodedKey === false) {
2657
            throw new InvalidArgumentException("Error while decoding key.");
2658
        }
2659
2660
        // This check is needed as decrypt() in version 2 can return false in case of error
2661
        $tmpValue = $rsa->decrypt($decodedKey);
2662
        if ($tmpValue !== false) {
0 ignored issues
show
introduced by
The condition $tmpValue !== false is always true.
Loading history...
2663
            return base64_encode($tmpValue);
2664
        } else {
2665
            return '';
2666
        }
2667
    } catch (Exception $e) {
2668
        if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
2669
            error_log('TEAMPASS Error - ldap - '.$e->getMessage());
2670
        }
2671
        return 'Exception: could not decrypt object';
2672
    }
2673
}
2674
2675
/**
2676
 * Encrypts a file.
2677
 *
2678
 * @param string $fileInName File name
2679
 * @param string $fileInPath Path to file
2680
 *
2681
 * @return array
2682
 */
2683
function encryptFile(string $fileInName, string $fileInPath): array
2684
{
2685
    if (defined('FILE_BUFFER_SIZE') === false) {
2686
        define('FILE_BUFFER_SIZE', 128 * 1024);
2687
    }
2688
2689
    // Load classes
2690
    $cipher = new Crypt_AES();
2691
2692
    // Generate an object key
2693
    $objectKey = uniqidReal(32);
2694
    // Set it as password
2695
    $cipher->setPassword($objectKey);
2696
    // Prevent against out of memory
2697
    $cipher->enableContinuousBuffer();
2698
2699
    // Encrypt the file content
2700
    $filePath = filter_var($fileInPath . '/' . $fileInName, FILTER_SANITIZE_URL);
2701
    $fileContent = file_get_contents($filePath);
2702
    $plaintext = $fileContent;
2703
    $ciphertext = $cipher->encrypt($plaintext);
2704
2705
    // Save new file
2706
    // deepcode ignore InsecureHash: is simply used to get a unique name
2707
    $hash = uniqid('', true);
2708
    $fileOut = $fileInPath . '/' . TP_FILE_PREFIX . $hash;
2709
    file_put_contents($fileOut, $ciphertext);
2710
    unlink($fileInPath . '/' . $fileInName);
2711
    return [
2712
        'fileHash' => base64_encode($hash),
2713
        'objectKey' => base64_encode($objectKey),
2714
    ];
2715
}
2716
2717
/**
2718
 * Decrypt a file.
2719
 *
2720
 * @param string $fileName File name
2721
 * @param string $filePath Path to file
2722
 * @param string $key      Key to use
2723
 *
2724
 * @return string|array
2725
 */
2726
function decryptFile(string $fileName, string $filePath, string $key): string|array
2727
{
2728
    if (! defined('FILE_BUFFER_SIZE')) {
2729
        define('FILE_BUFFER_SIZE', 128 * 1024);
2730
    }
2731
    
2732
    // Load classes
2733
    $cipher = new Crypt_AES();
2734
    $antiXSS = new AntiXSS();
2735
    
2736
    // Get file name
2737
    $safeFileName = $antiXSS->xss_clean(base64_decode($fileName));
2738
2739
    // Set the object key
2740
    $cipher->setPassword(base64_decode($key));
2741
    // Prevent against out of memory
2742
    $cipher->enableContinuousBuffer();
2743
    $cipher->disablePadding();
2744
    // Get file content
2745
    $safeFilePath = realpath($filePath . '/' . TP_FILE_PREFIX . $safeFileName);
2746
    if ($safeFilePath !== false && file_exists($safeFilePath)) {
2747
        $ciphertext = file_get_contents(filter_var($safeFilePath, FILTER_SANITIZE_URL));
2748
    } else {
2749
        // Handle the error: file doesn't exist or path is invalid
2750
        return [
2751
            'error' => true,
2752
            'message' => 'This file has not been found.',
2753
        ];
2754
    }
2755
2756
    if (WIP) error_log('DEBUG: File image url -> '.filter_var($safeFilePath, FILTER_SANITIZE_URL));
2757
2758
    // Decrypt file content and return
2759
    return base64_encode($cipher->decrypt($ciphertext));
2760
}
2761
2762
/**
2763
 * Generate a simple password
2764
 *
2765
 * @param int $length Length of string
2766
 * @param bool $symbolsincluded Allow symbols
2767
 *
2768
 * @return string
2769
 */
2770
function generateQuickPassword(int $length = 16, bool $symbolsincluded = true): string
2771
{
2772
    // Generate new user password
2773
    $small_letters = range('a', 'z');
2774
    $big_letters = range('A', 'Z');
2775
    $digits = range(0, 9);
2776
    $symbols = $symbolsincluded === true ?
2777
        ['#', '_', '-', '@', '$', '+', '!'] : [];
2778
    $res = array_merge($small_letters, $big_letters, $digits, $symbols);
2779
    $count = count($res);
2780
    // first variant
2781
2782
    $random_string = '';
2783
    for ($i = 0; $i < $length; ++$i) {
2784
        $random_string .= $res[random_int(0, $count - 1)];
2785
    }
2786
2787
    return $random_string;
2788
}
2789
2790
/**
2791
 * Permit to store the sharekey of an object for users.
2792
 *
2793
 * @param string $object_name             Type for table selection
2794
 * @param int    $post_folder_is_personal Personal
2795
 * @param int    $post_object_id          Object
2796
 * @param string $objectKey               Object key
2797
 * @param array  $SETTINGS                Teampass settings
2798
 * @param int    $user_id                 User ID if needed
2799
 * @param bool   $onlyForUser             If is TRUE, then the sharekey is only for the user
2800
 * @param bool   $deleteAll               If is TRUE, then all existing entries are deleted
2801
 * @param array  $objectKeyArray          Array of objects
2802
 * @param int    $all_users_except_id     All users except this one
2803
 * @param int    $apiUserId               API User ID
2804
 *
2805
 * @return void
2806
 */
2807
function storeUsersShareKey(
2808
    string $object_name,
2809
    int $post_folder_is_personal,
2810
    int $post_object_id,
2811
    string $objectKey,
2812
    bool $onlyForUser = false,
2813
    bool $deleteAll = true,
2814
    array $objectKeyArray = [],
2815
    int $all_users_except_id = -1,
2816
    int $apiUserId = -1
2817
): void {
2818
    
2819
    $session = SessionManager::getSession();
2820
    loadClasses('DB');
2821
2822
    // Delete existing entries for this object
2823
    if ($deleteAll === true) {
2824
        DB::delete(
2825
            prefixTable($object_name),
2826
            'object_id = %i',
2827
            $post_object_id
2828
        );
2829
    }
2830
2831
    // Get the user ID
2832
    $userId = ($apiUserId === -1) ? (int) $session->get('user-id') : $apiUserId;
0 ignored issues
show
Unused Code introduced by
The assignment to $userId is dead and can be removed.
Loading history...
2833
2834
    // Create sharekey for each user
2835
    $user_ids = [OTV_USER_ID, SSH_USER_ID, API_USER_ID];
2836
    if ($all_users_except_id !== -1) {
2837
        array_push($user_ids, (int) $all_users_except_id);
2838
    }
2839
    $users = DB::query(
2840
        'SELECT id, public_key
2841
        FROM ' . prefixTable('users') . '
2842
        WHERE id NOT IN %li
2843
        AND public_key != ""',
2844
        $user_ids
2845
    );
2846
    foreach ($users as $user) {
2847
        // Insert in DB the new object key for this item by user
2848
        if (count($objectKeyArray) === 0) {
2849
            if (WIP === true) {
2850
                error_log('TEAMPASS Debug - storeUsersShareKey case1 - ' . $object_name . ' - ' . $post_object_id . ' - ' . $user['id']);
2851
            }
2852
            
2853
            insertOrUpdateSharekey(
2854
                prefixTable($object_name),
2855
                $post_object_id,
2856
                (int) $user['id'],
2857
                encryptUserObjectKey($objectKey, $user['public_key'])
2858
            );
2859
        } else {
2860
            foreach ($objectKeyArray as $object) {
2861
                if (WIP === true) {
2862
                    error_log('TEAMPASS Debug - storeUsersShareKey case2 - ' . $object_name . ' - ' . $object['objectId'] . ' - ' . $user['id']);
2863
                }
2864
                
2865
                insertOrUpdateSharekey(
2866
                    prefixTable($object_name),
2867
                    (int) $object['objectId'],
2868
                    (int) $user['id'],
2869
                    encryptUserObjectKey($object['objectKey'], $user['public_key'])
2870
                );
2871
            }
2872
        }
2873
    }
2874
}
2875
2876
/**
2877
 * Insert or update sharekey for a user
2878
 * Handles duplicate key errors gracefully
2879
 * 
2880
 * @param string $tableName Table name (with prefix)
2881
 * @param int $objectId Object ID
2882
 * @param int $userId User ID
2883
 * @param string $shareKey Encrypted share key
2884
 * @return bool Success status
2885
 */
2886
function insertOrUpdateSharekey(
2887
    string $tableName,
2888
    int $objectId,
2889
    int $userId,
2890
    string $shareKey
2891
): bool {
2892
    try {
2893
        DB::query(
2894
            'INSERT INTO ' . $tableName . ' 
2895
            (object_id, user_id, share_key) 
2896
            VALUES (%i, %i, %s)
2897
            ON DUPLICATE KEY UPDATE share_key = VALUES(share_key)',
2898
            $objectId,
2899
            $userId,
2900
            $shareKey
2901
        );
2902
        return true;
2903
    } catch (Exception $e) {
2904
        error_log('TEAMPASS Error - insertOrUpdateSharekey: ' . $e->getMessage());
2905
        return false;
2906
    }
2907
}
2908
2909
/**
2910
 * Is this string base64 encoded?
2911
 *
2912
 * @param string $str Encoded string?
2913
 *
2914
 * @return bool
2915
 */
2916
function isBase64(string $str): bool
2917
{
2918
    $str = (string) trim($str);
2919
    if (! isset($str[0])) {
2920
        return false;
2921
    }
2922
2923
    $base64String = (string) base64_decode($str, true);
2924
    if ($base64String && base64_encode($base64String) === $str) {
2925
        return true;
2926
    }
2927
2928
    return false;
2929
}
2930
2931
/**
2932
 * Undocumented function
2933
 *
2934
 * @param string $field Parameter
2935
 *
2936
 * @return array|bool|resource|string
2937
 */
2938
function filterString(string $field)
2939
{
2940
    // Sanitize string
2941
    $field = filter_var(trim($field), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
2942
    if (empty($field) === false) {
2943
        // Load AntiXSS
2944
        $antiXss = new AntiXSS();
2945
        // Return
2946
        return $antiXss->xss_clean($field);
2947
    }
2948
2949
    return false;
2950
}
2951
2952
/**
2953
 * CHeck if provided credentials are allowed on server
2954
 *
2955
 * @param string $login    User Login
2956
 * @param string $password User Pwd
2957
 * @param array  $SETTINGS Teampass settings
2958
 *
2959
 * @return bool
2960
 */
2961
function ldapCheckUserPassword(string $login, string $password, array $SETTINGS): bool
2962
{
2963
    // Build ldap configuration array
2964
    $config = [
2965
        // Mandatory Configuration Options
2966
        'hosts' => [$SETTINGS['ldap_hosts']],
2967
        'base_dn' => $SETTINGS['ldap_bdn'],
2968
        'username' => $SETTINGS['ldap_username'],
2969
        'password' => $SETTINGS['ldap_password'],
2970
2971
        // Optional Configuration Options
2972
        'port' => $SETTINGS['ldap_port'],
2973
        'use_ssl' => (int) $SETTINGS['ldap_ssl'] === 1 ? true : false,
2974
        'use_tls' => (int) $SETTINGS['ldap_tls'] === 1 ? true : false,
2975
        'version' => 3,
2976
        'timeout' => 5,
2977
        'follow_referrals' => false,
2978
2979
        // Custom LDAP Options
2980
        'options' => [
2981
            // See: http://php.net/ldap_set_option
2982
            LDAP_OPT_X_TLS_REQUIRE_CERT => (isset($SETTINGS['ldap_tls_certificate_check']) ? $SETTINGS['ldap_tls_certificate_check'] : LDAP_OPT_X_TLS_HARD),
2983
        ],
2984
    ];
2985
    
2986
    $connection = new Connection($config);
2987
    // Connect to LDAP
2988
    try {
2989
        $connection->connect();
2990
    } catch (\LdapRecord\Auth\BindException $e) {
2991
        $error = $e->getDetailedError();
2992
        if ($error && defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
2993
            error_log('TEAMPASS Error - LDAP - '.$error->getErrorCode()." - ".$error->getErrorMessage(). " - ".$error->getDiagnosticMessage());
2994
        }
2995
        // deepcode ignore ServerLeak: No important data is sent
2996
        echo 'An error occurred.';
2997
        return false;
2998
    }
2999
3000
    // Authenticate user
3001
    try {
3002
        if ($SETTINGS['ldap_type'] === 'ActiveDirectory') {
3003
            $connection->auth()->attempt($login, $password, $stayAuthenticated = true);
3004
        } else {
3005
            $connection->auth()->attempt($SETTINGS['ldap_user_attribute'].'='.$login.','.(isset($SETTINGS['ldap_dn_additional_user_dn']) && !empty($SETTINGS['ldap_dn_additional_user_dn']) ? $SETTINGS['ldap_dn_additional_user_dn'].',' : '').$SETTINGS['ldap_bdn'], $password, $stayAuthenticated = true);
3006
        }
3007
    } catch (\LdapRecord\Auth\BindException $e) {
3008
        $error = $e->getDetailedError();
3009
        if ($error && defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
3010
            error_log('TEAMPASS Error - LDAP - '.$error->getErrorCode()." - ".$error->getErrorMessage(). " - ".$error->getDiagnosticMessage());
3011
        }
3012
        // deepcode ignore ServerLeak: No important data is sent
3013
        echo 'An error occurred.';
3014
        return false;
3015
    }
3016
3017
    return true;
3018
}
3019
3020
/**
3021
 * Removes from DB all sharekeys of this user
3022
 *
3023
 * @param int $userId User's id
3024
 * @param array   $SETTINGS Teampass settings
3025
 *
3026
 * @return bool
3027
 */
3028
function deleteUserObjetsKeys(int $userId, array $SETTINGS = []): bool
3029
{
3030
    // Return if technical accounts
3031
    if ($userId === OTV_USER_ID
3032
        || $userId === SSH_USER_ID
0 ignored issues
show
introduced by
The condition $userId === SSH_USER_ID is always false.
Loading history...
3033
        || $userId === API_USER_ID
0 ignored issues
show
introduced by
The condition $userId === API_USER_ID is always false.
Loading history...
3034
        || $userId === TP_USER_ID
0 ignored issues
show
introduced by
The condition $userId === TP_USER_ID is always false.
Loading history...
3035
    ) {
3036
        return false;
3037
    }
3038
3039
    // Load class DB
3040
    loadClasses('DB');
3041
3042
    // Remove all item sharekeys items
3043
    // expect if personal item
3044
    DB::delete(
3045
        prefixTable('sharekeys_items'),
3046
        'user_id = %i',// AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)'',
3047
        $userId
3048
    );
3049
    // Remove all item sharekeys files
3050
    DB::delete(
3051
        prefixTable('sharekeys_files'),
3052
        'user_id = %i',
3053
        $userId
3054
    );
3055
    // Remove all item sharekeys fields
3056
    DB::delete(
3057
        prefixTable('sharekeys_fields'),
3058
        'user_id = %i',
3059
        $userId
3060
    );
3061
    // Remove all item sharekeys logs
3062
    DB::delete(
3063
        prefixTable('sharekeys_logs'),
3064
        'user_id = %i', // AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
3065
        $userId
3066
    );
3067
    // Remove all item sharekeys suggestions
3068
    DB::delete(
3069
        prefixTable('sharekeys_suggestions'),
3070
        'user_id = %i',// AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
3071
        $userId
3072
    );
3073
    return false;
3074
}
3075
3076
/**
3077
 * Manage list of timezones   $SETTINGS Teampass settings
3078
 *
3079
 * @return array
3080
 */
3081
function timezone_list()
3082
{
3083
    static $timezones = null;
3084
    if ($timezones === null) {
3085
        $timezones = [];
3086
        $offsets = [];
3087
        $now = new DateTime('now', new DateTimeZone('UTC'));
3088
        foreach (DateTimeZone::listIdentifiers() as $timezone) {
3089
            $now->setTimezone(new DateTimeZone($timezone));
3090
            $offsets[] = $offset = $now->getOffset();
3091
            $timezones[$timezone] = '(' . format_GMT_offset($offset) . ') ' . format_timezone_name($timezone);
3092
        }
3093
3094
        array_multisort($offsets, $timezones);
3095
    }
3096
3097
    return $timezones;
3098
}
3099
3100
/**
3101
 * Provide timezone offset
3102
 *
3103
 * @param int $offset Timezone offset
3104
 *
3105
 * @return string
3106
 */
3107
function format_GMT_offset($offset): string
3108
{
3109
    $hours = intval($offset / 3600);
3110
    $minutes = abs(intval($offset % 3600 / 60));
3111
    return 'GMT' . ($offset ? sprintf('%+03d:%02d', $hours, $minutes) : '');
3112
}
3113
3114
/**
3115
 * Provides timezone name
3116
 *
3117
 * @param string $name Timezone name
3118
 *
3119
 * @return string
3120
 */
3121
function format_timezone_name($name): string
3122
{
3123
    $name = str_replace('/', ', ', $name);
3124
    $name = str_replace('_', ' ', $name);
3125
3126
    return str_replace('St ', 'St. ', $name);
3127
}
3128
3129
/**
3130
 * Provides info if user should use MFA based on roles
3131
 *
3132
 * @param string $userRolesIds  User roles ids
3133
 * @param string $mfaRoles      Roles for which MFA is requested
3134
 *
3135
 * @return bool
3136
 */
3137
function mfa_auth_requested_roles(string $userRolesIds, string $mfaRoles): bool
3138
{
3139
    if (empty($mfaRoles) === true) {
3140
        return true;
3141
    }
3142
3143
    $mfaRoles = array_values(json_decode($mfaRoles, true));
3144
    $userRolesIds = array_filter(explode(';', $userRolesIds));
3145
    if (count($mfaRoles) === 0 || count(array_intersect($mfaRoles, $userRolesIds)) > 0) {
3146
        return true;
3147
    }
3148
3149
    return false;
3150
}
3151
3152
/**
3153
 * Permits to clean a string for export purpose
3154
 *
3155
 * @param string $text
3156
 * @param bool $emptyCheckOnly
3157
 * 
3158
 * @return string
3159
 */
3160
function cleanStringForExport(string $text, bool $emptyCheckOnly = false): string
3161
{
3162
    if (is_null($text) === true || empty($text) === true) {
3163
        return '';
3164
    }
3165
    // only expected to check if $text was empty
3166
    elseif ($emptyCheckOnly === true) {
3167
        return $text;
3168
    }
3169
3170
    return strip_tags(
3171
        cleanString(
3172
            html_entity_decode($text, ENT_QUOTES | ENT_XHTML, 'UTF-8'),
3173
            true)
3174
        );
3175
}
3176
3177
/**
3178
 * Permits to check if user ID is valid
3179
 *
3180
 * @param integer $post_user_id
3181
 * @return bool
3182
 */
3183
function isUserIdValid($userId): bool
3184
{
3185
    if (is_null($userId) === false
3186
        && empty($userId) === false
3187
    ) {
3188
        return true;
3189
    }
3190
    return false;
3191
}
3192
3193
/**
3194
 * Check if a key exists and if its value equal the one expected
3195
 *
3196
 * @param string $key
3197
 * @param integer|string $value
3198
 * @param array $array
3199
 * 
3200
 * @return boolean
3201
 */
3202
function isKeyExistingAndEqual(
3203
    string $key,
3204
    /*PHP8 - integer|string*/$value,
3205
    array $array
3206
): bool
3207
{
3208
    if (isset($array[$key]) === true
3209
        && (is_int($value) === true ?
3210
            (int) $array[$key] === $value :
3211
            (string) $array[$key] === $value)
3212
    ) {
3213
        return true;
3214
    }
3215
    return false;
3216
}
3217
3218
/**
3219
 * Check if a variable is not set or equal to a value
3220
 *
3221
 * @param string|null $var
3222
 * @param integer|string $value
3223
 * 
3224
 * @return boolean
3225
 */
3226
function isKeyNotSetOrEqual(
3227
    /*PHP8 - string|null*/$var,
3228
    /*PHP8 - integer|string*/$value
3229
): bool
3230
{
3231
    if (isset($var) === false
3232
        || (is_int($value) === true ?
3233
            (int) $var === $value :
3234
            (string) $var === $value)
3235
    ) {
3236
        return true;
3237
    }
3238
    return false;
3239
}
3240
3241
/**
3242
 * Check if a key exists and if its value < to the one expected
3243
 *
3244
 * @param string $key
3245
 * @param integer $value
3246
 * @param array $array
3247
 * 
3248
 * @return boolean
3249
 */
3250
function isKeyExistingAndInferior(string $key, int $value, array $array): bool
3251
{
3252
    if (isset($array[$key]) === true && (int) $array[$key] < $value) {
3253
        return true;
3254
    }
3255
    return false;
3256
}
3257
3258
/**
3259
 * Check if a key exists and if its value > to the one expected
3260
 *
3261
 * @param string $key
3262
 * @param integer $value
3263
 * @param array $array
3264
 * 
3265
 * @return boolean
3266
 */
3267
function isKeyExistingAndSuperior(string $key, int $value, array $array): bool
3268
{
3269
    if (isset($array[$key]) === true && (int) $array[$key] > $value) {
3270
        return true;
3271
    }
3272
    return false;
3273
}
3274
3275
/**
3276
 * Check if values in array are set
3277
 * Return true if all set
3278
 * Return false if one of them is not set
3279
 *
3280
 * @param array $arrayOfValues
3281
 * @return boolean
3282
 */
3283
function isSetArrayOfValues(array $arrayOfValues): bool
3284
{
3285
    foreach($arrayOfValues as $value) {
3286
        if (isset($value) === false) {
3287
            return false;
3288
        }
3289
    }
3290
    return true;
3291
}
3292
3293
/**
3294
 * Check if values in array are set
3295
 * Return true if all set
3296
 * Return false if one of them is not set
3297
 *
3298
 * @param array $arrayOfValues
3299
 * @param integer|string $value
3300
 * @return boolean
3301
 */
3302
function isArrayOfVarsEqualToValue(
3303
    array $arrayOfVars,
3304
    /*PHP8 - integer|string*/$value
3305
) : bool
3306
{
3307
    foreach($arrayOfVars as $variable) {
3308
        if ($variable !== $value) {
3309
            return false;
3310
        }
3311
    }
3312
    return true;
3313
}
3314
3315
/**
3316
 * Checks if at least one variable in array is equal to value
3317
 *
3318
 * @param array $arrayOfValues
3319
 * @param integer|string $value
3320
 * @return boolean
3321
 */
3322
function isOneVarOfArrayEqualToValue(
3323
    array $arrayOfVars,
3324
    /*PHP8 - integer|string*/$value
3325
) : bool
3326
{
3327
    foreach($arrayOfVars as $variable) {
3328
        if ($variable === $value) {
3329
            return true;
3330
        }
3331
    }
3332
    return false;
3333
}
3334
3335
/**
3336
 * Checks is value is null, not set OR empty
3337
 *
3338
 * @param string|int|null $value
3339
 * @return boolean
3340
 */
3341
function isValueSetNullEmpty(string|int|null $value) : bool
3342
{
3343
    if (is_null($value) === true || empty($value) === true) {
3344
        return true;
3345
    }
3346
    return false;
3347
}
3348
3349
/**
3350
 * Checks if value is set and if empty is equal to passed boolean
3351
 *
3352
 * @param string|int $value
3353
 * @param boolean $boolean
3354
 * @return boolean
3355
 */
3356
function isValueSetEmpty($value, $boolean = true) : bool
3357
{
3358
    if (empty($value) === $boolean) {
3359
        return true;
3360
    }
3361
    return false;
3362
}
3363
3364
/**
3365
 * Ensure Complexity is translated
3366
 *
3367
 * @return void
3368
 */
3369
function defineComplexity() : void
3370
{
3371
    // Load user's language
3372
    $session = SessionManager::getSession();
3373
    $lang = new Language($session->get('user-language') ?? 'english');
3374
    
3375
    if (defined('TP_PW_COMPLEXITY') === false) {
3376
        define(
3377
            'TP_PW_COMPLEXITY',
3378
            [
3379
                TP_PW_STRENGTH_1 => array(TP_PW_STRENGTH_1, $lang->get('complex_level1'), 'fas fa-thermometer-empty text-danger'),
3380
                TP_PW_STRENGTH_2 => array(TP_PW_STRENGTH_2, $lang->get('complex_level2'), 'fas fa-thermometer-quarter text-warning'),
3381
                TP_PW_STRENGTH_3 => array(TP_PW_STRENGTH_3, $lang->get('complex_level3'), 'fas fa-thermometer-half text-warning'),
3382
                TP_PW_STRENGTH_4 => array(TP_PW_STRENGTH_4, $lang->get('complex_level4'), 'fas fa-thermometer-three-quarters text-success'),
3383
                TP_PW_STRENGTH_5 => array(TP_PW_STRENGTH_5, $lang->get('complex_level5'), 'fas fa-thermometer-full text-success'),
3384
            ]
3385
        );
3386
    }
3387
}
3388
3389
/**
3390
 * Uses Sanitizer to perform data sanitization
3391
 *
3392
 * @param array     $data
3393
 * @param array     $filters
3394
 * @return array|string
3395
 */
3396
function dataSanitizer(array $data, array $filters): array|string
3397
{
3398
    // Load Sanitizer library
3399
    $sanitizer = new Sanitizer($data, $filters);
3400
3401
    // Load AntiXSS
3402
    $antiXss = new AntiXSS();
3403
3404
    // Sanitize post and get variables
3405
    return $antiXss->xss_clean($sanitizer->sanitize());
3406
}
3407
3408
/**
3409
 * Permits to manage the cache tree for a user
3410
 *
3411
 * @param integer $user_id
3412
 * @param string $data
3413
 * @param array $SETTINGS
3414
 * @param string $field_update
3415
 * @return void
3416
 */
3417
function cacheTreeUserHandler(int $user_id, string $data, array $SETTINGS, string $field_update = '')
3418
{
3419
    // Load class DB
3420
    loadClasses('DB');
3421
3422
    // Exists ?
3423
    $userCacheId = DB::queryFirstRow(
3424
        'SELECT increment_id
3425
        FROM ' . prefixTable('cache_tree') . '
3426
        WHERE user_id = %i',
3427
        $user_id
3428
    );
3429
    
3430
    if (is_null($userCacheId) === true || count($userCacheId) === 0) {
3431
        // insert in table
3432
        DB::insert(
3433
            prefixTable('cache_tree'),
3434
            array(
3435
                'data' => $data,
3436
                'timestamp' => time(),
3437
                'user_id' => $user_id,
3438
                'visible_folders' => '',
3439
            )
3440
        );
3441
    } else {
3442
        if (empty($field_update) === true) {
3443
            DB::update(
3444
                prefixTable('cache_tree'),
3445
                [
3446
                    'timestamp' => time(),
3447
                    'data' => $data,
3448
                ],
3449
                'increment_id = %i',
3450
                $userCacheId['increment_id']
3451
            );
3452
        /* USELESS
3453
        } else {
3454
            DB::update(
3455
                prefixTable('cache_tree'),
3456
                [
3457
                    $field_update => $data,
3458
                ],
3459
                'increment_id = %i',
3460
                $userCacheId['increment_id']
3461
            );*/
3462
        }
3463
    }
3464
}
3465
3466
/**
3467
 * Permits to calculate a %
3468
 *
3469
 * @param float $nombre
3470
 * @param float $total
3471
 * @param float $pourcentage
3472
 * @return float
3473
 */
3474
function pourcentage(float $nombre, float $total, float $pourcentage): float
3475
{ 
3476
    $resultat = ($nombre/$total) * $pourcentage;
3477
    return round($resultat);
3478
}
3479
3480
/**
3481
 * Load the folders list from the cache
3482
 *
3483
 * @param string $fieldName
3484
 * @param string $sessionName
3485
 * @param boolean $forceRefresh
3486
 * @return array
3487
 */
3488
function loadFoldersListByCache(
3489
    string $fieldName,
3490
    string $sessionName,
3491
    bool $forceRefresh = false
3492
): array
3493
{
3494
    // Case when refresh is EXPECTED / MANDATORY
3495
    if ($forceRefresh === true) {
3496
        return [
3497
            'state' => false,
3498
            'data' => [],
3499
        ];
3500
    }
3501
    
3502
    $session = SessionManager::getSession();
3503
3504
    // Get last folder update
3505
    $lastFolderChange = DB::queryFirstRow(
3506
        'SELECT valeur FROM ' . prefixTable('misc') . '
3507
        WHERE type = %s AND intitule = %s',
3508
        'timestamp',
3509
        'last_folder_change'
3510
    );
3511
    if (DB::count() === 0) {
3512
        $lastFolderChange['valeur'] = 0;
3513
    }
3514
3515
    // Case when an update in the tree has been done
3516
    // Refresh is then mandatory
3517
    if ((int) $lastFolderChange['valeur'] > (int) (null !== $session->get('user-tree_last_refresh_timestamp') ? $session->get('user-tree_last_refresh_timestamp') : 0)) {
3518
        return [
3519
            'state' => false,
3520
            'data' => [],
3521
        ];
3522
    }
3523
    
3524
    // Does this user has a tree cache
3525
    $userCacheTree = DB::queryFirstRow(
3526
        'SELECT '.$fieldName.'
3527
        FROM ' . prefixTable('cache_tree') . '
3528
        WHERE user_id = %i',
3529
        $session->get('user-id')
3530
    );
3531
    if (empty($userCacheTree[$fieldName]) === false && $userCacheTree[$fieldName] !== '[]') {
3532
        return [
3533
            'state' => true,
3534
            'data' => $userCacheTree[$fieldName],
3535
            'extra' => '',
3536
        ];
3537
    }
3538
3539
    return [
3540
        'state' => false,
3541
        'data' => [],
3542
    ];
3543
}
3544
3545
3546
/**
3547
 * Permits to refresh the categories of folders
3548
 *
3549
 * @param array $folderIds
3550
 * @return void
3551
 */
3552
function handleFoldersCategories(
3553
    array $folderIds
3554
)
3555
{
3556
    // Load class DB
3557
    loadClasses('DB');
3558
3559
    $arr_data = array();
3560
3561
    // force full list of folders
3562
    if (count($folderIds) === 0) {
3563
        $folderIds = DB::queryFirstColumn(
3564
            'SELECT id
3565
            FROM ' . prefixTable('nested_tree') . '
3566
            WHERE personal_folder=%i',
3567
            0
3568
        );
3569
    }
3570
3571
    // Get complexity
3572
    defineComplexity();
3573
3574
    // update
3575
    foreach ($folderIds as $folder) {
3576
        // Do we have Categories
3577
        // get list of associated Categories
3578
        $arrCatList = array();
3579
        $rows_tmp = DB::query(
3580
            'SELECT c.id, c.title, c.level, c.type, c.masked, c.order, c.encrypted_data, c.role_visibility, c.is_mandatory,
3581
            f.id_category AS category_id
3582
            FROM ' . prefixTable('categories_folders') . ' AS f
3583
            INNER JOIN ' . prefixTable('categories') . ' AS c ON (f.id_category = c.parent_id)
3584
            WHERE id_folder=%i',
3585
            $folder
3586
        );
3587
        if (DB::count() > 0) {
3588
            foreach ($rows_tmp as $row) {
3589
                $arrCatList[$row['id']] = array(
3590
                    'id' => $row['id'],
3591
                    'title' => $row['title'],
3592
                    'level' => $row['level'],
3593
                    'type' => $row['type'],
3594
                    'masked' => $row['masked'],
3595
                    'order' => $row['order'],
3596
                    'encrypted_data' => $row['encrypted_data'],
3597
                    'role_visibility' => $row['role_visibility'],
3598
                    'is_mandatory' => $row['is_mandatory'],
3599
                    'category_id' => $row['category_id'],
3600
                );
3601
            }
3602
        }
3603
        $arr_data['categories'] = $arrCatList;
3604
3605
        // Now get complexity
3606
        $valTemp = '';
3607
        $data = DB::queryFirstRow(
3608
            'SELECT valeur
3609
            FROM ' . prefixTable('misc') . '
3610
            WHERE type = %s AND intitule=%i',
3611
            'complex',
3612
            $folder
3613
        );
3614
        if (DB::count() > 0 && empty($data['valeur']) === false) {
3615
            $valTemp = array(
3616
                'value' => $data['valeur'],
3617
                'text' => TP_PW_COMPLEXITY[$data['valeur']][1],
3618
            );
3619
        }
3620
        $arr_data['complexity'] = $valTemp;
3621
3622
        // Now get Roles
3623
        $valTemp = '';
3624
        $rows_tmp = DB::query(
3625
            'SELECT t.title
3626
            FROM ' . prefixTable('roles_values') . ' as v
3627
            INNER JOIN ' . prefixTable('roles_title') . ' as t ON (v.role_id = t.id)
3628
            WHERE v.folder_id = %i
3629
            GROUP BY title',
3630
            $folder
3631
        );
3632
        foreach ($rows_tmp as $record) {
3633
            $valTemp .= (empty($valTemp) === true ? '' : ' - ') . $record['title'];
3634
        }
3635
        $arr_data['visibilityRoles'] = $valTemp;
3636
3637
        // now save in DB
3638
        DB::update(
3639
            prefixTable('nested_tree'),
3640
            array(
3641
                'categories' => json_encode($arr_data),
3642
            ),
3643
            'id = %i',
3644
            $folder
3645
        );
3646
    }
3647
}
3648
3649
/**
3650
 * List all users that have specific roles
3651
 *
3652
 * @param array $roles
3653
 * @return array
3654
 */
3655
function getUsersWithRoles(
3656
    array $roles
3657
): array
3658
{
3659
    $session = SessionManager::getSession();
3660
    $arrUsers = array();
3661
3662
    foreach ($roles as $role) {
3663
        // loop on users and check if user has this role
3664
        $rows = DB::query(
3665
            'SELECT id, fonction_id
3666
            FROM ' . prefixTable('users') . '
3667
            WHERE id != %i AND admin = 0 AND fonction_id IS NOT NULL AND fonction_id != ""',
3668
            $session->get('user-id')
3669
        );
3670
        foreach ($rows as $user) {
3671
            $userRoles = is_null($user['fonction_id']) === false && empty($user['fonction_id']) === false ? explode(';', $user['fonction_id']) : [];
3672
            if (in_array($role, $userRoles, true) === true) {
3673
                array_push($arrUsers, $user['id']);
3674
            }
3675
        }
3676
    }
3677
3678
    return $arrUsers;
3679
}
3680
3681
3682
/**
3683
 * Get all users informations
3684
 *
3685
 * @param integer $userId
3686
 * @return array
3687
 */
3688
function getFullUserInfos(
3689
    int $userId
3690
): array
3691
{
3692
    if (empty($userId) === true) {
3693
        return array();
3694
    }
3695
3696
    $val = DB::queryFirstRow(
3697
        'SELECT *
3698
        FROM ' . prefixTable('users') . '
3699
        WHERE id = %i',
3700
        $userId
3701
    );
3702
3703
    return $val;
3704
}
3705
3706
/**
3707
 * Is required an upgrade
3708
 *
3709
 * @return boolean
3710
 */
3711
function upgradeRequired(): bool
3712
{
3713
    // Get settings.php
3714
    include_once __DIR__. '/../includes/config/settings.php';
3715
3716
    // Get timestamp in DB
3717
    $val = DB::queryFirstRow(
3718
        'SELECT valeur
3719
        FROM ' . prefixTable('misc') . '
3720
        WHERE type = %s AND intitule = %s',
3721
        'admin',
3722
        'upgrade_timestamp'
3723
    );
3724
3725
    // Check if upgrade is required
3726
    return (
3727
        is_null($val) || count($val) === 0 || !defined('UPGRADE_MIN_DATE') || 
3728
        empty($val['valeur']) || (int) $val['valeur'] < (int) UPGRADE_MIN_DATE
3729
    );
3730
}
3731
3732
/**
3733
 * Permits to change the user keys on his demand
3734
 *
3735
 * @param integer $userId
3736
 * @param string $passwordClear
3737
 * @param integer $nbItemsToTreat
3738
 * @param string $encryptionKey
3739
 * @param boolean $deleteExistingKeys
3740
 * @param boolean $sendEmailToUser
3741
 * @param boolean $encryptWithUserPassword
3742
 * @param boolean $generate_user_new_password
3743
 * @param string $emailBody
3744
 * @param boolean $user_self_change
3745
 * @param string $recovery_public_key
3746
 * @param string $recovery_private_key
3747
 * @param bool $userHasToEncryptPersonalItemsAfter
3748
 * @return string
3749
 */
3750
function handleUserKeys(
3751
    int $userId,
3752
    string $passwordClear,
3753
    int $nbItemsToTreat,
3754
    string $encryptionKey = '',
3755
    bool $deleteExistingKeys = false,
3756
    bool $sendEmailToUser = true,
3757
    bool $encryptWithUserPassword = false,
3758
    bool $generate_user_new_password = false,
3759
    string $emailBody = '',
3760
    bool $user_self_change = false,
3761
    string $recovery_public_key = '',
3762
    string $recovery_private_key = '',
3763
    bool $userHasToEncryptPersonalItemsAfter = false
3764
): string
3765
{
3766
    $session = SessionManager::getSession();
3767
    $lang = new Language($session->get('user-language') ?? 'english');
3768
3769
    // prepapre background tasks for item keys generation        
3770
    $userTP = DB::queryFirstRow(
3771
        'SELECT pw, public_key, private_key
3772
        FROM ' . prefixTable('users') . '
3773
        WHERE id = %i',
3774
        TP_USER_ID
3775
    );
3776
    if (DB::count() === 0) {
3777
        return prepareExchangedData(
3778
            array(
3779
                'error' => true,
3780
                'message' => 'User not exists',
3781
            ),
3782
            'encode'
3783
        );
3784
    }
3785
3786
    // Do we need to generate new user password
3787
    if ($generate_user_new_password === true) {
3788
        // Generate a new password
3789
        $passwordClear = GenerateCryptKey(20, false, true, true, false, true);
3790
    }
3791
3792
    // Create password hash
3793
    $passwordManager = new PasswordManager();
3794
    $hashedPassword = $passwordManager->hashPassword($passwordClear);
3795
    if ($passwordManager->verifyPassword($hashedPassword, $passwordClear) === false) {
3796
        return prepareExchangedData(
3797
            array(
3798
                'error' => true,
3799
                'message' => $lang->get('pw_hash_not_correct'),
3800
            ),
3801
            'encode'
3802
        );
3803
    }
3804
3805
    // Check if valid public/private keys
3806
    if ($recovery_public_key !== '' && $recovery_private_key !== '') {
3807
        try {
3808
            // Generate random string
3809
            $random_str = generateQuickPassword(12, false);
3810
            // Encrypt random string with user publick key
3811
            $encrypted = encryptUserObjectKey($random_str, $recovery_public_key);
3812
            // Decrypt $encrypted with private key
3813
            $decrypted = decryptUserObjectKey($encrypted, $recovery_private_key);
3814
            // Check if decryptUserObjectKey returns our random string
3815
            if ($decrypted !== $random_str) {
3816
                throw new Exception('Public/Private keypair invalid.');
3817
            }
3818
        } catch (Exception $e) {
3819
            // Show error message to user and log event
3820
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
3821
                error_log('ERROR: User '.$userId.' - '.$e->getMessage());
3822
            }
3823
            return prepareExchangedData([
3824
                    'error' => true,
3825
                    'message' => $lang->get('pw_encryption_error'),
3826
                ],
3827
                'encode'
3828
            );
3829
        }
3830
    }
3831
3832
    // Generate new keys
3833
    if ($user_self_change === true && empty($recovery_public_key) === false && empty($recovery_private_key) === false){
3834
        $userKeys = [
3835
            'public_key' => $recovery_public_key,
3836
            'private_key_clear' => $recovery_private_key,
3837
            'private_key' => encryptPrivateKey($passwordClear, $recovery_private_key),
3838
        ];
3839
    } else {
3840
        $userKeys = generateUserKeys($passwordClear);
3841
    }
3842
    
3843
    // Handle private key
3844
    insertPrivateKeyWithCurrentFlag(
3845
        $userId,
3846
        $userKeys['private_key'],
3847
    );
3848
3849
    // Save in DB
3850
    // TODO: remove private key field from Users table
3851
    DB::update(
3852
        prefixTable('users'),
3853
        array(
3854
            'pw' => $hashedPassword,
3855
            'public_key' => $userKeys['public_key'],
3856
            'private_key' => $userKeys['private_key'],
3857
            'keys_recovery_time' => NULL,
3858
        ),
3859
        'id=%i',
3860
        $userId
3861
    );
3862
3863
3864
    // update session too
3865
    if ($userId === $session->get('user-id')) {
3866
        $session->set('user-private_key', $userKeys['private_key_clear']);
3867
        $session->set('user-public_key', $userKeys['public_key']);
3868
        // Notify user that he must re download his keys:
3869
        $session->set('user-keys_recovery_time', NULL);
3870
    }
3871
3872
    // Manage empty encryption key
3873
    // Let's take the user's password if asked and if no encryption key provided
3874
    $encryptionKey = $encryptWithUserPassword === true && empty($encryptionKey) === true ? $passwordClear : $encryptionKey;
3875
3876
    // Create process
3877
    DB::insert(
3878
        prefixTable('background_tasks'),
3879
        array(
3880
            'created_at' => time(),
3881
            'process_type' => 'create_user_keys',
3882
            'arguments' => json_encode([
3883
                'new_user_id' => (int) $userId,
3884
                'new_user_pwd' => cryption($passwordClear, '','encrypt')['string'],
3885
                'new_user_code' => cryption(empty($encryptionKey) === true ? uniqidReal(20) : $encryptionKey, '','encrypt')['string'],
3886
                'owner_id' => (int) TP_USER_ID,
3887
                'creator_pwd' => $userTP['pw'],
3888
                'send_email' => $sendEmailToUser === true ? 1 : 0,
3889
                'otp_provided_new_value' => 1,
3890
                'email_body' => empty($emailBody) === true ? '' : $lang->get($emailBody),
3891
                'user_self_change' => $user_self_change === true ? 1 : 0,
3892
                'userHasToEncryptPersonalItemsAfter' => $userHasToEncryptPersonalItemsAfter === true ? 1 : 0,
3893
            ]),
3894
        )
3895
    );
3896
    $processId = DB::insertId();
3897
3898
    // Delete existing keys
3899
    if ($deleteExistingKeys === true) {
3900
        deleteUserObjetsKeys(
3901
            (int) $userId,
3902
        );
3903
    }
3904
3905
    // Create tasks
3906
    createUserTasks($processId, $nbItemsToTreat);
3907
3908
    // update user's new status
3909
    DB::update(
3910
        prefixTable('users'),
3911
        [
3912
            'is_ready_for_usage' => 0,
3913
            'otp_provided' => 1,
3914
            'ongoing_process_id' => $processId,
3915
            'special' => 'generate-keys',
3916
        ],
3917
        'id=%i',
3918
        $userId
3919
    );
3920
3921
    return prepareExchangedData(
3922
        array(
3923
            'error' => false,
3924
            'message' => '',
3925
            'user_password' => $generate_user_new_password === true ? $passwordClear : '',
3926
        ),
3927
        'encode'
3928
    );
3929
}
3930
3931
/**
3932
 * Permits to generate a new password for a user
3933
 *
3934
 * @param integer $processId
3935
 * @param integer $nbItemsToTreat
3936
 * @return void
3937
 
3938
 */
3939
function createUserTasks($processId, $nbItemsToTreat): void
3940
{
3941
    // Create subtask for step 0
3942
    DB::insert(
3943
        prefixTable('background_subtasks'),
3944
        array(
3945
            'task_id' => $processId,
3946
            'created_at' => time(),
3947
            'task' => json_encode([
3948
                'step' => 'step0',
3949
                'index' => 0,
3950
                'nb' => $nbItemsToTreat,
3951
            ]),
3952
        )
3953
    );
3954
3955
    // Prepare the subtask queries
3956
    $queries = [
3957
        'step20' => 'SELECT * FROM ' . prefixTable('items'),
3958
3959
        'step30' => 'SELECT * FROM ' . prefixTable('log_items') . 
3960
                    ' WHERE raison LIKE "at_pw :%" AND encryption_type = "teampass_aes"',
3961
3962
        'step40' => 'SELECT * FROM ' . prefixTable('categories_items') . 
3963
                    ' WHERE encryption_type = "teampass_aes"',
3964
3965
        'step50' => 'SELECT * FROM ' . prefixTable('suggestion'),
3966
3967
        'step60' => 'SELECT * FROM ' . prefixTable('files') . ' AS f
3968
                        INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
3969
                        WHERE f.status = "' . TP_ENCRYPTION_NAME . '"'
3970
    ];
3971
3972
    // Perform loop on $queries to create sub-tasks
3973
    foreach ($queries as $step => $query) {
3974
        DB::query($query);
3975
        createAllSubTasks($step, DB::count(), $nbItemsToTreat, $processId);
3976
    }
3977
3978
    // Create subtask for step 99
3979
    DB::insert(
3980
        prefixTable('background_subtasks'),
3981
        array(
3982
            'task_id' => $processId,
3983
            'created_at' => time(),
3984
            'task' => json_encode([
3985
                'step' => 'step99',
3986
            ]),
3987
        )
3988
    );
3989
}
3990
3991
/**
3992
 * Create all subtasks for a given action
3993
 * @param string $action The action to be performed
3994
 * @param int $totalElements Total number of elements to process
3995
 * @param int $elementsPerIteration Number of elements per iteration
3996
 * @param int $taskId The ID of the task
3997
 */
3998
function createAllSubTasks($action, $totalElements, $elementsPerIteration, $taskId) {
3999
    // Calculate the number of iterations
4000
    $iterations = ceil($totalElements / $elementsPerIteration);
4001
4002
    // Create the subtasks
4003
    for ($i = 0; $i < $iterations; $i++) {
4004
        DB::insert(prefixTable('background_subtasks'), [
4005
            'task_id' => $taskId,
4006
            'created_at' => time(),
4007
            'task' => json_encode([
4008
                "step" => $action,
4009
                "index" => $i * $elementsPerIteration,
4010
                "nb" => $elementsPerIteration,
4011
            ]),
4012
        ]);
4013
    }
4014
}
4015
4016
/**
4017
 * Permeits to check the consistency of date versus columns definition
4018
 *
4019
 * @param string $table
4020
 * @param array $dataFields
4021
 * @return array
4022
 */
4023
function validateDataFields(
4024
    string $table,
4025
    array $dataFields
4026
): array
4027
{
4028
    // Get table structure
4029
    $result = DB::query(
4030
        "SELECT `COLUMN_NAME`, `CHARACTER_MAXIMUM_LENGTH` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%l' AND TABLE_NAME = '%l';",
4031
        DB_NAME,
4032
        $table
4033
    );
4034
4035
    foreach ($result as $row) {
4036
        $field = $row['COLUMN_NAME'];
4037
        $maxLength = is_null($row['CHARACTER_MAXIMUM_LENGTH']) === false ? (int) $row['CHARACTER_MAXIMUM_LENGTH'] : '';
4038
4039
        if (isset($dataFields[$field]) === true && is_array($dataFields[$field]) === false && empty($maxLength) === false) {
4040
            if (strlen((string) $dataFields[$field]) > $maxLength) {
4041
                return [
4042
                    'state' => false,
4043
                    'field' => $field,
4044
                    'maxLength' => $maxLength,
4045
                    'currentLength' => strlen((string) $dataFields[$field]),
4046
                ];
4047
            }
4048
        }
4049
    }
4050
    
4051
    return [
4052
        'state' => true,
4053
        'message' => '',
4054
    ];
4055
}
4056
4057
/**
4058
 * Adapt special characters sanitized during filter_var with option FILTER_SANITIZE_SPECIAL_CHARS operation
4059
 *
4060
 * @param string $string
4061
 * @return string
4062
 */
4063
function filterVarBack(string $string): string
4064
{
4065
    $arr = [
4066
        '&#060;' => '<',
4067
        '&#062;' => '>',
4068
        '&#034;' => '"',
4069
        '&#039;' => "'",
4070
        '&#038;' => '&',
4071
    ];
4072
4073
    foreach ($arr as $key => $value) {
4074
        $string = str_replace($key, $value, $string);
4075
    }
4076
4077
    return $string;
4078
}
4079
4080
/**
4081
 * 
4082
 */
4083
function storeTask(
4084
    string $taskName,
4085
    int $user_id,
4086
    int $is_personal_folder,
4087
    int $folder_destination_id,
4088
    int $item_id,
4089
    string $object_keys,
4090
    array $fields_keys = [],
4091
    array $files_keys = []
4092
)
4093
{
4094
    if (in_array($taskName, ['item_copy', 'new_item', 'update_item'])) {
4095
        // Create process
4096
        DB::insert(
4097
            prefixTable('background_tasks'),
4098
            array(
4099
                'created_at' => time(),
4100
                'process_type' => $taskName,
4101
                'arguments' => json_encode([
4102
                    'item_id' => $item_id,
4103
                    'object_key' => $object_keys,
4104
                ]),
4105
                'item_id' => $item_id,
4106
            )
4107
        );
4108
        $processId = DB::insertId();
4109
4110
        // Create tasks
4111
        // 1- Create password sharekeys for users of this new ITEM
4112
        DB::insert(
4113
            prefixTable('background_subtasks'),
4114
            array(
4115
                'task_id' => $processId,
4116
                'created_at' => time(),
4117
                'task' => json_encode([
4118
                    'step' => 'create_users_pwd_key',
4119
                    'index' => 0,
4120
                ]),
4121
            )
4122
        );
4123
4124
        // 2- Create fields sharekeys for users of this new ITEM
4125
        DB::insert(
4126
            prefixTable('background_subtasks'),
4127
            array(
4128
                'task_id' => $processId,
4129
                'created_at' => time(),
4130
                'task' => json_encode([
4131
                    'step' => 'create_users_fields_key',
4132
                    'index' => 0,
4133
                    'fields_keys' => $fields_keys,
4134
                ]),
4135
            )
4136
        );
4137
4138
        // 3- Create files sharekeys for users of this new ITEM
4139
        DB::insert(
4140
            prefixTable('background_subtasks'),
4141
            array(
4142
                'task_id' => $processId,
4143
                'created_at' => time(),
4144
                'task' => json_encode([
4145
                    'step' => 'create_users_files_key',
4146
                    'index' => 0,
4147
                    'files_keys' => $files_keys,
4148
                ]),
4149
            )
4150
        );
4151
    }
4152
}
4153
4154
/**
4155
 * 
4156
 */
4157
function createTaskForItem(
4158
    string $processType,
4159
    string|array $taskName,
4160
    int $itemId,
4161
    int $userId,
4162
    string $objectKey,
4163
    int $parentId = -1,
4164
    array $fields_keys = [],
4165
    array $files_keys = []
4166
)
4167
{
4168
    // 1- Create main process
4169
    // ---
4170
    
4171
    // Create process
4172
    DB::insert(
4173
        prefixTable('background_tasks'),
4174
        array(
4175
            'created_at' => time(),
4176
            'process_type' => $processType,
4177
            'arguments' => json_encode([
4178
                'all_users_except_id' => (int) $userId,
4179
                'item_id' => (int) $itemId,
4180
                'object_key' => $objectKey,
4181
                'author' => (int) $userId,
4182
            ]),
4183
            'item_id' => (int) $parentId !== -1 ?  $parentId : null,
4184
        )
4185
    );
4186
    $processId = DB::insertId();
4187
4188
    // 2- Create expected tasks
4189
    // ---
4190
    if (is_array($taskName) === false) {
0 ignored issues
show
introduced by
The condition is_array($taskName) === false is always false.
Loading history...
4191
        $taskName = [$taskName];
4192
    }
4193
    foreach($taskName as $task) {
4194
        if (WIP === true) error_log('createTaskForItem - task: '.$task);
4195
        switch ($task) {
4196
            case 'item_password':
4197
                
4198
                DB::insert(
4199
                    prefixTable('background_subtasks'),
4200
                    array(
4201
                        'task_id' => $processId,
4202
                        'created_at' => time(),
4203
                        'task' => json_encode([
4204
                            'step' => 'create_users_pwd_key',
4205
                            'index' => 0,
4206
                        ]),
4207
                    )
4208
                );
4209
4210
                break;
4211
            case 'item_field':
4212
                
4213
                DB::insert(
4214
                    prefixTable('background_subtasks'),
4215
                    array(
4216
                        'task_id' => $processId,
4217
                        'created_at' => time(),
4218
                        'task' => json_encode([
4219
                            'step' => 'create_users_fields_key',
4220
                            'index' => 0,
4221
                            'fields_keys' => $fields_keys,
4222
                        ]),
4223
                    )
4224
                );
4225
4226
                break;
4227
            case 'item_file':
4228
4229
                DB::insert(
4230
                    prefixTable('background_subtasks'),
4231
                    array(
4232
                        'task_id' => $processId,
4233
                        'created_at' => time(),
4234
                        'task' => json_encode([
4235
                            'step' => 'create_users_files_key',
4236
                            'index' => 0,
4237
                            'fields_keys' => $files_keys,
4238
                        ]),
4239
                    )
4240
                );
4241
                break;
4242
            default:
4243
                # code...
4244
                break;
4245
        }
4246
    }
4247
}
4248
4249
4250
function deleteProcessAndRelatedTasks(int $processId)
4251
{
4252
    // Delete process
4253
    DB::delete(
4254
        prefixTable('background_tasks'),
4255
        'id=%i',
4256
        $processId
4257
    );
4258
4259
    // Delete tasks
4260
    DB::delete(
4261
        prefixTable('background_subtasks'),
4262
        'task_id=%i',
4263
        $processId
4264
    );
4265
4266
}
4267
4268
/**
4269
 * Return PHP binary path
4270
 *
4271
 * @return string
4272
 */
4273
function getPHPBinary(): string
4274
{
4275
    // Get PHP binary path
4276
    $phpBinaryFinder = new PhpExecutableFinder();
4277
    $phpBinaryPath = $phpBinaryFinder->find();
4278
    return $phpBinaryPath === false ? 'false' : $phpBinaryPath;
4279
}
4280
4281
4282
4283
/**
4284
 * Delete unnecessary keys for personal items
4285
 *
4286
 * @param boolean $allUsers
4287
 * @param integer $user_id
4288
 * @return void
4289
 */
4290
function purgeUnnecessaryKeys(bool $allUsers = true, int $user_id=0)
4291
{
4292
    if ($allUsers === true) {
4293
        // Load class DB
4294
        if (class_exists('DB') === false) {
4295
            loadClasses('DB');
4296
        }
4297
4298
        $users = DB::query(
4299
            'SELECT id
4300
            FROM ' . prefixTable('users') . '
4301
            WHERE id NOT IN ('.OTV_USER_ID.', '.TP_USER_ID.', '.SSH_USER_ID.', '.API_USER_ID.')
4302
            ORDER BY login ASC'
4303
        );
4304
        foreach ($users as $user) {
4305
            purgeUnnecessaryKeysForUser((int) $user['id']);
4306
        }
4307
    } else {
4308
        purgeUnnecessaryKeysForUser((int) $user_id);
4309
    }
4310
}
4311
4312
/**
4313
 * Delete unnecessary keys for personal items
4314
 *
4315
 * @param integer $user_id
4316
 * @return void
4317
 */
4318
function purgeUnnecessaryKeysForUser(int $user_id=0)
4319
{
4320
    if ($user_id === 0) {
4321
        return;
4322
    }
4323
4324
    // Load class DB
4325
    loadClasses('DB');
4326
4327
    $personalItems = DB::queryFirstColumn(
4328
        'SELECT id
4329
        FROM ' . prefixTable('items') . ' AS i
4330
        INNER JOIN ' . prefixTable('log_items') . ' AS li ON li.id_item = i.id
4331
        WHERE i.perso = 1 AND li.action = "at_creation" AND li.id_user IN (%i, '.TP_USER_ID.')',
4332
        $user_id
4333
    );
4334
    if (count($personalItems) > 0) {
4335
        // Item keys
4336
        DB::delete(
4337
            prefixTable('sharekeys_items'),
4338
            'object_id IN %li AND user_id NOT IN %ls',
4339
            $personalItems,
4340
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4341
        );
4342
        // Files keys
4343
        DB::delete(
4344
            prefixTable('sharekeys_files'),
4345
            'object_id IN %li AND user_id NOT IN %ls',
4346
            $personalItems,
4347
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4348
        );
4349
        // Fields keys
4350
        DB::delete(
4351
            prefixTable('sharekeys_fields'),
4352
            'object_id IN %li AND user_id NOT IN %ls',
4353
            $personalItems,
4354
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4355
        );
4356
        // Logs keys
4357
        DB::delete(
4358
            prefixTable('sharekeys_logs'),
4359
            'object_id IN %li AND user_id NOT IN %ls',
4360
            $personalItems,
4361
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4362
        );
4363
    }
4364
}
4365
4366
/**
4367
 * Generate recovery keys file
4368
 *
4369
 * @param integer $userId
4370
 * @param array $SETTINGS
4371
 * @return string
4372
 */
4373
function handleUserRecoveryKeysDownload(int $userId, array $SETTINGS):string
4374
{
4375
    $session = SessionManager::getSession();
4376
    // Check if user exists
4377
    $userInfo = DB::queryFirstRow(
4378
        'SELECT login
4379
        FROM ' . prefixTable('users') . '
4380
        WHERE id = %i',
4381
        $userId
4382
    );
4383
4384
    if (DB::count() > 0) {
4385
        $now = (int) time();
4386
        // Prepare file content
4387
        $export_value = file_get_contents(__DIR__."/../includes/core/teampass_ascii.txt")."\n".
4388
            "Generation date: ".date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now)."\n\n".
4389
            "RECOVERY KEYS - Not to be shared - To be store safely\n\n".
4390
            "Public Key:\n".$session->get('user-public_key')."\n\n".
4391
            "Private Key:\n".$session->get('user-private_key')."\n\n";
4392
4393
        // Update user's keys_recovery_time
4394
        DB::update(
4395
            prefixTable('users'),
4396
            [
4397
                'keys_recovery_time' => $now,
4398
            ],
4399
            'id=%i',
4400
            $userId
4401
        );
4402
        $session->set('user-keys_recovery_time', $now);
4403
4404
        //Log into DB the user's disconnection
4405
        logEvents($SETTINGS, 'user_mngt', 'at_user_keys_download', (string) $userId, $userInfo['login']);
4406
        
4407
        // Return data
4408
        return prepareExchangedData(
4409
            array(
4410
                'error' => false,
4411
                'datetime' => date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now),
4412
                'timestamp' => $now,
4413
                'content' => base64_encode($export_value),
4414
                'login' => $userInfo['login'],
4415
            ),
4416
            'encode'
4417
        );
4418
    }
4419
4420
    return prepareExchangedData(
4421
        array(
4422
            'error' => true,
4423
            'datetime' => '',
4424
        ),
4425
        'encode'
4426
    );
4427
}
4428
4429
/**
4430
 * Permits to load expected classes
4431
 *
4432
 * @param string $className
4433
 * @return void
4434
 */
4435
function loadClasses(string $className = ''): void
4436
{
4437
    require_once __DIR__. '/../includes/config/include.php';
4438
    require_once __DIR__. '/../includes/config/settings.php';
4439
    require_once __DIR__.'/../vendor/autoload.php';
4440
4441
    if (defined('DB_PASSWD_CLEAR') === false) {
4442
        define('DB_PASSWD_CLEAR', defuseReturnDecrypted(DB_PASSWD));
4443
    }
4444
4445
    if (empty($className) === false) {
4446
        // Load class DB
4447
        if ((string) $className === 'DB') {
4448
            //Connect to DB
4449
            DB::$host = DB_HOST;
4450
            DB::$user = DB_USER;
4451
            DB::$password = DB_PASSWD_CLEAR;
4452
            DB::$dbName = DB_NAME;
4453
            DB::$port = DB_PORT;
4454
            DB::$encoding = DB_ENCODING;
4455
            DB::$ssl = DB_SSL;
4456
            DB::$connect_options = DB_CONNECT_OPTIONS;
4457
        }
4458
    }
4459
}
4460
4461
/**
4462
 * Returns the page the user is visiting.
4463
 *
4464
 * @return string The page name
4465
 */
4466
function getCurrectPage($SETTINGS)
4467
{
4468
    
4469
    $request = SymfonyRequest::createFromGlobals();
4470
4471
    // Parse the url
4472
    parse_str(
4473
        substr(
4474
            (string) $request->getRequestUri(),
4475
            strpos((string) $request->getRequestUri(), '?') + 1
4476
        ),
4477
        $result
4478
    );
4479
4480
    return $result['page'];
4481
}
4482
4483
/**
4484
 * Permits to return value if set
4485
 *
4486
 * @param string|int $value
4487
 * @param string|int|null $retFalse
4488
 * @param string|int $retTrue
4489
 * @return mixed
4490
 */
4491
function returnIfSet($value, $retFalse = '', $retTrue = null): mixed
4492
{
4493
    if (!empty($value)) {
4494
        return is_null($retTrue) ? $value : $retTrue;
4495
    }
4496
    return $retFalse;
4497
}
4498
4499
4500
/**
4501
 * SEnd email to user
4502
 *
4503
 * @param string $post_receipt
4504
 * @param string $post_body
4505
 * @param string $post_subject
4506
 * @param array $post_replace
4507
 * @param boolean $immediate_email
4508
 * @param string $encryptedUserPassword
4509
 * @return string
4510
 */
4511
function sendMailToUser(
4512
    string $post_receipt,
4513
    string $post_body,
4514
    string $post_subject,
4515
    array $post_replace,
4516
    bool $immediate_email = false,
4517
    $encryptedUserPassword = ''
4518
): ?string {
4519
    global $SETTINGS;
4520
    $emailSettings = new EmailSettings($SETTINGS);
4521
    $emailService = new EmailService();
4522
    $antiXss = new AntiXSS();
4523
4524
    // Sanitize inputs
4525
    $post_receipt = filter_var($post_receipt, FILTER_SANITIZE_EMAIL);
4526
    $post_subject = $antiXss->xss_clean($post_subject);
4527
    $post_body = $antiXss->xss_clean($post_body);
4528
4529
    if (count($post_replace) > 0) {
4530
        $post_body = str_replace(
4531
            array_keys($post_replace),
4532
            array_values($post_replace),
4533
            $post_body
4534
        );
4535
    }
4536
4537
    // Remove newlines to prevent header injection
4538
    $post_body = str_replace(array("\r", "\n"), '', $post_body);    
4539
4540
    if ($immediate_email === true) {
4541
        // Send email
4542
        $ret = $emailService->sendMail(
4543
            $post_subject,
4544
            $post_body,
4545
            $post_receipt,
4546
            $emailSettings,
4547
            '',
4548
            false
4549
        );
4550
    
4551
        $ret = json_decode($ret, true);
4552
    
4553
        return prepareExchangedData(
4554
            array(
4555
                'error' => empty($ret['error']) === true ? false : true,
4556
                'message' => $ret['message'],
4557
            ),
4558
            'encode'
4559
        );
4560
    } else {
4561
        // Send through task handler
4562
        prepareSendingEmail(
4563
            $post_subject,
4564
            $post_body,
4565
            $post_receipt,
4566
            "",
4567
            $encryptedUserPassword,
4568
        );
4569
    }
4570
4571
    return null;
4572
}
4573
4574
/**
4575
 * Converts a password strengh value to zxcvbn level
4576
 * 
4577
 * @param integer $passwordStrength
4578
 * 
4579
 * @return integer
4580
 */
4581
function convertPasswordStrength($passwordStrength): int
4582
{
4583
    if ($passwordStrength === 0) {
4584
        return TP_PW_STRENGTH_1;
4585
    } else if ($passwordStrength === 1) {
4586
        return TP_PW_STRENGTH_2;
4587
    } else if ($passwordStrength === 2) {
4588
        return TP_PW_STRENGTH_3;
4589
    } else if ($passwordStrength === 3) {
4590
        return TP_PW_STRENGTH_4;
4591
    } else {
4592
        return TP_PW_STRENGTH_5;
4593
    }
4594
}
4595
4596
/**
4597
 * Check that a password is strong. The password needs to have at least :
4598
 *   - length >= 10.
4599
 *   - Uppercase and lowercase chars.
4600
 *   - Number or special char.
4601
 *   - Not contain username, name or mail part.
4602
 *   - Different from previous password.
4603
 * 
4604
 * @param string $password - Password to ckeck.
4605
 * @return bool - true if the password is strong, false otherwise.
4606
 */
4607
function isPasswordStrong($password) {
4608
    $session = SessionManager::getSession();
4609
4610
    // Password can't contain login, name or lastname
4611
    $forbiddenWords = [
4612
        $session->get('user-login'),
4613
        $session->get('user-name'),
4614
        $session->get('user-lastname'),
4615
    ];
4616
4617
    // Cut out the email
4618
    if ($email = $session->get('user-email')) {
4619
        $emailParts = explode('@', $email);
4620
4621
        if (count($emailParts) === 2) {
4622
            // Mail username (removed @domain.tld)
4623
            $forbiddenWords[] = $emailParts[0];
4624
4625
            // Organisation name (removed username@ and .tld)
4626
            $domain = explode('.', $emailParts[1]);
4627
            if (count($domain) > 1)
4628
                $forbiddenWords[] = $domain[0];
4629
        }
4630
    }
4631
4632
    // Search forbidden words in password
4633
    foreach ($forbiddenWords as $word) {
4634
        if (empty($word))
4635
            continue;
4636
4637
        // Stop if forbidden word found in password
4638
        if (stripos($password, $word) !== false)
4639
            return false;
4640
    }
4641
4642
    // Get password complexity
4643
    $length = strlen($password);
4644
    $hasUppercase = preg_match('/[A-Z]/', $password);
4645
    $hasLowercase = preg_match('/[a-z]/', $password);
4646
    $hasNumber = preg_match('/[0-9]/', $password);
4647
    $hasSpecialChar = preg_match('/[\W_]/', $password);
4648
4649
    // Get current user hash
4650
    $userHash = DB::queryFirstRow(
4651
        "SELECT pw FROM " . prefixtable('users') . " WHERE id = %d;",
4652
        $session->get('user-id')
4653
    )['pw'];
4654
4655
    $passwordManager = new PasswordManager();
4656
    
4657
    return $length >= 8
4658
           && $hasUppercase
4659
           && $hasLowercase
4660
           && ($hasNumber || $hasSpecialChar)
4661
           && !$passwordManager->verifyPassword($userHash, $password);
4662
}
4663
4664
4665
/**
4666
 * Converts a value to a string, handling various types and cases.
4667
 *
4668
 * @param mixed $value La valeur à convertir
4669
 * @param string $default Valeur par défaut si la conversion n'est pas possible
4670
 * @return string
4671
 */
4672
function safeString($value, string $default = ''): string
4673
{
4674
    // Simple cases
4675
    if (is_string($value)) {
4676
        return $value;
4677
    }
4678
    
4679
    if (is_scalar($value)) {
4680
        return (string) $value;
4681
    }
4682
    
4683
    // Special cases
4684
    if (is_null($value)) {
4685
        return $default;
4686
    }
4687
    
4688
    if (is_array($value)) {
4689
        return empty($value) ? $default : json_encode($value, JSON_UNESCAPED_UNICODE);
4690
    }
4691
    
4692
    if (is_object($value)) {
4693
        // Vérifie si l'objet implémente __toString()
4694
        if (method_exists($value, '__toString')) {
4695
            return (string) $value;
4696
        }
4697
        
4698
        // Alternative: serialize ou json selon le contexte
4699
        return get_class($value) . (method_exists($value, 'getId') ? '#' . $value->getId() : '');
4700
    }
4701
    
4702
    if (is_resource($value)) {
4703
        return 'Resource#' . get_resource_id($value) . ' of type ' . get_resource_type($value);
4704
    }
4705
    
4706
    // Cas par défaut
4707
    return $default;
4708
}
4709
4710
/**
4711
 * Check if a user has access to a file
4712
 *
4713
 * @param integer $userId
4714
 * @param integer $fileId
4715
 * @return boolean
4716
 */
4717
function userHasAccessToFile(int $userId, int $fileId): bool
4718
{
4719
    // Check if user is admin
4720
    // Refuse access if user does not exist and/or is admin
4721
    $user = DB::queryFirstRow(
4722
        'SELECT admin
4723
        FROM ' . prefixTable('users') . '
4724
        WHERE id = %i',
4725
        $userId
4726
    );
4727
    if (DB::count() === 0 || (int) $user['admin'] === 1) {
4728
        return false;
4729
    }
4730
4731
    // Get file info
4732
    $file = DB::queryFirstRow(
4733
        'SELECT f.id_item, i.id_tree
4734
        FROM ' . prefixTable('files') . ' as f
4735
        INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
4736
        WHERE f.id = %i',
4737
        $fileId
4738
    );
4739
    if (DB::count() === 0) {
4740
        return false;
4741
    }
4742
4743
    // Check if user has access to the item
4744
    include_once __DIR__. '/items.queries.php';
4745
    $itemAccess = getCurrentAccessRights(
4746
        (int) filter_var($userId, FILTER_SANITIZE_NUMBER_INT),
4747
        (int) filter_var($file['id_item'], FILTER_SANITIZE_NUMBER_INT),
4748
        (int) filter_var($file['id_tree'], FILTER_SANITIZE_NUMBER_INT),
4749
        (string) filter_var('show', FILTER_SANITIZE_SPECIAL_CHARS),
4750
    );
4751
4752
    return $itemAccess['access'] === true;
4753
}
4754
4755
/**
4756
 * Check if a user has access to a backup file
4757
 * 
4758
 * @param integer $userId
4759
 * @param string $file
4760
 * @param string $key
4761
 * @param string $keyTmp
4762
 * @return boolean
4763
 */
4764
function userHasAccessToBackupFile(int $userId, string $file, string $key, string $keyTmp): bool
4765
{
4766
    $session = SessionManager::getSession();
4767
4768
    // Ensure session keys are ok
4769
    if ($session->get('key') !== $key || $session->get('user-key_tmp') !== $keyTmp) {
4770
        return false;
4771
    }
4772
    
4773
    // Check if user is admin
4774
    // Refuse access if user does not exist and/or is not admin
4775
    $user = DB::queryFirstRow(
4776
        'SELECT admin
4777
        FROM ' . prefixTable('users') . '
4778
        WHERE id = %i',
4779
        $userId
4780
    );
4781
    if (DB::count() === 0 || (int) $user['admin'] === 0) {
4782
        return false;
4783
    }
4784
    
4785
    // Ensure that user has performed the backup
4786
    DB::queryFirstRow(
4787
        'SELECT f.id
4788
        FROM ' . prefixTable('log_system') . ' as f
4789
        WHERE f.type = %s AND f.label = %s AND f.qui = %i AND f.field_1 = %s',
4790
        'admin_action',
4791
        'dataBase backup',
4792
        $userId,
4793
        $file
4794
    );
4795
    if (DB::count() === 0) {
4796
        return false;
4797
    }
4798
4799
    return true;
4800
}
4801
4802
/**
4803
 * Ensure that personal items have only keys for their owner
4804
 *
4805
 * @param integer $userId
4806
 * @param integer $itemId
4807
 * @return boolean
4808
 */
4809
function EnsurePersonalItemHasOnlyKeysForOwner(int $userId, int $itemId): bool
4810
{
4811
    // Check if user is admin
4812
    // Refuse access if user does not exist and/or is admin
4813
    $user = DB::queryFirstRow(
4814
        'SELECT admin
4815
        FROM ' . prefixTable('users') . '
4816
        WHERE id = %i',
4817
        $userId
4818
    );
4819
    if (DB::count() === 0 || (int) $user['admin'] === 1) {
4820
        return false;
4821
    }
4822
4823
    // Get item info
4824
    $item = DB::queryFirstRow(
4825
        'SELECT i.perso, i.id_tree
4826
        FROM ' . prefixTable('items') . ' as i
4827
        WHERE i.id = %i',
4828
        $itemId
4829
    );
4830
    if (DB::count() === 0 || (int) $item['perso'] === 0) {
4831
        return false;
4832
    }
4833
4834
    // Get item owner
4835
    $itemOwner = DB::queryFirstRow(
4836
        'SELECT li.id_user
4837
        FROM ' . prefixTable('log_items') . ' as li
4838
        WHERE li.id_item = %i AND li.action = %s',
4839
        $itemId,
4840
        'at_creation'
4841
    );
4842
    if (DB::count() === 0 || (int) $itemOwner['id_user'] !== $userId) {
4843
        return false;
4844
    }
4845
4846
    // Delete all keys for this item except for the owner and TeamPass system user
4847
    DB::delete(
4848
        prefixTable('sharekeys_items'),
4849
        'object_id = %i AND user_id NOT IN %ls',
4850
        $itemId,
4851
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4852
    );
4853
    DB::delete(
4854
        prefixTable('sharekeys_files'),
4855
        'object_id IN (SELECT id FROM '.prefixTable('files').' WHERE id_item = %i) AND user_id NOT IN %ls',
4856
        $itemId,
4857
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4858
    );
4859
    DB::delete(
4860
        prefixTable('sharekeys_fields'),
4861
        'object_id IN (SELECT id FROM '.prefixTable('fields').' WHERE id_item = %i) AND user_id NOT IN %ls',
4862
        $itemId,
4863
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4864
    );
4865
    DB::delete(
4866
        prefixTable('sharekeys_logs'),
4867
        'object_id IN (SELECT id FROM '.prefixTable('log_items').' WHERE id_item = %i) AND user_id NOT IN %ls',
4868
        $itemId,
4869
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4870
    );
4871
4872
    return true;
4873
}
4874
4875
/**
4876
 * Insert a new record in a table with an "is_current" flag.
4877
 * This function ensures that only one record per user has the "is_current" flag set to true.
4878
 * 
4879
 * @param int $userId The ID of the user.
4880
 * @param string $privateKey The private key to be inserted.
4881
 * @return void
4882
 */
4883
function insertPrivateKeyWithCurrentFlag(int $userId, string $privateKey) {    
4884
    try {
4885
        DB::startTransaction();
4886
        
4887
        // Disable is_current for existing records of the user
4888
        DB::update(
4889
            prefixTable('user_private_keys'),
4890
            array('is_current' => false),
4891
            "user_id = %i AND is_current = %i",
4892
            $userId,
4893
            true
4894
        );
4895
        
4896
        // Insert the new record
4897
        DB::insert(
4898
            prefixTable('user_private_keys'),
4899
            array(
4900
                'user_id' => $userId,
4901
                'private_key' => $privateKey,
4902
                'is_current' => true,
4903
            )
4904
        );
4905
        
4906
        DB::commit();
4907
        
4908
    } catch (Exception $e) {
4909
        DB::rollback();
4910
        throw $e;
4911
    }
4912
}
4913
4914
/**
4915
 * Check and migrate personal items at user login
4916
 * After successful authentication and private key decryption
4917
 * 
4918
 * @param int $userId User ID
4919
 * @param string $privateKeyDecrypted Decrypted private key from login
4920
 * @param string $passwordClear Clear user password
4921
 * @return void
4922
 */
4923
function checkAndMigratePersonalItems($userId, $privateKeyDecrypted, $passwordClear) {
4924
    $session = SessionManager::getSession();
4925
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
4926
4927
    // 1. Check migration flag in users table
4928
    $user = DB::queryFirstRow(
4929
        "SELECT personal_items_migrated, login 
4930
         FROM ".prefixTable('users')." 
4931
         WHERE id = %i",
4932
        $userId
4933
    );
4934
    
4935
    if ((int) $user['personal_items_migrated'] === 1) {
4936
        return; // Already migrated, nothing to do
4937
    }
4938
    
4939
    // 2. Check if user actually has personal items to migrate
4940
    $personalFolderId = DB::queryFirstField(
4941
        "SELECT id FROM ".prefixTable('nested_tree') ."
4942
         WHERE personal_folder = 1 
4943
         AND title = %s",
4944
        $userId
4945
    );
4946
    
4947
    if (!$personalFolderId) {
4948
        // User has no personal folder, mark as migrated
4949
        DB::update(prefixTable('users'), [
4950
            'personal_items_migrated' => 1
4951
        ], ['id' => $userId]);
4952
        return;
4953
    }
4954
    
4955
    // 3. Count items to migrate
4956
    // Get list of all personal subfolders
4957
    $personalFoldersIds = $tree->getDescendants($personalFolderId, true, false, true);
4958
    $itemsToMigrate = DB::query(
4959
        "SELECT i.id
4960
         FROM ".prefixTable('items')." i
4961
         WHERE i.perso = 1 
4962
         AND i.id_tree IN %li",
4963
        $personalFoldersIds
4964
    );
4965
    
4966
    $totalItems = count($itemsToMigrate);
4967
    
4968
    if ($totalItems == 0) {
4969
        // No items to migrate, mark user as migrated
4970
        DB::update(prefixTable('users'), [
4971
            'personal_items_migrated' => 1
4972
        ], ['id' => $userId]);
4973
        return;
4974
    }
4975
    
4976
    // 4. Check if migration task already exists and is pending
4977
    $existingTask = DB::queryFirstRow(
4978
        "SELECT increment_id, status FROM ".prefixTable('background_tasks')."
4979
         WHERE process_type = 'migrate_user_personal_items'
4980
         AND item_id = %i
4981
         AND status IN ('pending', 'in_progress')
4982
         ORDER BY created_at DESC LIMIT 1",
4983
        $userId
4984
    );
4985
    
4986
    if ($existingTask) {
4987
        // Migration already in progress
4988
        $session->set('migration_personal_items_in_progress', true);
4989
        return;
4990
    }
4991
    
4992
    // 5. Create migration task
4993
    createUserMigrationTask($userId, $privateKeyDecrypted, $passwordClear, json_encode($personalFoldersIds));
4994
    
4995
    // 6. Notify user
4996
    $session->set('migration_personal_items_started', true);
4997
    $session->set('migration_total_items', $totalItems);
4998
}
4999
5000
/**
5001
 * Create migration task for a specific user
5002
 * 
5003
 * @param int $userId User ID
5004
 * @param string $privateKeyDecrypted Decrypted private key
5005
 * @param string $passwordClear Clear user password
5006
 * @param string $personalFolderIds
5007
 * @return void
5008
 */
5009
function createUserMigrationTask($userId, $privateKeyDecrypted, $passwordClear, $personalFolderIds): void
5010
{
5011
    // Decrypt all personal items with this key
5012
    // Launch the re-encryption process for personal items
5013
    // Create process
5014
    DB::insert(
5015
        prefixTable('background_tasks'),
5016
        array(
5017
            'created_at' => time(),
5018
            'process_type' => 'migrate_user_personal_items',
5019
            'arguments' => json_encode([
5020
                'user_id' => (int) $userId,
5021
                'user_pwd' => cryption($passwordClear, '','encrypt')['string'],
5022
                'user_private_key' => cryption($privateKeyDecrypted, '','encrypt')['string'],
5023
                'personal_folders_ids' => $personalFolderIds,
5024
            ]),
5025
            'is_in_progress' => 0,
5026
            'status' => 'pending',
5027
            'item_id' => $userId // Use item_id to store user_id for easy filtering
5028
        )
5029
    );
5030
    $processId = DB::insertId();
5031
5032
    // Create tasks
5033
    createUserMigrationSubTasks($processId, NUMBER_ITEMS_IN_BATCH);
5034
5035
    // update user's new status
5036
    DB::update(
5037
        prefixTable('users'),
5038
        [
5039
            'is_ready_for_usage' => 0,
5040
            'ongoing_process_id' => $processId,
5041
        ],
5042
        'id=%i',
5043
        $userId
5044
    );
5045
}
5046
5047
function createUserMigrationSubTasks($processId, $nbItemsToTreat): void
5048
{
5049
    // Prepare the subtask queries
5050
    $queries = [
5051
        'user-personal-items-migration-step10' => 'SELECT * FROM ' . prefixTable('items'),
5052
5053
        'user-personal-items-migration-step20' => 'SELECT * FROM ' . prefixTable('log_items') . 
5054
                    ' WHERE raison LIKE "at_pw :%" AND encryption_type = "teampass_aes"',
5055
5056
        'user-personal-items-migration-step30' => 'SELECT * FROM ' . prefixTable('categories_items') . 
5057
                    ' WHERE encryption_type = "teampass_aes"',
5058
5059
        'user-personal-items-migration-step40' => 'SELECT * FROM ' . prefixTable('suggestion'),
5060
5061
        'user-personal-items-migration-step50' => 'SELECT * FROM ' . prefixTable('files') . ' AS f
5062
                        INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
5063
                        WHERE f.status = "' . TP_ENCRYPTION_NAME . '"'
5064
    ];
5065
5066
    // Perform loop on $queries to create sub-tasks
5067
    foreach ($queries as $step => $query) {
5068
        DB::query($query);
5069
        createAllSubTasks($step, DB::count(), $nbItemsToTreat, $processId);
5070
    }
5071
5072
    // Create subtask for step final
5073
    DB::insert(
5074
        prefixTable('background_subtasks'),
5075
        array(
5076
            'task_id' => $processId,
5077
            'created_at' => time(),
5078
            'task' => json_encode([
5079
                'step' => 'user-personal-items-migration-step-final',
5080
            ]),
5081
        )
5082
    );
5083
}
5084
5085
/**
5086
 * Add or update an item in user's latest items list (max 20, FIFO)
5087
 * 
5088
 * @param int $userId User ID
5089
 * @param int $itemId Item ID to add
5090
 * @return void
5091
 */
5092
function updateUserLatestItems(int $userId, int $itemId): void
5093
{
5094
    // 1. Insert or update the item with current timestamp
5095
    DB::query(
5096
        'INSERT INTO ' . prefixTable('users_latest_items') . ' (user_id, item_id, accessed_at)
5097
        VALUES (%i, %i, NOW())
5098
        ON DUPLICATE KEY UPDATE accessed_at = NOW()',
5099
        $userId,
5100
        $itemId
5101
    );
5102
    
5103
    // 2. Keep only the 20 most recent items (delete older ones)
5104
    DB::query(
5105
        'DELETE FROM ' . prefixTable('users_latest_items') . '
5106
        WHERE user_id = %i
5107
        AND increment_id NOT IN (
5108
            SELECT increment_id FROM (
5109
                SELECT increment_id 
5110
                FROM ' . prefixTable('users_latest_items') . '
5111
                WHERE user_id = %i
5112
                ORDER BY accessed_at DESC
5113
                LIMIT 20
5114
            ) AS keep_items
5115
        )',
5116
        $userId,
5117
        $userId
5118
    );
5119
}
5120
5121
/**
5122
 * Get complete user data with all relational tables
5123
 * 
5124
 * @param string $login User login
5125
 * @param int|null $userId User ID (alternative to login)
5126
 * @return array|null User data or null if not found
5127
 */
5128
function getUserCompleteData($login = null, $userId = null)
5129
{
5130
    if (empty($login) && empty($userId)) {
5131
        return null;
5132
    }
5133
    
5134
    // Build WHERE clause
5135
    if (!empty($login)) {
5136
        $whereClause = 'u.login = %s AND u.deleted_at IS NULL';
5137
        $whereParam = $login;
5138
    } else {
5139
        $whereClause = 'u.id = %i AND u.deleted_at IS NULL';
5140
        $whereParam = $userId;
5141
    }
5142
    
5143
    // Get user with all related data
5144
    $data = DB::queryFirstRow(
5145
        'SELECT u.*, 
5146
         a.value AS api_key, a.enabled AS api_enabled, a.allowed_folders as api_allowed_folders, a.allowed_to_create as api_allowed_to_create, a.allowed_to_read as api_allowed_to_read, a.allowed_to_update as api_allowed_to_update , a.allowed_to_delete as api_allowed_to_delete,
5147
         GROUP_CONCAT(DISTINCT ug.group_id ORDER BY ug.group_id SEPARATOR ";") AS groupes_visibles,
5148
         GROUP_CONCAT(DISTINCT ugf.group_id ORDER BY ugf.group_id SEPARATOR ";") AS groupes_interdits,
5149
         GROUP_CONCAT(DISTINCT CASE WHEN ur.source = "manual" THEN ur.role_id END ORDER BY ur.role_id SEPARATOR ";") AS fonction_id,
5150
         GROUP_CONCAT(DISTINCT CASE WHEN ur.source = "ad" THEN ur.role_id END ORDER BY ur.role_id SEPARATOR ";") AS roles_from_ad_groups,
5151
         GROUP_CONCAT(DISTINCT uf.item_id ORDER BY uf.created_at SEPARATOR ";") AS favourites,
5152
         GROUP_CONCAT(DISTINCT ul.item_id ORDER BY ul.accessed_at DESC SEPARATOR ";") AS latest_items
5153
        FROM ' . prefixTable('users') . ' AS u
5154
        LEFT JOIN ' . prefixTable('api') . ' AS a ON (u.id = a.user_id)
5155
        LEFT JOIN ' . prefixTable('users_groups') . ' AS ug ON (u.id = ug.user_id)
5156
        LEFT JOIN ' . prefixTable('users_groups_forbidden') . ' AS ugf ON (u.id = ugf.user_id)
5157
        LEFT JOIN ' . prefixTable('users_roles') . ' AS ur ON (u.id = ur.user_id)
5158
        LEFT JOIN ' . prefixTable('users_favorites') . ' AS uf ON (u.id = uf.user_id)
5159
        LEFT JOIN ' . prefixTable('users_latest_items') . ' AS ul ON (u.id = ul.user_id)
5160
        WHERE ' . $whereClause . '
5161
        GROUP BY u.id',
5162
        $whereParam
5163
    );
5164
    
5165
    // Ensure empty strings instead of NULL for concatenated fields
5166
    if ($data) {
5167
        $data['groupes_visibles'] = $data['groupes_visibles'] ?? '';
5168
        $data['groupes_interdits'] = $data['groupes_interdits'] ?? '';
5169
        $data['fonction_id'] = $data['fonction_id'] ?? '';
5170
        $data['roles_from_ad_groups'] = $data['roles_from_ad_groups'] ?? '';
5171
        $data['favourites'] = $data['favourites'] ?? '';
5172
        $data['latest_items'] = $data['latest_items'] ?? '';
5173
    }
5174
    
5175
    return $data;
5176
}
5177
5178
5179
5180
5181
// ----------------------
5182
// Users Groups
5183
// ----------------------
5184
/**
5185
 * Add a group to user's accessible groups
5186
 */
5187
function addUserGroup(int $userId, int $groupId): void
5188
{
5189
    DB::query(
5190
        'INSERT IGNORE INTO ' . prefixTable('users_groups') . ' (user_id, group_id)
5191
        VALUES (%i, %i)',
5192
        $userId,
5193
        $groupId
5194
    );
5195
}
5196
5197
/**
5198
 * Remove a group from user's accessible groups
5199
 */
5200
function removeUserGroup(int $userId, int $groupId): void
5201
{
5202
    DB::query(
5203
        'DELETE FROM ' . prefixTable('users_groups') . '
5204
        WHERE user_id = %i AND group_id = %i',
5205
        $userId,
5206
        $groupId
5207
    );
5208
}
5209
5210
/**
5211
 * Replace all user's groups with a new list
5212
 * 
5213
 * @param int $userId User ID
5214
 * @param array $groupIds Array of group IDs
5215
 */
5216
function setUserGroups(int $userId, array $groupIds): void
5217
{
5218
    // Delete all existing groups
5219
    DB::query(
5220
        'DELETE FROM ' . prefixTable('users_groups') . ' WHERE user_id = %i',
5221
        $userId
5222
    );
5223
    
5224
    // Insert new groups
5225
    foreach ($groupIds as $groupId) {
5226
        if (!empty($groupId) && is_numeric($groupId)) {
5227
            addUserGroup($userId, (int) $groupId);
5228
        }
5229
    }
5230
}
5231
5232
/**
5233
 * Get all user's accessible groups
5234
 * 
5235
 * @return array Array of group IDs
5236
 */
5237
function getUserGroups(int $userId): array
5238
{
5239
    $result = DB::query(
5240
        'SELECT group_id FROM ' . prefixTable('users_groups') . ' 
5241
        WHERE user_id = %i ORDER BY group_id',
5242
        $userId
5243
    );
5244
    return array_column($result, 'group_id');
5245
}
5246
// --<
5247
5248
// ----------------------
5249
// Users Groups Forbidden
5250
// ----------------------
5251
/**
5252
 * Add a forbidden group to user
5253
 */
5254
function addUserForbiddenGroup(int $userId, int $groupId): void
5255
{
5256
    DB::query(
5257
        'INSERT IGNORE INTO ' . prefixTable('users_groups_forbidden') . ' (user_id, group_id)
5258
        VALUES (%i, %i)',
5259
        $userId,
5260
        $groupId
5261
    );
5262
}
5263
5264
/**
5265
 * Remove a forbidden group from user
5266
 */
5267
function removeUserForbiddenGroup(int $userId, int $groupId): void
5268
{
5269
    DB::query(
5270
        'DELETE FROM ' . prefixTable('users_groups_forbidden') . '
5271
        WHERE user_id = %i AND group_id = %i',
5272
        $userId,
5273
        $groupId
5274
    );
5275
}
5276
5277
/**
5278
 * Replace all user's forbidden groups
5279
 */
5280
function setUserForbiddenGroups(int $userId, array $groupIds): void
5281
{
5282
    DB::query(
5283
        'DELETE FROM ' . prefixTable('users_groups_forbidden') . ' WHERE user_id = %i',
5284
        $userId
5285
    );
5286
    
5287
    foreach ($groupIds as $groupId) {
5288
        if (!empty($groupId) && is_numeric($groupId)) {
5289
            addUserForbiddenGroup($userId, (int) $groupId);
5290
        }
5291
    }
5292
}
5293
5294
/**
5295
 * Get all user's forbidden groups
5296
 */
5297
function getUserForbiddenGroups(int $userId): array
5298
{
5299
    $result = DB::query(
5300
        'SELECT group_id FROM ' . prefixTable('users_groups_forbidden') . ' 
5301
        WHERE user_id = %i ORDER BY group_id',
5302
        $userId
5303
    );
5304
    return array_column($result, 'group_id');
5305
}
5306
// ---<
5307
5308
// ----------------------
5309
// Users Roles
5310
// ----------------------
5311
/**
5312
 * Add a role to user
5313
 * 
5314
 * @param string $source 'manual', 'ad', 'ldap', 'oauth2'
5315
 */
5316
function addUserRole(int $userId, int $roleId, string $source = 'manual'): void
5317
{
5318
    DB::query(
5319
        'INSERT IGNORE INTO ' . prefixTable('users_roles') . ' (user_id, role_id, source)
5320
        VALUES (%i, %i, %s)',
5321
        $userId,
5322
        $roleId,
5323
        $source
5324
    );
5325
}
5326
5327
/**
5328
 * Remove a role from user
5329
 */
5330
function removeUserRole(int $userId, int $roleId, string $source = 'manual'): void
5331
{
5332
    DB::query(
5333
        'DELETE FROM ' . prefixTable('users_roles') . '
5334
        WHERE user_id = %i AND role_id = %i AND source = %s',
5335
        $userId,
5336
        $roleId,
5337
        $source
5338
    );
5339
}
5340
5341
/**
5342
 * Remove all roles of a specific source
5343
 */
5344
function removeUserRolesBySource(int $userId, string $source): void
5345
{
5346
    DB::query(
5347
        'DELETE FROM ' . prefixTable('users_roles') . '
5348
        WHERE user_id = %i AND source = %s',
5349
        $userId,
5350
        $source
5351
    );
5352
}
5353
5354
/**
5355
 * Replace all user's roles for a specific source
5356
 * 
5357
 * @param string $source 'manual', 'ad', 'ldap', 'oauth2'
5358
 */
5359
function setUserRoles(int $userId, array $roleIds, string $source = 'manual'): void
5360
{
5361
    // Delete existing roles for this source
5362
    removeUserRolesBySource($userId, $source);
5363
    
5364
    // Insert new roles
5365
    foreach ($roleIds as $roleId) {
5366
        if (!empty($roleId) && is_numeric($roleId)) {
5367
            addUserRole($userId, (int) $roleId, $source);
5368
        }
5369
    }
5370
}
5371
5372
/**
5373
 * Get all user's roles by source
5374
 * 
5375
 * @param string|null $source Filter by source, null = all sources
5376
 */
5377
function getUserRoles(int $userId, ?string $source = null): array
5378
{
5379
    if ($source !== null) {
5380
        $result = DB::query(
5381
            'SELECT role_id FROM ' . prefixTable('users_roles') . ' 
5382
            WHERE user_id = %i AND source = %s ORDER BY role_id',
5383
            $userId,
5384
            $source
5385
        );
5386
    } else {
5387
        $result = DB::query(
5388
            'SELECT role_id FROM ' . prefixTable('users_roles') . ' 
5389
            WHERE user_id = %i ORDER BY role_id',
5390
            $userId
5391
        );
5392
    }
5393
    return array_column($result, 'role_id');
5394
}
5395
// ---<
5396
5397
// ----------------------
5398
// Users Favorites
5399
// ----------------------
5400
/**
5401
 * Add an item to user's favorites
5402
 */
5403
function addUserFavorite(int $userId, int $itemId): void
5404
{
5405
    DB::query(
5406
        'INSERT IGNORE INTO ' . prefixTable('users_favorites') . ' (user_id, item_id)
5407
        VALUES (%i, %i)',
5408
        $userId,
5409
        $itemId
5410
    );
5411
}
5412
5413
/**
5414
 * Remove an item from user's favorites
5415
 */
5416
function removeUserFavorite(int $userId, int $itemId): void
5417
{
5418
    DB::query(
5419
        'DELETE FROM ' . prefixTable('users_favorites') . '
5420
        WHERE user_id = %i AND item_id = %i',
5421
        $userId,
5422
        $itemId
5423
    );
5424
}
5425
5426
/**
5427
 * Toggle favorite (add if not exists, remove if exists)
5428
 */
5429
function toggleUserFavorite(int $userId, int $itemId): bool
5430
{
5431
    $exists = DB::queryFirstRow(
5432
        'SELECT increment_id FROM ' . prefixTable('users_favorites') . '
5433
        WHERE user_id = %i AND item_id = %i',
5434
        $userId,
5435
        $itemId
5436
    );
5437
    
5438
    if ($exists) {
5439
        removeUserFavorite($userId, $itemId);
5440
        return false; // Removed
5441
    } else {
5442
        addUserFavorite($userId, $itemId);
5443
        return true; // Added
5444
    }
5445
}
5446
5447
/**
5448
 * Replace all user's favorites
5449
 */
5450
function setUserFavorites(int $userId, array $itemIds): void
5451
{
5452
    DB::query(
5453
        'DELETE FROM ' . prefixTable('users_favorites') . ' WHERE user_id = %i',
5454
        $userId
5455
    );
5456
    
5457
    foreach ($itemIds as $itemId) {
5458
        if (!empty($itemId) && is_numeric($itemId)) {
5459
            addUserFavorite($userId, (int) $itemId);
5460
        }
5461
    }
5462
}
5463
5464
/**
5465
 * Get all user's favorites
5466
 */
5467
function getUserFavorites(int $userId): array
5468
{
5469
    $result = DB::query(
5470
        'SELECT item_id FROM ' . prefixTable('users_favorites') . ' 
5471
        WHERE user_id = %i ORDER BY created_at DESC',
5472
        $userId
5473
    );
5474
    return array_column($result, 'item_id');
5475
}
5476
5477
/**
5478
 * Check if item is in user's favorites
5479
 */
5480
function isUserFavorite(int $userId, int $itemId): bool
5481
{
5482
    $result = DB::queryFirstRow(
5483
        'SELECT increment_id FROM ' . prefixTable('users_favorites') . '
5484
        WHERE user_id = %i AND item_id = %i',
5485
        $userId,
5486
        $itemId
5487
    );
5488
    return !empty($result);
5489
}
5490
// ---<
5491