Passed
Pull Request — master (#4822)
by Nils
06:14
created

GenerateCryptKey()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 26
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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

1840
            /** @scrutinizer ignore-type */ $password
Loading history...
1841
        );
1842
    } elseif ($type === 'encrypt') {
1843
        // Encrypt file
1844
        $err = defuseFileEncrypt(
1845
            $source_file,
1846
            $target_file,
1847
            $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

1847
            /** @scrutinizer ignore-type */ $password
Loading history...
1848
        );
1849
    }
1850
1851
    // return error
1852
    return $err === true ? $err : '';
1853
}
1854
1855
/**
1856
 * Encrypt a file with Defuse.
1857
 *
1858
 * @param string $source_file path to source file
1859
 * @param string $target_file path to target file
1860
 * @param array  $SETTINGS    Settings
1861
 * @param string $password    A password
1862
 *
1863
 * @return string|bool
1864
 */
1865
function defuseFileEncrypt(
1866
    string $source_file,
1867
    string $target_file,
1868
    ?string $password = null
1869
) {
1870
    $err = '';
1871
    try {
1872
        CryptoFile::encryptFileWithPassword(
1873
            $source_file,
1874
            $target_file,
1875
            $password
1876
        );
1877
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
1878
        $err = 'wrong_key';
1879
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
1880
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
1881
        $err = 'environment_error';
1882
    } catch (CryptoException\IOException $ex) {
1883
        error_log('TEAMPASS-Error-General: ' . $ex->getMessage());
1884
        $err = 'general_error';
1885
    }
1886
1887
    // return error
1888
    return empty($err) === false ? $err : true;
1889
}
1890
1891
/**
1892
 * Decrypt a file with Defuse.
1893
 *
1894
 * @param string $source_file path to source file
1895
 * @param string $target_file path to target file
1896
 * @param array  $SETTINGS    Settings
1897
 * @param string $password    A password
1898
 *
1899
 * @return string|bool
1900
 */
1901
function defuseFileDecrypt(
1902
    string $source_file,
1903
    string $target_file,
1904
    ?string $password = null
1905
) {
1906
    $err = '';
1907
    try {
1908
        CryptoFile::decryptFileWithPassword(
1909
            $source_file,
1910
            $target_file,
1911
            $password
1912
        );
1913
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
1914
        $err = 'wrong_key';
1915
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
1916
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
1917
        $err = 'environment_error';
1918
    } catch (CryptoException\IOException $ex) {
1919
        error_log('TEAMPASS-Error-General: ' . $ex->getMessage());
1920
        $err = 'general_error';
1921
    }
1922
1923
    // return error
1924
    return empty($err) === false ? $err : true;
1925
}
1926
1927
/*
1928
* NOT TO BE USED
1929
*/
1930
/**
1931
 * Undocumented function.
1932
 *
1933
 * @param string $text Text to debug
1934
 */
1935
function debugTeampass(string $text): void
1936
{
1937
    $debugFile = fopen('D:/wamp64/www/TeamPass/debug.txt', 'r+');
1938
    if ($debugFile !== false) {
1939
        fputs($debugFile, $text);
1940
        fclose($debugFile);
1941
    }
1942
}
1943
1944
/**
1945
 * DELETE the file with expected command depending on server type.
1946
 *
1947
 * @param string $file     Path to file
1948
 * @param array  $SETTINGS Teampass settings
1949
 *
1950
 * @return void
1951
 */
1952
function fileDelete(string $file, array $SETTINGS): void
1953
{
1954
    // Load AntiXSS
1955
    $antiXss = new AntiXSS();
1956
    $file = $antiXss->xss_clean($file);
1957
    if (is_file($file)) {
1958
        unlink($file);
1959
    }
1960
}
1961
1962
/**
1963
 * Permits to extract the file extension.
1964
 *
1965
 * @param string $file File name
1966
 *
1967
 * @return string
1968
 */
1969
function getFileExtension(string $file): string
1970
{
1971
    if (strpos($file, '.') === false) {
1972
        return $file;
1973
    }
1974
1975
    return substr($file, strrpos($file, '.') + 1);
1976
}
1977
1978
/**
1979
 * Chmods files and folders with different permissions.
1980
 *
1981
 * This is an all-PHP alternative to using: \n
1982
 * <tt>exec("find ".$path." -type f -exec chmod 644 {} \;");</tt> \n
1983
 * <tt>exec("find ".$path." -type d -exec chmod 755 {} \;");</tt>
1984
 *
1985
 * @author Jeppe Toustrup (tenzer at tenzer dot dk)
1986
  *
1987
 * @param string $path      An either relative or absolute path to a file or directory which should be processed.
1988
 * @param int    $filePerm The permissions any found files should get.
1989
 * @param int    $dirPerm  The permissions any found folder should get.
1990
 *
1991
 * @return bool Returns TRUE if the path if found and FALSE if not.
1992
 *
1993
 * @warning The permission levels has to be entered in octal format, which
1994
 * normally means adding a zero ("0") in front of the permission level. \n
1995
 * More info at: http://php.net/chmod.
1996
*/
1997
1998
function recursiveChmod(
1999
    string $path,
2000
    int $filePerm = 0644,
2001
    int  $dirPerm = 0755
2002
) {
2003
    // Check if the path exists
2004
    $path = basename($path);
2005
    if (! file_exists($path)) {
2006
        return false;
2007
    }
2008
2009
    // See whether this is a file
2010
    if (is_file($path)) {
2011
        // Chmod the file with our given filepermissions
2012
        try {
2013
            chmod($path, $filePerm);
2014
        } catch (Exception $e) {
2015
            return false;
2016
        }
2017
    // If this is a directory...
2018
    } elseif (is_dir($path)) {
2019
        // Then get an array of the contents
2020
        $foldersAndFiles = scandir($path);
2021
        // Remove "." and ".." from the list
2022
        $entries = array_slice($foldersAndFiles, 2);
2023
        // Parse every result...
2024
        foreach ($entries as $entry) {
2025
            // And call this function again recursively, with the same permissions
2026
            recursiveChmod($path.'/'.$entry, $filePerm, $dirPerm);
2027
        }
2028
2029
        // When we are done with the contents of the directory, we chmod the directory itself
2030
        try {
2031
            chmod($path, $filePerm);
2032
        } catch (Exception $e) {
2033
            return false;
2034
        }
2035
    }
2036
2037
    // Everything seemed to work out well, return true
2038
    return true;
2039
}
2040
2041
/**
2042
 * Check if user can access to this item.
2043
 *
2044
 * @param int   $item_id ID of item
2045
 * @param array $SETTINGS
2046
 *
2047
 * @return bool|string
2048
 */
2049
function accessToItemIsGranted(int $item_id, array $SETTINGS)
2050
{
2051
    
2052
    $session = SessionManager::getSession();
2053
    $session_groupes_visibles = $session->get('user-accessible_folders');
2054
    $session_list_restricted_folders_for_items = $session->get('system-list_restricted_folders_for_items');
2055
    // Load item data
2056
    $data = DB::queryFirstRow(
2057
        'SELECT id_tree
2058
        FROM ' . prefixTable('items') . '
2059
        WHERE id = %i',
2060
        $item_id
2061
    );
2062
    // Check if user can access this folder
2063
    if (in_array($data['id_tree'], $session_groupes_visibles) === false) {
2064
        // Now check if this folder is restricted to user
2065
        if (isset($session_list_restricted_folders_for_items[$data['id_tree']]) === true
2066
            && in_array($item_id, $session_list_restricted_folders_for_items[$data['id_tree']]) === false
2067
        ) {
2068
            return 'ERR_FOLDER_NOT_ALLOWED';
2069
        }
2070
    }
2071
2072
    return true;
2073
}
2074
2075
/**
2076
 * Creates a unique key.
2077
 *
2078
 * @param int $lenght Key lenght
2079
 *
2080
 * @return string
2081
 */
2082
function uniqidReal(int $lenght = 13): string
2083
{
2084
    if (function_exists('random_bytes')) {
2085
        $bytes = random_bytes(intval(ceil($lenght / 2)));
2086
    } elseif (function_exists('openssl_random_pseudo_bytes')) {
2087
        $bytes = openssl_random_pseudo_bytes(intval(ceil($lenght / 2)));
2088
    } else {
2089
        throw new Exception('no cryptographically secure random function available');
2090
    }
2091
2092
    return substr(bin2hex($bytes), 0, $lenght);
2093
}
2094
2095
/**
2096
 * Obfuscate an email.
2097
 *
2098
 * @param string $email Email address
2099
 *
2100
 * @return string
2101
 */
2102
function obfuscateEmail(string $email): string
2103
{
2104
    $email = explode("@", $email);
2105
    $name = $email[0];
2106
    if (strlen($name) > 3) {
2107
        $name = substr($name, 0, 2);
2108
        for ($i = 0; $i < strlen($email[0]) - 3; $i++) {
2109
            $name .= "*";
2110
        }
2111
        $name .= substr($email[0], -1, 1);
2112
    }
2113
    $host = explode(".", $email[1])[0];
2114
    if (strlen($host) > 3) {
2115
        $host = substr($host, 0, 1);
2116
        for ($i = 0; $i < strlen(explode(".", $email[1])[0]) - 2; $i++) {
2117
            $host .= "*";
2118
        }
2119
        $host .= substr(explode(".", $email[1])[0], -1, 1);
2120
    }
2121
    $email = $name . "@" . $host . "." . explode(".", $email[1])[1];
2122
    return $email;
2123
}
2124
2125
/**
2126
 * Get id and title from role_titles table.
2127
 *
2128
 * @return array
2129
 */
2130
function getRolesTitles(): array
2131
{
2132
    // Load class DB
2133
    loadClasses('DB');
2134
    
2135
    // Insert log in DB
2136
    return DB::query(
2137
        'SELECT id, title
2138
        FROM ' . prefixTable('roles_title')
2139
    );
2140
}
2141
2142
/**
2143
 * Undocumented function.
2144
 *
2145
 * @param int $bytes Size of file
2146
 *
2147
 * @return string
2148
 */
2149
function formatSizeUnits(int $bytes): string
2150
{
2151
    if ($bytes >= 1073741824) {
2152
        $bytes = number_format($bytes / 1073741824, 2) . ' GB';
2153
    } elseif ($bytes >= 1048576) {
2154
        $bytes = number_format($bytes / 1048576, 2) . ' MB';
2155
    } elseif ($bytes >= 1024) {
2156
        $bytes = number_format($bytes / 1024, 2) . ' KB';
2157
    } elseif ($bytes > 1) {
2158
        $bytes .= ' bytes';
2159
    } elseif ($bytes === 1) {
2160
        $bytes .= ' byte';
2161
    } else {
2162
        $bytes = '0 bytes';
2163
    }
2164
2165
    return $bytes;
2166
}
2167
2168
/**
2169
 * Generate user pair of keys.
2170
 *
2171
 * @param string $userPwd User password
2172
 *
2173
 * @return array
2174
 */
2175
function generateUserKeys(string $userPwd): array
2176
{
2177
    // Sanitize
2178
    $antiXss = new AntiXSS();
2179
    $userPwd = $antiXss->xss_clean($userPwd);
2180
    // Load classes
2181
    $rsa = new Crypt_RSA();
2182
    $cipher = new Crypt_AES();
2183
    // Create the private and public key
2184
    $res = $rsa->createKey(4096);
2185
    // Encrypt the privatekey
2186
    $cipher->setPassword($userPwd);
2187
    $privatekey = $cipher->encrypt($res['privatekey']);
2188
    return [
2189
        'private_key' => base64_encode($privatekey),
2190
        'public_key' => base64_encode($res['publickey']),
2191
        'private_key_clear' => base64_encode($res['privatekey']),
2192
    ];
2193
}
2194
2195
/**
2196
 * Permits to decrypt the user's privatekey.
2197
 *
2198
 * @param string $userPwd        User password
2199
 * @param string $userPrivateKey User private key
2200
 *
2201
 * @return string|object
2202
 */
2203
function decryptPrivateKey(string $userPwd, string $userPrivateKey)
2204
{
2205
    // Sanitize
2206
    $antiXss = new AntiXSS();
2207
    $userPwd = $antiXss->xss_clean($userPwd);
2208
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2209
2210
    if (empty($userPwd) === false) {
2211
        // Load classes
2212
        $cipher = new Crypt_AES();
2213
        // Encrypt the privatekey
2214
        $cipher->setPassword($userPwd);
2215
        try {
2216
            return base64_encode((string) $cipher->decrypt(base64_decode($userPrivateKey)));
2217
        } catch (Exception $e) {
2218
            return $e;
2219
        }
2220
    }
2221
    return '';
2222
}
2223
2224
/**
2225
 * Permits to encrypt the user's privatekey.
2226
 *
2227
 * @param string $userPwd        User password
2228
 * @param string $userPrivateKey User private key
2229
 *
2230
 * @return string
2231
 */
2232
function encryptPrivateKey(string $userPwd, string $userPrivateKey): string
2233
{
2234
    // Sanitize
2235
    $antiXss = new AntiXSS();
2236
    $userPwd = $antiXss->xss_clean($userPwd);
2237
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2238
2239
    if (empty($userPwd) === false) {
2240
        // Load classes
2241
        $cipher = new Crypt_AES();
2242
        // Encrypt the privatekey
2243
        $cipher->setPassword($userPwd);        
2244
        try {
2245
            return base64_encode($cipher->encrypt(base64_decode($userPrivateKey)));
2246
        } catch (Exception $e) {
2247
            return $e->getMessage();
2248
        }
2249
    }
2250
    return '';
2251
}
2252
2253
/**
2254
 * Encrypts a string using AES.
2255
 *
2256
 * @param string $data String to encrypt
2257
 * @param string $key
2258
 *
2259
 * @return array
2260
 */
2261
function doDataEncryption(string $data, ?string $key = NULL): array
2262
{
2263
    // Sanitize
2264
    $antiXss = new AntiXSS();
2265
    $data = $antiXss->xss_clean($data);
2266
    
2267
    // Load classes
2268
    $cipher = new Crypt_AES(CRYPT_AES_MODE_CBC);
2269
    // Generate an object key
2270
    $objectKey = is_null($key) === true ? uniqidReal(KEY_LENGTH) : $antiXss->xss_clean($key);
2271
    // Set it as password
2272
    $cipher->setPassword($objectKey);
2273
    return [
2274
        'encrypted' => base64_encode($cipher->encrypt($data)),
2275
        'objectKey' => base64_encode($objectKey),
2276
    ];
2277
}
2278
2279
/**
2280
 * Decrypts a string using AES.
2281
 *
2282
 * @param string $data Encrypted data
2283
 * @param string $key  Key to uncrypt
2284
 *
2285
 * @return string
2286
 */
2287
function doDataDecryption(string $data, string $key): string
2288
{
2289
    // Sanitize
2290
    $antiXss = new AntiXSS();
2291
    $data = $antiXss->xss_clean($data);
2292
    $key = $antiXss->xss_clean($key);
2293
2294
    // Load classes
2295
    $cipher = new Crypt_AES();
2296
    // Set the object key
2297
    $cipher->setPassword(base64_decode($key));
2298
    return base64_encode((string) $cipher->decrypt(base64_decode($data)));
2299
}
2300
2301
/**
2302
 * Encrypts using RSA a string using a public key.
2303
 *
2304
 * @param string $key       Key to be encrypted
2305
 * @param string $publicKey User public key
2306
 *
2307
 * @return string
2308
 */
2309
function encryptUserObjectKey(string $key, string $publicKey): string
2310
{
2311
    // Empty password
2312
    if (empty($key)) return '';
2313
2314
    // Sanitize
2315
    $antiXss = new AntiXSS();
2316
    $publicKey = $antiXss->xss_clean($publicKey);
2317
    // Load classes
2318
    $rsa = new Crypt_RSA();
2319
    // Load the public key
2320
    $decodedPublicKey = base64_decode($publicKey, true);
2321
    if ($decodedPublicKey === false) {
2322
        throw new InvalidArgumentException("Error while decoding key.");
2323
    }
2324
    $rsa->loadKey($decodedPublicKey);
2325
    // Encrypt
2326
    $encrypted = $rsa->encrypt(base64_decode($key));
2327
    if (empty($encrypted)) {  // Check if key is empty or null
2328
        throw new RuntimeException("Error while encrypting key.");
2329
    }
2330
    // Return
2331
    return base64_encode($encrypted);
2332
}
2333
2334
/**
2335
 * Decrypts using RSA an encrypted string using a private key.
2336
 *
2337
 * @param string $key        Encrypted key
2338
 * @param string $privateKey User private key
2339
 *
2340
 * @return string
2341
 */
2342
function decryptUserObjectKey(string $key, string $privateKey): string
2343
{
2344
    // Sanitize
2345
    $antiXss = new AntiXSS();
2346
    $privateKey = $antiXss->xss_clean($privateKey);
2347
2348
    // Load classes
2349
    $rsa = new Crypt_RSA();
2350
    // Load the private key
2351
    $decodedPrivateKey = base64_decode($privateKey, true);
2352
    if ($decodedPrivateKey === false) {
2353
        throw new InvalidArgumentException("Error while decoding private key.");
2354
    }
2355
2356
    $rsa->loadKey($decodedPrivateKey);
2357
2358
    // Decrypt
2359
    try {
2360
        $decodedKey = base64_decode($key, true);
2361
        if ($decodedKey === false) {
2362
            throw new InvalidArgumentException("Error while decoding key.");
2363
        }
2364
2365
        // This check is needed as decrypt() in version 2 can return false in case of error
2366
        $tmpValue = $rsa->decrypt($decodedKey);
2367
        if ($tmpValue !== false) {
0 ignored issues
show
introduced by
The condition $tmpValue !== false is always true.
Loading history...
2368
            return base64_encode($tmpValue);
2369
        } else {
2370
            return '';
2371
        }
2372
    } catch (Exception $e) {
2373
        if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
2374
            error_log('TEAMPASS Error - ldap - '.$e->getMessage());
2375
        }
2376
        return 'Exception: could not decrypt object';
2377
    }
2378
}
2379
2380
/**
2381
 * Encrypts a file.
2382
 *
2383
 * @param string $fileInName File name
2384
 * @param string $fileInPath Path to file
2385
 *
2386
 * @return array
2387
 */
2388
function encryptFile(string $fileInName, string $fileInPath): array
2389
{
2390
    if (defined('FILE_BUFFER_SIZE') === false) {
2391
        define('FILE_BUFFER_SIZE', 128 * 1024);
2392
    }
2393
2394
    // Load classes
2395
    $cipher = new Crypt_AES();
2396
2397
    // Generate an object key
2398
    $objectKey = uniqidReal(32);
2399
    // Set it as password
2400
    $cipher->setPassword($objectKey);
2401
    // Prevent against out of memory
2402
    $cipher->enableContinuousBuffer();
2403
2404
    // Encrypt the file content
2405
    $filePath = filter_var($fileInPath . '/' . $fileInName, FILTER_SANITIZE_URL);
2406
    $fileContent = file_get_contents($filePath);
2407
    $plaintext = $fileContent;
2408
    $ciphertext = $cipher->encrypt($plaintext);
2409
2410
    // Save new file
2411
    // deepcode ignore InsecureHash: is simply used to get a unique name
2412
    $hash = uniqid('', true);
2413
    $fileOut = $fileInPath . '/' . TP_FILE_PREFIX . $hash;
2414
    file_put_contents($fileOut, $ciphertext);
2415
    unlink($fileInPath . '/' . $fileInName);
2416
    return [
2417
        'fileHash' => base64_encode($hash),
2418
        'objectKey' => base64_encode($objectKey),
2419
    ];
2420
}
2421
2422
/**
2423
 * Decrypt a file.
2424
 *
2425
 * @param string $fileName File name
2426
 * @param string $filePath Path to file
2427
 * @param string $key      Key to use
2428
 *
2429
 * @return string|array
2430
 */
2431
function decryptFile(string $fileName, string $filePath, string $key): string|array
2432
{
2433
    if (! defined('FILE_BUFFER_SIZE')) {
2434
        define('FILE_BUFFER_SIZE', 128 * 1024);
2435
    }
2436
    
2437
    // Load classes
2438
    $cipher = new Crypt_AES();
2439
    $antiXSS = new AntiXSS();
2440
    
2441
    // Get file name
2442
    $safeFileName = $antiXSS->xss_clean(base64_decode($fileName));
2443
2444
    // Set the object key
2445
    $cipher->setPassword(base64_decode($key));
2446
    // Prevent against out of memory
2447
    $cipher->enableContinuousBuffer();
2448
    $cipher->disablePadding();
2449
    // Get file content
2450
    $safeFilePath = realpath($filePath . '/' . TP_FILE_PREFIX . $safeFileName);
2451
    if ($safeFilePath !== false && file_exists($safeFilePath)) {
2452
        $ciphertext = file_get_contents(filter_var($safeFilePath, FILTER_SANITIZE_URL));
2453
    } else {
2454
        // Handle the error: file doesn't exist or path is invalid
2455
        return [
2456
            'error' => true,
2457
            'message' => 'This file has not been found.',
2458
        ];
2459
    }
2460
2461
    if (WIP) error_log('DEBUG: File image url -> '.filter_var($safeFilePath, FILTER_SANITIZE_URL));
2462
2463
    // Decrypt file content and return
2464
    return base64_encode($cipher->decrypt($ciphertext));
2465
}
2466
2467
/**
2468
 * Generate a simple password
2469
 *
2470
 * @param int $length Length of string
2471
 * @param bool $symbolsincluded Allow symbols
2472
 *
2473
 * @return string
2474
 */
2475
function generateQuickPassword(int $length = 16, bool $symbolsincluded = true): string
2476
{
2477
    // Generate new user password
2478
    $small_letters = range('a', 'z');
2479
    $big_letters = range('A', 'Z');
2480
    $digits = range(0, 9);
2481
    $symbols = $symbolsincluded === true ?
2482
        ['#', '_', '-', '@', '$', '+', '!'] : [];
2483
    $res = array_merge($small_letters, $big_letters, $digits, $symbols);
2484
    $count = count($res);
2485
    // first variant
2486
2487
    $random_string = '';
2488
    for ($i = 0; $i < $length; ++$i) {
2489
        $random_string .= $res[random_int(0, $count - 1)];
2490
    }
2491
2492
    return $random_string;
2493
}
2494
2495
/**
2496
 * Permit to store the sharekey of an object for users.
2497
 *
2498
 * @param string $object_name             Type for table selection
2499
 * @param int    $post_folder_is_personal Personal
2500
 * @param int    $post_object_id          Object
2501
 * @param string $objectKey               Object key
2502
 * @param array  $SETTINGS                Teampass settings
2503
 * @param int    $user_id                 User ID if needed
2504
 * @param bool   $onlyForUser             If is TRUE, then the sharekey is only for the user
2505
 * @param bool   $deleteAll               If is TRUE, then all existing entries are deleted
2506
 * @param array  $objectKeyArray          Array of objects
2507
 * @param int    $all_users_except_id     All users except this one
2508
 * @param int    $apiUserId               API User ID
2509
 *
2510
 * @return void
2511
 */
2512
function storeUsersShareKey(
2513
    string $object_name,
2514
    int $post_folder_is_personal,
2515
    int $post_object_id,
2516
    string $objectKey,
2517
    bool $onlyForUser = false,
2518
    bool $deleteAll = true,
2519
    array $objectKeyArray = [],
2520
    int $all_users_except_id = -1,
2521
    int $apiUserId = -1
2522
): void {
2523
    
2524
    $session = SessionManager::getSession();
2525
    loadClasses('DB');
2526
2527
    // Delete existing entries for this object
2528
    if ($deleteAll === true) {
2529
        DB::delete(
2530
            $object_name,
2531
            'object_id = %i',
2532
            $post_object_id
2533
        );
2534
    }
2535
2536
    // Get the user ID
2537
    $userId = ($apiUserId === -1) ? (int) $session->get('user-id') : $apiUserId;
2538
    
2539
    // $onlyForUser is only dynamically set by external calls
2540
    if (
2541
        $onlyForUser === true || (int) $post_folder_is_personal === 1
2542
    ) {
2543
        // Only create the sharekey for a user
2544
        $user = DB::queryFirstRow(
2545
            'SELECT public_key
2546
            FROM ' . prefixTable('users') . '
2547
            WHERE id = %i
2548
            AND public_key != ""',
2549
            $userId
2550
        );
2551
2552
        if (empty($objectKey) === false) {
2553
            DB::insert(
2554
                $object_name,
2555
                [
2556
                    'object_id' => (int) $post_object_id,
2557
                    'user_id' => $userId,
2558
                    'share_key' => encryptUserObjectKey(
2559
                        $objectKey,
2560
                        $user['public_key']
2561
                    ),
2562
                ]
2563
            );
2564
        } else if (count($objectKeyArray) > 0) {
2565
            foreach ($objectKeyArray as $object) {
2566
                DB::insert(
2567
                    $object_name,
2568
                    [
2569
                        'object_id' => (int) $object['objectId'],
2570
                        'user_id' => $userId,
2571
                        'share_key' => encryptUserObjectKey(
2572
                            $object['objectKey'],
2573
                            $user['public_key']
2574
                        ),
2575
                    ]
2576
                );
2577
            }
2578
        }
2579
    } else {
2580
        // Create sharekey for each user
2581
        $user_ids = [OTV_USER_ID, SSH_USER_ID, API_USER_ID];
2582
        if ($all_users_except_id !== -1) {
2583
            array_push($user_ids, (int) $all_users_except_id);
2584
        }
2585
        $users = DB::query(
2586
            'SELECT id, public_key
2587
            FROM ' . prefixTable('users') . '
2588
            WHERE id NOT IN %li
2589
            AND public_key != ""',
2590
            $user_ids
2591
        );
2592
        //DB::debugmode(false);
2593
        foreach ($users as $user) {
2594
            // Insert in DB the new object key for this item by user
2595
            if (count($objectKeyArray) === 0) {
2596
                if (WIP === true) error_log('TEAMPASS Debug - storeUsersShareKey case1 - ' . $object_name . ' - ' . $post_object_id . ' - ' . $user['id'] . ' - ' . $objectKey);
2597
                DB::insert(
2598
                    $object_name,
2599
                    [
2600
                        'object_id' => $post_object_id,
2601
                        'user_id' => (int) $user['id'],
2602
                        'share_key' => encryptUserObjectKey(
2603
                            $objectKey,
2604
                            $user['public_key']
2605
                        ),
2606
                    ]
2607
                );
2608
            } else {
2609
                foreach ($objectKeyArray as $object) {
2610
                    if (WIP === true) error_log('TEAMPASS Debug - storeUsersShareKey case2 - ' . $object_name . ' - ' . $object['objectId'] . ' - ' . $user['id'] . ' - ' . $object['objectKey']);
2611
                    DB::insert(
2612
                        $object_name,
2613
                        [
2614
                            'object_id' => (int) $object['objectId'],
2615
                            'user_id' => (int) $user['id'],
2616
                            'share_key' => encryptUserObjectKey(
2617
                                $object['objectKey'],
2618
                                $user['public_key']
2619
                            ),
2620
                        ]
2621
                    );
2622
                }
2623
            }
2624
        }
2625
    }
2626
}
2627
2628
/**
2629
 * Is this string base64 encoded?
2630
 *
2631
 * @param string $str Encoded string?
2632
 *
2633
 * @return bool
2634
 */
2635
function isBase64(string $str): bool
2636
{
2637
    $str = (string) trim($str);
2638
    if (! isset($str[0])) {
2639
        return false;
2640
    }
2641
2642
    $base64String = (string) base64_decode($str, true);
2643
    if ($base64String && base64_encode($base64String) === $str) {
2644
        return true;
2645
    }
2646
2647
    return false;
2648
}
2649
2650
/**
2651
 * Undocumented function
2652
 *
2653
 * @param string $field Parameter
2654
 *
2655
 * @return array|bool|resource|string
2656
 */
2657
function filterString(string $field)
2658
{
2659
    // Sanitize string
2660
    $field = filter_var(trim($field), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
2661
    if (empty($field) === false) {
2662
        // Load AntiXSS
2663
        $antiXss = new AntiXSS();
2664
        // Return
2665
        return $antiXss->xss_clean($field);
2666
    }
2667
2668
    return false;
2669
}
2670
2671
/**
2672
 * CHeck if provided credentials are allowed on server
2673
 *
2674
 * @param string $login    User Login
2675
 * @param string $password User Pwd
2676
 * @param array  $SETTINGS Teampass settings
2677
 *
2678
 * @return bool
2679
 */
2680
function ldapCheckUserPassword(string $login, string $password, array $SETTINGS): bool
2681
{
2682
    // Build ldap configuration array
2683
    $config = [
2684
        // Mandatory Configuration Options
2685
        'hosts' => [$SETTINGS['ldap_hosts']],
2686
        'base_dn' => $SETTINGS['ldap_bdn'],
2687
        'username' => $SETTINGS['ldap_username'],
2688
        'password' => $SETTINGS['ldap_password'],
2689
2690
        // Optional Configuration Options
2691
        'port' => $SETTINGS['ldap_port'],
2692
        'use_ssl' => (int) $SETTINGS['ldap_ssl'] === 1 ? true : false,
2693
        'use_tls' => (int) $SETTINGS['ldap_tls'] === 1 ? true : false,
2694
        'version' => 3,
2695
        'timeout' => 5,
2696
        'follow_referrals' => false,
2697
2698
        // Custom LDAP Options
2699
        'options' => [
2700
            // See: http://php.net/ldap_set_option
2701
            LDAP_OPT_X_TLS_REQUIRE_CERT => (isset($SETTINGS['ldap_tls_certificate_check']) ? $SETTINGS['ldap_tls_certificate_check'] : LDAP_OPT_X_TLS_HARD),
2702
        ],
2703
    ];
2704
    
2705
    $connection = new Connection($config);
2706
    // Connect to LDAP
2707
    try {
2708
        $connection->connect();
2709
    } catch (\LdapRecord\Auth\BindException $e) {
2710
        $error = $e->getDetailedError();
2711
        if ($error && defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
2712
            error_log('TEAMPASS Error - LDAP - '.$error->getErrorCode()." - ".$error->getErrorMessage(). " - ".$error->getDiagnosticMessage());
2713
        }
2714
        // deepcode ignore ServerLeak: No important data is sent
2715
        echo 'An error occurred.';
2716
        return false;
2717
    }
2718
2719
    // Authenticate user
2720
    try {
2721
        if ($SETTINGS['ldap_type'] === 'ActiveDirectory') {
2722
            $connection->auth()->attempt($login, $password, $stayAuthenticated = true);
2723
        } else {
2724
            $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);
2725
        }
2726
    } catch (\LdapRecord\Auth\BindException $e) {
2727
        $error = $e->getDetailedError();
2728
        if ($error && defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
2729
            error_log('TEAMPASS Error - LDAP - '.$error->getErrorCode()." - ".$error->getErrorMessage(). " - ".$error->getDiagnosticMessage());
2730
        }
2731
        // deepcode ignore ServerLeak: No important data is sent
2732
        echo 'An error occurred.';
2733
        return false;
2734
    }
2735
2736
    return true;
2737
}
2738
2739
/**
2740
 * Removes from DB all sharekeys of this user
2741
 *
2742
 * @param int $userId User's id
2743
 * @param array   $SETTINGS Teampass settings
2744
 *
2745
 * @return bool
2746
 */
2747
function deleteUserObjetsKeys(int $userId, array $SETTINGS = []): bool
2748
{
2749
    // Load class DB
2750
    loadClasses('DB');
2751
2752
    // Remove all item sharekeys items
2753
    // expect if personal item
2754
    DB::delete(
2755
        prefixTable('sharekeys_items'),
2756
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2757
        $userId
2758
    );
2759
    // Remove all item sharekeys files
2760
    DB::delete(
2761
        prefixTable('sharekeys_files'),
2762
        'user_id = %i AND object_id NOT IN (
2763
            SELECT f.id 
2764
            FROM ' . prefixTable('items') . ' AS i 
2765
            INNER JOIN ' . prefixTable('files') . ' AS f ON f.id_item = i.id
2766
            WHERE i.perso = 1
2767
        )',
2768
        $userId
2769
    );
2770
    // Remove all item sharekeys fields
2771
    DB::delete(
2772
        prefixTable('sharekeys_fields'),
2773
        'user_id = %i AND object_id NOT IN (
2774
            SELECT c.id 
2775
            FROM ' . prefixTable('items') . ' AS i 
2776
            INNER JOIN ' . prefixTable('categories_items') . ' AS c ON c.item_id = i.id
2777
            WHERE i.perso = 1
2778
        )',
2779
        $userId
2780
    );
2781
    // Remove all item sharekeys logs
2782
    DB::delete(
2783
        prefixTable('sharekeys_logs'),
2784
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2785
        $userId
2786
    );
2787
    // Remove all item sharekeys suggestions
2788
    DB::delete(
2789
        prefixTable('sharekeys_suggestions'),
2790
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2791
        $userId
2792
    );
2793
    return false;
2794
}
2795
2796
/**
2797
 * Manage list of timezones   $SETTINGS Teampass settings
2798
 *
2799
 * @return array
2800
 */
2801
function timezone_list()
2802
{
2803
    static $timezones = null;
2804
    if ($timezones === null) {
2805
        $timezones = [];
2806
        $offsets = [];
2807
        $now = new DateTime('now', new DateTimeZone('UTC'));
2808
        foreach (DateTimeZone::listIdentifiers() as $timezone) {
2809
            $now->setTimezone(new DateTimeZone($timezone));
2810
            $offsets[] = $offset = $now->getOffset();
2811
            $timezones[$timezone] = '(' . format_GMT_offset($offset) . ') ' . format_timezone_name($timezone);
2812
        }
2813
2814
        array_multisort($offsets, $timezones);
2815
    }
2816
2817
    return $timezones;
2818
}
2819
2820
/**
2821
 * Provide timezone offset
2822
 *
2823
 * @param int $offset Timezone offset
2824
 *
2825
 * @return string
2826
 */
2827
function format_GMT_offset($offset): string
2828
{
2829
    $hours = intval($offset / 3600);
2830
    $minutes = abs(intval($offset % 3600 / 60));
2831
    return 'GMT' . ($offset ? sprintf('%+03d:%02d', $hours, $minutes) : '');
2832
}
2833
2834
/**
2835
 * Provides timezone name
2836
 *
2837
 * @param string $name Timezone name
2838
 *
2839
 * @return string
2840
 */
2841
function format_timezone_name($name): string
2842
{
2843
    $name = str_replace('/', ', ', $name);
2844
    $name = str_replace('_', ' ', $name);
2845
2846
    return str_replace('St ', 'St. ', $name);
2847
}
2848
2849
/**
2850
 * Provides info if user should use MFA based on roles
2851
 *
2852
 * @param string $userRolesIds  User roles ids
2853
 * @param string $mfaRoles      Roles for which MFA is requested
2854
 *
2855
 * @return bool
2856
 */
2857
function mfa_auth_requested_roles(string $userRolesIds, string $mfaRoles): bool
2858
{
2859
    if (empty($mfaRoles) === true) {
2860
        return true;
2861
    }
2862
2863
    $mfaRoles = array_values(json_decode($mfaRoles, true));
2864
    $userRolesIds = array_filter(explode(';', $userRolesIds));
2865
    if (count($mfaRoles) === 0 || count(array_intersect($mfaRoles, $userRolesIds)) > 0) {
2866
        return true;
2867
    }
2868
2869
    return false;
2870
}
2871
2872
/**
2873
 * Permits to clean a string for export purpose
2874
 *
2875
 * @param string $text
2876
 * @param bool $emptyCheckOnly
2877
 * 
2878
 * @return string
2879
 */
2880
function cleanStringForExport(string $text, bool $emptyCheckOnly = false): string
2881
{
2882
    if (is_null($text) === true || empty($text) === true) {
2883
        return '';
2884
    }
2885
    // only expected to check if $text was empty
2886
    elseif ($emptyCheckOnly === true) {
2887
        return $text;
2888
    }
2889
2890
    return strip_tags(
2891
        cleanString(
2892
            html_entity_decode($text, ENT_QUOTES | ENT_XHTML, 'UTF-8'),
2893
            true)
2894
        );
2895
}
2896
2897
/**
2898
 * Permits to check if user ID is valid
2899
 *
2900
 * @param integer $post_user_id
2901
 * @return bool
2902
 */
2903
function isUserIdValid($userId): bool
2904
{
2905
    if (is_null($userId) === false
2906
        && empty($userId) === false
2907
    ) {
2908
        return true;
2909
    }
2910
    return false;
2911
}
2912
2913
/**
2914
 * Check if a key exists and if its value equal the one expected
2915
 *
2916
 * @param string $key
2917
 * @param integer|string $value
2918
 * @param array $array
2919
 * 
2920
 * @return boolean
2921
 */
2922
function isKeyExistingAndEqual(
2923
    string $key,
2924
    /*PHP8 - integer|string*/$value,
2925
    array $array
2926
): bool
2927
{
2928
    if (isset($array[$key]) === true
2929
        && (is_int($value) === true ?
2930
            (int) $array[$key] === $value :
2931
            (string) $array[$key] === $value)
2932
    ) {
2933
        return true;
2934
    }
2935
    return false;
2936
}
2937
2938
/**
2939
 * Check if a variable is not set or equal to a value
2940
 *
2941
 * @param string|null $var
2942
 * @param integer|string $value
2943
 * 
2944
 * @return boolean
2945
 */
2946
function isKeyNotSetOrEqual(
2947
    /*PHP8 - string|null*/$var,
2948
    /*PHP8 - integer|string*/$value
2949
): bool
2950
{
2951
    if (isset($var) === false
2952
        || (is_int($value) === true ?
2953
            (int) $var === $value :
2954
            (string) $var === $value)
2955
    ) {
2956
        return true;
2957
    }
2958
    return false;
2959
}
2960
2961
/**
2962
 * Check if a key exists and if its value < to the one expected
2963
 *
2964
 * @param string $key
2965
 * @param integer $value
2966
 * @param array $array
2967
 * 
2968
 * @return boolean
2969
 */
2970
function isKeyExistingAndInferior(string $key, int $value, array $array): bool
2971
{
2972
    if (isset($array[$key]) === true && (int) $array[$key] < $value) {
2973
        return true;
2974
    }
2975
    return false;
2976
}
2977
2978
/**
2979
 * Check if a key exists and if its value > to the one expected
2980
 *
2981
 * @param string $key
2982
 * @param integer $value
2983
 * @param array $array
2984
 * 
2985
 * @return boolean
2986
 */
2987
function isKeyExistingAndSuperior(string $key, int $value, array $array): bool
2988
{
2989
    if (isset($array[$key]) === true && (int) $array[$key] > $value) {
2990
        return true;
2991
    }
2992
    return false;
2993
}
2994
2995
/**
2996
 * Check if values in array are set
2997
 * Return true if all set
2998
 * Return false if one of them is not set
2999
 *
3000
 * @param array $arrayOfValues
3001
 * @return boolean
3002
 */
3003
function isSetArrayOfValues(array $arrayOfValues): bool
3004
{
3005
    foreach($arrayOfValues as $value) {
3006
        if (isset($value) === false) {
3007
            return false;
3008
        }
3009
    }
3010
    return true;
3011
}
3012
3013
/**
3014
 * Check if values in array are set
3015
 * Return true if all set
3016
 * Return false if one of them is not set
3017
 *
3018
 * @param array $arrayOfValues
3019
 * @param integer|string $value
3020
 * @return boolean
3021
 */
3022
function isArrayOfVarsEqualToValue(
3023
    array $arrayOfVars,
3024
    /*PHP8 - integer|string*/$value
3025
) : bool
3026
{
3027
    foreach($arrayOfVars as $variable) {
3028
        if ($variable !== $value) {
3029
            return false;
3030
        }
3031
    }
3032
    return true;
3033
}
3034
3035
/**
3036
 * Checks if at least one variable in array is equal to value
3037
 *
3038
 * @param array $arrayOfValues
3039
 * @param integer|string $value
3040
 * @return boolean
3041
 */
3042
function isOneVarOfArrayEqualToValue(
3043
    array $arrayOfVars,
3044
    /*PHP8 - integer|string*/$value
3045
) : bool
3046
{
3047
    foreach($arrayOfVars as $variable) {
3048
        if ($variable === $value) {
3049
            return true;
3050
        }
3051
    }
3052
    return false;
3053
}
3054
3055
/**
3056
 * Checks is value is null, not set OR empty
3057
 *
3058
 * @param string|int|null $value
3059
 * @return boolean
3060
 */
3061
function isValueSetNullEmpty(string|int|null $value) : bool
3062
{
3063
    if (is_null($value) === true || empty($value) === true) {
3064
        return true;
3065
    }
3066
    return false;
3067
}
3068
3069
/**
3070
 * Checks if value is set and if empty is equal to passed boolean
3071
 *
3072
 * @param string|int $value
3073
 * @param boolean $boolean
3074
 * @return boolean
3075
 */
3076
function isValueSetEmpty($value, $boolean = true) : bool
3077
{
3078
    if (empty($value) === $boolean) {
3079
        return true;
3080
    }
3081
    return false;
3082
}
3083
3084
/**
3085
 * Ensure Complexity is translated
3086
 *
3087
 * @return void
3088
 */
3089
function defineComplexity() : void
3090
{
3091
    // Load user's language
3092
    $session = SessionManager::getSession();
3093
    $lang = new Language($session->get('user-language') ?? 'english');
3094
    
3095
    if (defined('TP_PW_COMPLEXITY') === false) {
3096
        define(
3097
            'TP_PW_COMPLEXITY',
3098
            [
3099
                TP_PW_STRENGTH_1 => array(TP_PW_STRENGTH_1, $lang->get('complex_level1'), 'fas fa-thermometer-empty text-danger'),
3100
                TP_PW_STRENGTH_2 => array(TP_PW_STRENGTH_2, $lang->get('complex_level2'), 'fas fa-thermometer-quarter text-warning'),
3101
                TP_PW_STRENGTH_3 => array(TP_PW_STRENGTH_3, $lang->get('complex_level3'), 'fas fa-thermometer-half text-warning'),
3102
                TP_PW_STRENGTH_4 => array(TP_PW_STRENGTH_4, $lang->get('complex_level4'), 'fas fa-thermometer-three-quarters text-success'),
3103
                TP_PW_STRENGTH_5 => array(TP_PW_STRENGTH_5, $lang->get('complex_level5'), 'fas fa-thermometer-full text-success'),
3104
            ]
3105
        );
3106
    }
3107
}
3108
3109
/**
3110
 * Uses Sanitizer to perform data sanitization
3111
 *
3112
 * @param array     $data
3113
 * @param array     $filters
3114
 * @return array|string
3115
 */
3116
function dataSanitizer(array $data, array $filters): array|string
3117
{
3118
    // Load Sanitizer library
3119
    $sanitizer = new Sanitizer($data, $filters);
3120
3121
    // Load AntiXSS
3122
    $antiXss = new AntiXSS();
3123
3124
    // Sanitize post and get variables
3125
    return $antiXss->xss_clean($sanitizer->sanitize());
3126
}
3127
3128
/**
3129
 * Permits to manage the cache tree for a user
3130
 *
3131
 * @param integer $user_id
3132
 * @param string $data
3133
 * @param array $SETTINGS
3134
 * @param string $field_update
3135
 * @return void
3136
 */
3137
function cacheTreeUserHandler(int $user_id, string $data, array $SETTINGS, string $field_update = '')
3138
{
3139
    // Load class DB
3140
    loadClasses('DB');
3141
3142
    // Exists ?
3143
    $userCacheId = DB::queryFirstRow(
3144
        'SELECT increment_id
3145
        FROM ' . prefixTable('cache_tree') . '
3146
        WHERE user_id = %i',
3147
        $user_id
3148
    );
3149
    
3150
    if (is_null($userCacheId) === true || count($userCacheId) === 0) {
3151
        // insert in table
3152
        DB::insert(
3153
            prefixTable('cache_tree'),
3154
            array(
3155
                'data' => $data,
3156
                'timestamp' => time(),
3157
                'user_id' => $user_id,
3158
                'visible_folders' => '',
3159
            )
3160
        );
3161
    } else {
3162
        if (empty($field_update) === true) {
3163
            DB::update(
3164
                prefixTable('cache_tree'),
3165
                [
3166
                    'timestamp' => time(),
3167
                    'data' => $data,
3168
                ],
3169
                'increment_id = %i',
3170
                $userCacheId['increment_id']
3171
            );
3172
        /* USELESS
3173
        } else {
3174
            DB::update(
3175
                prefixTable('cache_tree'),
3176
                [
3177
                    $field_update => $data,
3178
                ],
3179
                'increment_id = %i',
3180
                $userCacheId['increment_id']
3181
            );*/
3182
        }
3183
    }
3184
}
3185
3186
/**
3187
 * Permits to calculate a %
3188
 *
3189
 * @param float $nombre
3190
 * @param float $total
3191
 * @param float $pourcentage
3192
 * @return float
3193
 */
3194
function pourcentage(float $nombre, float $total, float $pourcentage): float
3195
{ 
3196
    $resultat = ($nombre/$total) * $pourcentage;
3197
    return round($resultat);
3198
}
3199
3200
/**
3201
 * Load the folders list from the cache
3202
 *
3203
 * @param string $fieldName
3204
 * @param string $sessionName
3205
 * @param boolean $forceRefresh
3206
 * @return array
3207
 */
3208
function loadFoldersListByCache(
3209
    string $fieldName,
3210
    string $sessionName,
3211
    bool $forceRefresh = false
3212
): array
3213
{
3214
    // Case when refresh is EXPECTED / MANDATORY
3215
    if ($forceRefresh === true) {
3216
        return [
3217
            'state' => false,
3218
            'data' => [],
3219
        ];
3220
    }
3221
    
3222
    $session = SessionManager::getSession();
3223
3224
    // Get last folder update
3225
    $lastFolderChange = DB::queryFirstRow(
3226
        'SELECT valeur FROM ' . prefixTable('misc') . '
3227
        WHERE type = %s AND intitule = %s',
3228
        'timestamp',
3229
        'last_folder_change'
3230
    );
3231
    if (DB::count() === 0) {
3232
        $lastFolderChange['valeur'] = 0;
3233
    }
3234
3235
    // Case when an update in the tree has been done
3236
    // Refresh is then mandatory
3237
    if ((int) $lastFolderChange['valeur'] > (int) (null !== $session->get('user-tree_last_refresh_timestamp') ? $session->get('user-tree_last_refresh_timestamp') : 0)) {
3238
        return [
3239
            'state' => false,
3240
            'data' => [],
3241
        ];
3242
    }
3243
3244
    // Does this user has the tree structure in session?
3245
    // If yes then use it
3246
    if (count(null !== $session->get('user-folders_list') ? $session->get('user-folders_list') : []) > 0) {
3247
        return [
3248
            'state' => true,
3249
            'data' => json_encode($session->get('user-folders_list')[0]),
3250
            'extra' => 'to_be_parsed',
3251
        ];
3252
    }
3253
    
3254
    // Does this user has a tree cache
3255
    $userCacheTree = DB::queryFirstRow(
3256
        'SELECT '.$fieldName.'
3257
        FROM ' . prefixTable('cache_tree') . '
3258
        WHERE user_id = %i',
3259
        $session->get('user-id')
3260
    );
3261
    if (empty($userCacheTree[$fieldName]) === false && $userCacheTree[$fieldName] !== '[]') {
3262
        SessionManager::addRemoveFromSessionAssociativeArray(
3263
            'user-folders_list',
3264
            [$userCacheTree[$fieldName]],
3265
            'add'
3266
        );
3267
        return [
3268
            'state' => true,
3269
            'data' => $userCacheTree[$fieldName],
3270
            'extra' => '',
3271
        ];
3272
    }
3273
3274
    return [
3275
        'state' => false,
3276
        'data' => [],
3277
    ];
3278
}
3279
3280
3281
/**
3282
 * Permits to refresh the categories of folders
3283
 *
3284
 * @param array $folderIds
3285
 * @return void
3286
 */
3287
function handleFoldersCategories(
3288
    array $folderIds
3289
)
3290
{
3291
    // Load class DB
3292
    loadClasses('DB');
3293
3294
    $arr_data = array();
3295
3296
    // force full list of folders
3297
    if (count($folderIds) === 0) {
3298
        $folderIds = DB::queryFirstColumn(
3299
            'SELECT id
3300
            FROM ' . prefixTable('nested_tree') . '
3301
            WHERE personal_folder=%i',
3302
            0
3303
        );
3304
    }
3305
3306
    // Get complexity
3307
    defineComplexity();
3308
3309
    // update
3310
    foreach ($folderIds as $folder) {
3311
        // Do we have Categories
3312
        // get list of associated Categories
3313
        $arrCatList = array();
3314
        $rows_tmp = DB::query(
3315
            'SELECT c.id, c.title, c.level, c.type, c.masked, c.order, c.encrypted_data, c.role_visibility, c.is_mandatory,
3316
            f.id_category AS category_id
3317
            FROM ' . prefixTable('categories_folders') . ' AS f
3318
            INNER JOIN ' . prefixTable('categories') . ' AS c ON (f.id_category = c.parent_id)
3319
            WHERE id_folder=%i',
3320
            $folder
3321
        );
3322
        if (DB::count() > 0) {
3323
            foreach ($rows_tmp as $row) {
3324
                $arrCatList[$row['id']] = array(
3325
                    'id' => $row['id'],
3326
                    'title' => $row['title'],
3327
                    'level' => $row['level'],
3328
                    'type' => $row['type'],
3329
                    'masked' => $row['masked'],
3330
                    'order' => $row['order'],
3331
                    'encrypted_data' => $row['encrypted_data'],
3332
                    'role_visibility' => $row['role_visibility'],
3333
                    'is_mandatory' => $row['is_mandatory'],
3334
                    'category_id' => $row['category_id'],
3335
                );
3336
            }
3337
        }
3338
        $arr_data['categories'] = $arrCatList;
3339
3340
        // Now get complexity
3341
        $valTemp = '';
3342
        $data = DB::queryFirstRow(
3343
            'SELECT valeur
3344
            FROM ' . prefixTable('misc') . '
3345
            WHERE type = %s AND intitule=%i',
3346
            'complex',
3347
            $folder
3348
        );
3349
        if (DB::count() > 0 && empty($data['valeur']) === false) {
3350
            $valTemp = array(
3351
                'value' => $data['valeur'],
3352
                'text' => TP_PW_COMPLEXITY[$data['valeur']][1],
3353
            );
3354
        }
3355
        $arr_data['complexity'] = $valTemp;
3356
3357
        // Now get Roles
3358
        $valTemp = '';
3359
        $rows_tmp = DB::query(
3360
            'SELECT t.title
3361
            FROM ' . prefixTable('roles_values') . ' as v
3362
            INNER JOIN ' . prefixTable('roles_title') . ' as t ON (v.role_id = t.id)
3363
            WHERE v.folder_id = %i
3364
            GROUP BY title',
3365
            $folder
3366
        );
3367
        foreach ($rows_tmp as $record) {
3368
            $valTemp .= (empty($valTemp) === true ? '' : ' - ') . $record['title'];
3369
        }
3370
        $arr_data['visibilityRoles'] = $valTemp;
3371
3372
        // now save in DB
3373
        DB::update(
3374
            prefixTable('nested_tree'),
3375
            array(
3376
                'categories' => json_encode($arr_data),
3377
            ),
3378
            'id = %i',
3379
            $folder
3380
        );
3381
    }
3382
}
3383
3384
/**
3385
 * List all users that have specific roles
3386
 *
3387
 * @param array $roles
3388
 * @return array
3389
 */
3390
function getUsersWithRoles(
3391
    array $roles
3392
): array
3393
{
3394
    $session = SessionManager::getSession();
3395
    $arrUsers = array();
3396
3397
    foreach ($roles as $role) {
3398
        // loop on users and check if user has this role
3399
        $rows = DB::query(
3400
            'SELECT id, fonction_id
3401
            FROM ' . prefixTable('users') . '
3402
            WHERE id != %i AND admin = 0 AND fonction_id IS NOT NULL AND fonction_id != ""',
3403
            $session->get('user-id')
3404
        );
3405
        foreach ($rows as $user) {
3406
            $userRoles = is_null($user['fonction_id']) === false && empty($user['fonction_id']) === false ? explode(';', $user['fonction_id']) : [];
3407
            if (in_array($role, $userRoles, true) === true) {
3408
                array_push($arrUsers, $user['id']);
3409
            }
3410
        }
3411
    }
3412
3413
    return $arrUsers;
3414
}
3415
3416
3417
/**
3418
 * Get all users informations
3419
 *
3420
 * @param integer $userId
3421
 * @return array
3422
 */
3423
function getFullUserInfos(
3424
    int $userId
3425
): array
3426
{
3427
    if (empty($userId) === true) {
3428
        return array();
3429
    }
3430
3431
    $val = DB::queryFirstRow(
3432
        'SELECT *
3433
        FROM ' . prefixTable('users') . '
3434
        WHERE id = %i',
3435
        $userId
3436
    );
3437
3438
    return $val;
3439
}
3440
3441
/**
3442
 * Is required an upgrade
3443
 *
3444
 * @return boolean
3445
 */
3446
function upgradeRequired(): bool
3447
{
3448
    // Get settings.php
3449
    include_once __DIR__. '/../includes/config/settings.php';
3450
3451
    // Get timestamp in DB
3452
    $val = DB::queryFirstRow(
3453
        'SELECT valeur
3454
        FROM ' . prefixTable('misc') . '
3455
        WHERE type = %s AND intitule = %s',
3456
        'admin',
3457
        'upgrade_timestamp'
3458
    );
3459
3460
    // Check if upgrade is required
3461
    return (
3462
        is_null($val) || count($val) === 0 || !defined('UPGRADE_MIN_DATE') || 
3463
        empty($val['valeur']) || (int) $val['valeur'] < (int) UPGRADE_MIN_DATE
3464
    );
3465
}
3466
3467
/**
3468
 * Permits to change the user keys on his demand
3469
 *
3470
 * @param int $userId
3471
 * @param string $passwordClear
3472
 * @param int $nbItemsToTreat
3473
 * @param string $encryptionKey
3474
 * @param bool $deleteExistingKeys
3475
 * @param bool $sendEmailToUser
3476
 * @param bool $encryptWithUserPassword
3477
 * @param bool $generate_user_new_password
3478
 * @param string $emailBody
3479
 * @param bool $user_self_change
3480
 * @param string $recovery_public_key
3481
 * @param string $recovery_private_key
3482
 * @return string
3483
 */
3484
function handleUserKeys(
3485
    int $userId,
3486
    string $passwordClear,
3487
    int $nbItemsToTreat,
3488
    string $encryptionKey = '',
3489
    bool $deleteExistingKeys = false,
3490
    bool $sendEmailToUser = true,
3491
    bool $encryptWithUserPassword = false,
3492
    bool $generate_user_new_password = false,
3493
    string $emailBody = '',
3494
    bool $user_self_change = false,
3495
    string $recovery_public_key = '',
3496
    string $recovery_private_key = ''
3497
): string {
3498
    try {
3499
        $session = SessionManager::getSession();
3500
        $lang = new Language($session->get('user-language') ?? 'english');
3501
3502
        error_log('TEAMPASS Debug - handleUserKeys - userId: ' . $userId . ' - generate_user_new_password: ' . ($generate_user_new_password ? 'true' : 'false'));
3503
        // Validate user existence
3504
        $userTP = validateUserExistence($userId);
3505
        if ($userTP === null) {
3506
            return prepareErrorResponse($lang->get('user_not_exists'));
3507
        }
3508
3509
        // Handle password generation if needed
3510
        $passwordClear = handlePasswordGeneration($passwordClear, $generate_user_new_password);
3511
3512
        // Validate password
3513
        if (!validatePassword($passwordClear, $lang)) {
3514
            return prepareErrorResponse($lang->get('pw_hash_not_correct'));
3515
        }
3516
3517
        // Validate recovery keys if provided
3518
        if (!empty($recovery_public_key) && !empty($recovery_private_key)) {
3519
            if (!validateRecoveryKeys($recovery_public_key, $recovery_private_key, $lang)) {
3520
                return prepareErrorResponse($lang->get('pw_encryption_error'));
3521
            }
3522
        }
3523
3524
        // Generate user keys
3525
        $userKeys = generateUserKeysWrapper(
3526
            $passwordClear,
3527
            $user_self_change,
3528
            $recovery_public_key,
3529
            $recovery_private_key
3530
        );
3531
3532
        // Save keys to database
3533
        if (!saveUserKeysToDatabase($userId, $passwordClear, $userKeys)) {
3534
            return prepareErrorResponse($lang->get('db_error'));
3535
        }
3536
3537
        // Prepare encryption key
3538
        $finalEncryptionKey = prepareEncryptionKey($encryptionKey, $passwordClear, $encryptWithUserPassword);
3539
3540
        // Create background process
3541
        $processId = createBackgroundProcess(
3542
            $userId,
3543
            $passwordClear,
3544
            $finalEncryptionKey,
3545
            $userTP,
3546
            $sendEmailToUser,
3547
            $emailBody,
3548
            $user_self_change,
3549
            $nbItemsToTreat,
3550
            $encryptWithUserPassword,
3551
            $lang
3552
        );
3553
3554
        // Handle existing keys deletion if needed
3555
        if ($deleteExistingKeys) {
3556
            deleteExistingUserKeys($userId);
3557
        }
3558
3559
        // Create user tasks
3560
        createUserTasks($processId, $nbItemsToTreat);
3561
3562
        // Update user status
3563
        updateUserStatus($userId, $processId);
3564
3565
        return prepareSuccessResponse([
3566
            'message' => '',
3567
            'user_password' => $generate_user_new_password ? $passwordClear : '',
3568
        ]);
3569
3570
    } catch (Exception $e) {
3571
        return prepareErrorResponse($e->getMessage());
3572
    }
3573
}
3574
3575
/**
3576
 * Prepares the encryption key based on parameters
3577
 */
3578
function prepareEncryptionKey(string $encryptionKey, string $passwordClear, bool $encryptWithUserPassword): string {
3579
    if (!empty($encryptionKey)) {
3580
        return $encryptionKey;
3581
    }
3582
    return $encryptWithUserPassword ? $passwordClear : uniqidReal(20);
3583
}
3584
3585
/**
3586
 * Validates user existence in the database
3587
 */
3588
function validateUserExistence(int $userId): ?array {
3589
    $userTP = DB::queryFirstRow(
3590
        'SELECT pw, public_key, private_key
3591
        FROM ' . prefixTable('users') . '
3592
        WHERE id = %i',
3593
        TP_USER_ID
3594
    );
3595
    return DB::count() === 0 ? null : $userTP;
3596
}
3597
3598
/**
3599
 * Handles password generation if needed
3600
 */
3601
function handlePasswordGeneration(string $passwordClear, bool $generateNewPassword): string {
3602
    if ($generateNewPassword) {
3603
        $passwordManager = new PasswordManager();
3604
        $newPassword = $passwordManager->generatePassword(20, false, true, true, false, true);
3605
        return $newPassword;
3606
    }
3607
    return $passwordClear;
3608
}
3609
3610
/**
3611
 * Validates the password by hashing and verifying
3612
 */
3613
function validatePassword(string $passwordClear, Language $lang): bool {
3614
    $passwordManager = new PasswordManager();
3615
    $hashedPassword = $passwordManager->hashPassword($passwordClear);
3616
    return $passwordManager->verifyPassword($hashedPassword, $passwordClear);
3617
}
3618
3619
/**
3620
 * Validates recovery keys by testing encryption/decryption
3621
 */
3622
function validateRecoveryKeys(string $publicKey, string $privateKey, Language $lang): bool {
3623
    try {
3624
        $random_str = generateQuickPassword(12, false);
3625
        $encrypted = encryptUserObjectKey($random_str, $publicKey);
3626
        $decrypted = decryptUserObjectKey($encrypted, $privateKey);
3627
        return $decrypted === $random_str;
3628
    } catch (Exception $e) {
3629
        if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
3630
            error_log('ERROR: User validation - '.$e->getMessage());
3631
        }
3632
        return false;
3633
    }
3634
}
3635
3636
/**
3637
 * Generates user keys based on parameters
3638
 */
3639
function generateUserKeysWrapper(
3640
    string $passwordClear,
3641
    bool $userSelfChange,
3642
    string $recoveryPublicKey,
3643
    string $recoveryPrivateKey
3644
): array {
3645
    if ($userSelfChange && !empty($recoveryPublicKey) && !empty($recoveryPrivateKey)) {
3646
        return [
3647
            'public_key' => $recoveryPublicKey,
3648
            'private_key_clear' => $recoveryPrivateKey,
3649
            'private_key' => encryptPrivateKey($passwordClear, $recoveryPrivateKey),
3650
        ];
3651
    }
3652
    return generateUserKeys($passwordClear);
3653
}
3654
3655
/**
3656
 * Saves user keys to database and updates session if needed
3657
 */
3658
function saveUserKeysToDatabase(
3659
    int $userId,
3660
    string $passwordClear,
3661
    array $userKeys
3662
): bool {
3663
    $session = SessionManager::getSession();
3664
    $passwordManager = new PasswordManager();
3665
    $affectedRows  = DB::update(
3666
        prefixTable('users'),
3667
        [
3668
            'pw' => $passwordManager->hashPassword($passwordClear),
3669
            'public_key' => $userKeys['public_key'],
3670
            'private_key' => $userKeys['private_key'],
3671
            'keys_recovery_time' => NULL,
3672
        ],
3673
        'id=%i',
3674
        $userId
3675
    );
3676
    // Convert the result to boolean (true if at least one row was updated)
3677
    $success = $affectedRows > 0;
3678
3679
    // Update session if needed
3680
    if ($userId === $session->get('user-id')) {
3681
        $session->set('user-private_key', $userKeys['private_key_clear'] ?? '');
3682
        $session->set('user-public_key', $userKeys['public_key']);
3683
        $session->set('user-keys_recovery_time', NULL);
3684
    }
3685
3686
    return $success;
3687
}
3688
3689
/**
3690
 * Creates the background process for key generation
3691
 */
3692
function createBackgroundProcess(
3693
    int $userId,
3694
    string $passwordClear,
3695
    string $encryptionKey,
3696
    array $userTP,
3697
    bool $sendEmailToUser,
3698
    string $emailBody,
3699
    bool $userSelfChange,
3700
    int $nbItemsToTreat,
3701
    bool $encryptWithUserPassword,
3702
    Language $lang
3703
): int {
3704
    // Insert background task
3705
    DB::insert(
3706
        prefixTable('background_tasks'),
3707
        [
3708
            'created_at' => time(),
3709
            'process_type' => 'create_user_keys',
3710
            'arguments' => json_encode([
3711
                'new_user_id' => $userId,
3712
                'new_user_pwd' => cryption($passwordClear, '', 'encrypt')['string'],
3713
                'new_user_code' => cryption($encryptionKey, '', 'encrypt')['string'],
3714
                'owner_id' => (int) TP_USER_ID,
3715
                'creator_pwd' => $userTP['pw'],
3716
                'send_email' => $sendEmailToUser ? 1 : 0,
3717
                'otp_provided_new_value' => 1,
3718
                'email_body' => empty($emailBody) ? '' : $lang->get($emailBody),
3719
                'user_self_change' => $userSelfChange ? 1 : 0,
3720
            ]),
3721
        ]
3722
    );
3723
3724
    return DB::insertId();
3725
}
3726
3727
/**
3728
 * Updates the user's status in the database
3729
 */
3730
function updateUserStatus(int $userId, int $processId): void {
3731
    DB::update(
3732
        prefixTable('users'),
3733
        [
3734
            'is_ready_for_usage' => 0,
3735
            'otp_provided' => 1,
3736
            'ongoing_process_id' => $processId,
3737
            'special' => 'generate-keys',
3738
        ],
3739
        'id=%i',
3740
        $userId
3741
    );
3742
}
3743
3744
/**
3745
 * Deletes existing user keys
3746
 */
3747
function deleteExistingUserKeys(int $userId): void {
3748
    deleteUserObjetsKeys($userId);
3749
}
3750
3751
3752
/**
3753
 * Permits to generate a new password for a user
3754
 *
3755
 * @param integer $processId
3756
 * @param integer $nbItemsToTreat
3757
 * @return void
3758
 
3759
 */
3760
function createUserTasks($processId, $nbItemsToTreat): void
3761
{
3762
    // Create subtask for step 0
3763
    DB::insert(
3764
        prefixTable('background_subtasks'),
3765
        array(
3766
            'task_id' => $processId,
3767
            'created_at' => time(),
3768
            'task' => json_encode([
3769
                'step' => 'step0',
3770
                'index' => 0,
3771
                'nb' => $nbItemsToTreat,
3772
            ]),
3773
        )
3774
    );
3775
3776
    // Prepare the subtask queries
3777
    $queries = [
3778
        'step20' => 'SELECT * FROM ' . prefixTable('items'),
3779
3780
        'step30' => 'SELECT * FROM ' . prefixTable('log_items') . 
3781
                    ' WHERE raison LIKE "at_pw :%" AND encryption_type = "teampass_aes"',
3782
3783
        'step40' => 'SELECT * FROM ' . prefixTable('categories_items') . 
3784
                    ' WHERE encryption_type = "teampass_aes"',
3785
3786
        'step50' => 'SELECT * FROM ' . prefixTable('suggestion'),
3787
3788
        'step60' => 'SELECT * FROM ' . prefixTable('files') . ' AS f
3789
                        INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
3790
                        WHERE f.status = "' . TP_ENCRYPTION_NAME . '"'
3791
    ];
3792
3793
    // Perform loop on $queries to create sub-tasks
3794
    foreach ($queries as $step => $query) {
3795
        DB::query($query);
3796
        createAllSubTasks($step, DB::count(), $nbItemsToTreat, $processId);
3797
    }
3798
3799
    // Create subtask for step 99
3800
    DB::insert(
3801
        prefixTable('background_subtasks'),
3802
        array(
3803
            'task_id' => $processId,
3804
            'created_at' => time(),
3805
            'task' => json_encode([
3806
                'step' => 'step99',
3807
            ]),
3808
        )
3809
    );
3810
}
3811
3812
/**
3813
 * Create all subtasks for a given action
3814
 * @param string $action The action to be performed
3815
 * @param int $totalElements Total number of elements to process
3816
 * @param int $elementsPerIteration Number of elements per iteration
3817
 * @param int $taskId The ID of the task
3818
 */
3819
function createAllSubTasks($action, $totalElements, $elementsPerIteration, $taskId) {
3820
    // Calculate the number of iterations
3821
    $iterations = ceil($totalElements / $elementsPerIteration);
3822
3823
    // Create the subtasks
3824
    for ($i = 0; $i < $iterations; $i++) {
3825
        DB::insert(prefixTable('background_subtasks'), [
3826
            'task_id' => $taskId,
3827
            'created_at' => time(),
3828
            'task' => json_encode([
3829
                "step" => $action,
3830
                "index" => $i * $elementsPerIteration,
3831
                "nb" => $elementsPerIteration,
3832
            ]),
3833
        ]);
3834
    }
3835
}
3836
3837
/**
3838
 * Permeits to check the consistency of date versus columns definition
3839
 *
3840
 * @param string $table
3841
 * @param array $dataFields
3842
 * @return array
3843
 */
3844
function validateDataFields(
3845
    string $table,
3846
    array $dataFields
3847
): array
3848
{
3849
    // Get table structure
3850
    $result = DB::query(
3851
        "SELECT `COLUMN_NAME`, `CHARACTER_MAXIMUM_LENGTH` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%l' AND TABLE_NAME = '%l';",
3852
        DB_NAME,
3853
        $table
3854
    );
3855
3856
    foreach ($result as $row) {
3857
        $field = $row['COLUMN_NAME'];
3858
        $maxLength = is_null($row['CHARACTER_MAXIMUM_LENGTH']) === false ? (int) $row['CHARACTER_MAXIMUM_LENGTH'] : '';
3859
3860
        if (isset($dataFields[$field]) === true && is_array($dataFields[$field]) === false && empty($maxLength) === false) {
3861
            if (strlen((string) $dataFields[$field]) > $maxLength) {
3862
                return [
3863
                    'state' => false,
3864
                    'field' => $field,
3865
                    'maxLength' => $maxLength,
3866
                    'currentLength' => strlen((string) $dataFields[$field]),
3867
                ];
3868
            }
3869
        }
3870
    }
3871
    
3872
    return [
3873
        'state' => true,
3874
        'message' => '',
3875
    ];
3876
}
3877
3878
/**
3879
 * Adapt special characters sanitized during filter_var with option FILTER_SANITIZE_SPECIAL_CHARS operation
3880
 *
3881
 * @param string $string
3882
 * @return string
3883
 */
3884
function filterVarBack(string $string): string
3885
{
3886
    $arr = [
3887
        '&#060;' => '<',
3888
        '&#062;' => '>',
3889
        '&#034;' => '"',
3890
        '&#039;' => "'",
3891
        '&#038;' => '&',
3892
    ];
3893
3894
    foreach ($arr as $key => $value) {
3895
        $string = str_replace($key, $value, $string);
3896
    }
3897
3898
    return $string;
3899
}
3900
3901
/**
3902
 * 
3903
 */
3904
function storeTask(
3905
    string $taskName,
3906
    int $user_id,
3907
    int $is_personal_folder,
3908
    int $folder_destination_id,
3909
    int $item_id,
3910
    string $object_keys,
3911
    array $fields_keys = [],
3912
    array $files_keys = []
3913
)
3914
{
3915
    if (in_array($taskName, ['item_copy', 'new_item', 'update_item'])) {
3916
        // Create process
3917
        DB::insert(
3918
            prefixTable('background_tasks'),
3919
            array(
3920
                'created_at' => time(),
3921
                'process_type' => $taskName,
3922
                'arguments' => json_encode([
3923
                    'item_id' => $item_id,
3924
                    'object_key' => $object_keys,
3925
                ]),
3926
                'item_id' => $item_id,
3927
            )
3928
        );
3929
        $processId = DB::insertId();
3930
3931
        // Create tasks
3932
        // 1- Create password sharekeys for users of this new ITEM
3933
        DB::insert(
3934
            prefixTable('background_subtasks'),
3935
            array(
3936
                'task_id' => $processId,
3937
                'created_at' => time(),
3938
                'task' => json_encode([
3939
                    'step' => 'create_users_pwd_key',
3940
                    'index' => 0,
3941
                ]),
3942
            )
3943
        );
3944
3945
        // 2- Create fields sharekeys for users of this new ITEM
3946
        DB::insert(
3947
            prefixTable('background_subtasks'),
3948
            array(
3949
                'task_id' => $processId,
3950
                'created_at' => time(),
3951
                'task' => json_encode([
3952
                    'step' => 'create_users_fields_key',
3953
                    'index' => 0,
3954
                    'fields_keys' => $fields_keys,
3955
                ]),
3956
            )
3957
        );
3958
3959
        // 3- Create files sharekeys for users of this new ITEM
3960
        DB::insert(
3961
            prefixTable('background_subtasks'),
3962
            array(
3963
                'task_id' => $processId,
3964
                'created_at' => time(),
3965
                'task' => json_encode([
3966
                    'step' => 'create_users_files_key',
3967
                    'index' => 0,
3968
                    'files_keys' => $files_keys,
3969
                ]),
3970
            )
3971
        );
3972
    }
3973
}
3974
3975
/**
3976
 * 
3977
 */
3978
function createTaskForItem(
3979
    string $processType,
3980
    string|array $taskName,
3981
    int $itemId,
3982
    int $userId,
3983
    string $objectKey,
3984
    int $parentId = -1,
3985
    array $fields_keys = [],
3986
    array $files_keys = []
3987
)
3988
{
3989
    // 1- Create main process
3990
    // ---
3991
    
3992
    // Create process
3993
    DB::insert(
3994
        prefixTable('background_tasks'),
3995
        array(
3996
            'created_at' => time(),
3997
            'process_type' => $processType,
3998
            'arguments' => json_encode([
3999
                'all_users_except_id' => (int) $userId,
4000
                'item_id' => (int) $itemId,
4001
                'object_key' => $objectKey,
4002
                'author' => (int) $userId,
4003
            ]),
4004
            'item_id' => (int) $parentId !== -1 ?  $parentId : null,
4005
        )
4006
    );
4007
    $processId = DB::insertId();
4008
4009
    // 2- Create expected tasks
4010
    // ---
4011
    if (is_array($taskName) === false) {
0 ignored issues
show
introduced by
The condition is_array($taskName) === false is always false.
Loading history...
4012
        $taskName = [$taskName];
4013
    }
4014
    foreach($taskName as $task) {
4015
        if (WIP === true) error_log('createTaskForItem - task: '.$task);
4016
        switch ($task) {
4017
            case 'item_password':
4018
                
4019
                DB::insert(
4020
                    prefixTable('background_subtasks'),
4021
                    array(
4022
                        'task_id' => $processId,
4023
                        'created_at' => time(),
4024
                        'task' => json_encode([
4025
                            'step' => 'create_users_pwd_key',
4026
                            'index' => 0,
4027
                        ]),
4028
                    )
4029
                );
4030
4031
                break;
4032
            case 'item_field':
4033
                
4034
                DB::insert(
4035
                    prefixTable('background_subtasks'),
4036
                    array(
4037
                        'task_id' => $processId,
4038
                        'created_at' => time(),
4039
                        'task' => json_encode([
4040
                            'step' => 'create_users_fields_key',
4041
                            'index' => 0,
4042
                            'fields_keys' => $fields_keys,
4043
                        ]),
4044
                    )
4045
                );
4046
4047
                break;
4048
            case 'item_file':
4049
4050
                DB::insert(
4051
                    prefixTable('background_subtasks'),
4052
                    array(
4053
                        'task_id' => $processId,
4054
                        'created_at' => time(),
4055
                        'task' => json_encode([
4056
                            'step' => 'create_users_files_key',
4057
                            'index' => 0,
4058
                            'fields_keys' => $files_keys,
4059
                        ]),
4060
                    )
4061
                );
4062
                break;
4063
            default:
4064
                # code...
4065
                break;
4066
        }
4067
    }
4068
}
4069
4070
4071
function deleteProcessAndRelatedTasks(int $processId)
4072
{
4073
    // Delete process
4074
    DB::delete(
4075
        prefixTable('background_tasks'),
4076
        'id=%i',
4077
        $processId
4078
    );
4079
4080
    // Delete tasks
4081
    DB::delete(
4082
        prefixTable('background_subtasks'),
4083
        'task_id=%i',
4084
        $processId
4085
    );
4086
4087
}
4088
4089
/**
4090
 * Return PHP binary path
4091
 *
4092
 * @return string
4093
 */
4094
function getPHPBinary(): string
4095
{
4096
    // Get PHP binary path
4097
    $phpBinaryFinder = new PhpExecutableFinder();
4098
    $phpBinaryPath = $phpBinaryFinder->find();
4099
    return $phpBinaryPath === false ? 'false' : $phpBinaryPath;
4100
}
4101
4102
4103
4104
/**
4105
 * Delete unnecessary keys for personal items
4106
 *
4107
 * @param boolean $allUsers
4108
 * @param integer $user_id
4109
 * @return void
4110
 */
4111
function purgeUnnecessaryKeys(bool $allUsers = true, int $user_id=0)
4112
{
4113
    if ($allUsers === true) {
4114
        // Load class DB
4115
        if (class_exists('DB') === false) {
4116
            loadClasses('DB');
4117
        }
4118
4119
        $users = DB::query(
4120
            'SELECT id
4121
            FROM ' . prefixTable('users') . '
4122
            WHERE id NOT IN ('.OTV_USER_ID.', '.TP_USER_ID.', '.SSH_USER_ID.', '.API_USER_ID.')
4123
            ORDER BY login ASC'
4124
        );
4125
        foreach ($users as $user) {
4126
            purgeUnnecessaryKeysForUser((int) $user['id']);
4127
        }
4128
    } else {
4129
        purgeUnnecessaryKeysForUser((int) $user_id);
4130
    }
4131
}
4132
4133
/**
4134
 * Delete unnecessary keys for personal items
4135
 *
4136
 * @param integer $user_id
4137
 * @return void
4138
 */
4139
function purgeUnnecessaryKeysForUser(int $user_id=0)
4140
{
4141
    if ($user_id === 0) {
4142
        return;
4143
    }
4144
4145
    // Load class DB
4146
    loadClasses('DB');
4147
4148
    $personalItems = DB::queryFirstColumn(
4149
        'SELECT id
4150
        FROM ' . prefixTable('items') . ' AS i
4151
        INNER JOIN ' . prefixTable('log_items') . ' AS li ON li.id_item = i.id
4152
        WHERE i.perso = 1 AND li.action = "at_creation" AND li.id_user IN (%i, '.TP_USER_ID.')',
4153
        $user_id
4154
    );
4155
    if (count($personalItems) > 0) {
4156
        // Item keys
4157
        DB::delete(
4158
            prefixTable('sharekeys_items'),
4159
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4160
            $personalItems,
4161
            $user_id
4162
        );
4163
        // Files keys
4164
        DB::delete(
4165
            prefixTable('sharekeys_files'),
4166
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4167
            $personalItems,
4168
            $user_id
4169
        );
4170
        // Fields keys
4171
        DB::delete(
4172
            prefixTable('sharekeys_fields'),
4173
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4174
            $personalItems,
4175
            $user_id
4176
        );
4177
        // Logs keys
4178
        DB::delete(
4179
            prefixTable('sharekeys_logs'),
4180
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4181
            $personalItems,
4182
            $user_id
4183
        );
4184
    }
4185
}
4186
4187
/**
4188
 * Generate recovery keys file
4189
 *
4190
 * @param integer $userId
4191
 * @param array $SETTINGS
4192
 * @return string
4193
 */
4194
function handleUserRecoveryKeysDownload(int $userId, array $SETTINGS):string
4195
{
4196
    $session = SessionManager::getSession();
4197
    // Check if user exists
4198
    $userInfo = DB::queryFirstRow(
4199
        'SELECT login
4200
        FROM ' . prefixTable('users') . '
4201
        WHERE id = %i',
4202
        $userId
4203
    );
4204
4205
    if (DB::count() > 0) {
4206
        $now = (int) time();
4207
        // Prepare file content
4208
        $export_value = file_get_contents(__DIR__."/../includes/core/teampass_ascii.txt")."\n".
4209
            "Generation date: ".date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now)."\n\n".
4210
            "RECOVERY KEYS - Not to be shared - To be store safely\n\n".
4211
            "Public Key:\n".$session->get('user-public_key')."\n\n".
4212
            "Private Key:\n".$session->get('user-private_key')."\n\n";
4213
4214
        // Update user's keys_recovery_time
4215
        DB::update(
4216
            prefixTable('users'),
4217
            [
4218
                'keys_recovery_time' => $now,
4219
            ],
4220
            'id=%i',
4221
            $userId
4222
        );
4223
        $session->set('user-keys_recovery_time', $now);
4224
4225
        //Log into DB the user's disconnection
4226
        logEvents($SETTINGS, 'user_mngt', 'at_user_keys_download', (string) $userId, $userInfo['login']);
4227
        
4228
        // Return data
4229
        return prepareExchangedData(
4230
            array(
4231
                'error' => false,
4232
                'datetime' => date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now),
4233
                'timestamp' => $now,
4234
                'content' => base64_encode($export_value),
4235
                'login' => $userInfo['login'],
4236
            ),
4237
            'encode'
4238
        );
4239
    }
4240
4241
    return prepareExchangedData(
4242
        array(
4243
            'error' => true,
4244
            'datetime' => '',
4245
        ),
4246
        'encode'
4247
    );
4248
}
4249
4250
/**
4251
 * Permits to load expected classes
4252
 *
4253
 * @param string $className
4254
 * @return void
4255
 */
4256
function loadClasses(string $className = ''): void
4257
{
4258
    require_once __DIR__. '/../includes/config/include.php';
4259
    require_once __DIR__. '/../includes/config/settings.php';
4260
    require_once __DIR__.'/../vendor/autoload.php';
4261
4262
    if (defined('DB_PASSWD_CLEAR') === false) {
4263
        define('DB_PASSWD_CLEAR', defuseReturnDecrypted(DB_PASSWD));
4264
    }
4265
4266
    if (empty($className) === false) {
4267
        // Load class DB
4268
        if ((string) $className === 'DB') {
4269
            //Connect to DB
4270
            DB::$host = DB_HOST;
4271
            DB::$user = DB_USER;
4272
            DB::$password = DB_PASSWD_CLEAR;
4273
            DB::$dbName = DB_NAME;
4274
            DB::$port = DB_PORT;
4275
            DB::$encoding = DB_ENCODING;
4276
            DB::$ssl = DB_SSL;
4277
            DB::$connect_options = DB_CONNECT_OPTIONS;
4278
        }
4279
    }
4280
}
4281
4282
/**
4283
 * Returns the page the user is visiting.
4284
 *
4285
 * @return string The page name
4286
 */
4287
function getCurrectPage($SETTINGS)
4288
{
4289
    
4290
    $request = SymfonyRequest::createFromGlobals();
4291
4292
    // Parse the url
4293
    parse_str(
4294
        substr(
4295
            (string) $request->getRequestUri(),
4296
            strpos((string) $request->getRequestUri(), '?') + 1
4297
        ),
4298
        $result
4299
    );
4300
4301
    return $result['page'];
4302
}
4303
4304
/**
4305
 * Permits to return value if set
4306
 *
4307
 * @param string|int $value
4308
 * @param string|int|null $retFalse
4309
 * @param string|int $retTrue
4310
 * @return mixed
4311
 */
4312
function returnIfSet($value, $retFalse = '', $retTrue = null): mixed
4313
{
4314
    if (!empty($value)) {
4315
        return is_null($retTrue) ? $value : $retTrue;
4316
    }
4317
    return $retFalse;
4318
}
4319
4320
4321
/**
4322
 * SEnd email to user
4323
 *
4324
 * @param string $post_receipt
4325
 * @param string $post_body
4326
 * @param string $post_subject
4327
 * @param array $post_replace
4328
 * @param boolean $immediate_email
4329
 * @param string $encryptedUserPassword
4330
 * @return string
4331
 */
4332
function sendMailToUser(
4333
    string $post_receipt,
4334
    string $post_body,
4335
    string $post_subject,
4336
    array $post_replace,
4337
    bool $immediate_email = false,
4338
    $encryptedUserPassword = ''
4339
): ?string {
4340
    global $SETTINGS;
4341
    $emailSettings = new EmailSettings($SETTINGS);
4342
    $emailService = new EmailService();
4343
    $antiXss = new AntiXSS();
4344
4345
    // Sanitize inputs
4346
    $post_receipt = filter_var($post_receipt, FILTER_SANITIZE_EMAIL);
4347
    $post_subject = $antiXss->xss_clean($post_subject);
4348
    $post_body = $antiXss->xss_clean($post_body);
4349
4350
    if (count($post_replace) > 0) {
4351
        $post_body = str_replace(
4352
            array_keys($post_replace),
4353
            array_values($post_replace),
4354
            $post_body
4355
        );
4356
    }
4357
4358
    // Remove newlines to prevent header injection
4359
    $post_body = str_replace(array("\r", "\n"), '', $post_body);    
4360
4361
    if ($immediate_email === true) {
4362
        // Send email
4363
        $ret = $emailService->sendMail(
4364
            $post_subject,
4365
            $post_body,
4366
            $post_receipt,
4367
            $emailSettings,
4368
            '',
4369
            false
4370
        );
4371
    
4372
        $ret = json_decode($ret, true);
4373
    
4374
        return prepareExchangedData(
4375
            array(
4376
                'error' => empty($ret['error']) === true ? false : true,
4377
                'message' => $ret['message'],
4378
            ),
4379
            'encode'
4380
        );
4381
    } else {
4382
        // Send through task handler
4383
        prepareSendingEmail(
4384
            $post_subject,
4385
            $post_body,
4386
            $post_receipt,
4387
            "",
4388
            $encryptedUserPassword,
4389
        );
4390
    }
4391
4392
    return null;
4393
}
4394
4395
/**
4396
 * Converts a password strengh value to zxcvbn level
4397
 * 
4398
 * @param integer $passwordStrength
4399
 * 
4400
 * @return integer
4401
 */
4402
function convertPasswordStrength($passwordStrength): int
4403
{
4404
    if ($passwordStrength === 0) {
4405
        return TP_PW_STRENGTH_1;
4406
    } else if ($passwordStrength === 1) {
4407
        return TP_PW_STRENGTH_2;
4408
    } else if ($passwordStrength === 2) {
4409
        return TP_PW_STRENGTH_3;
4410
    } else if ($passwordStrength === 3) {
4411
        return TP_PW_STRENGTH_4;
4412
    } else {
4413
        return TP_PW_STRENGTH_5;
4414
    }
4415
}
4416
4417
/**
4418
 * Check that a password is strong. The password needs to have at least :
4419
 *   - length >= 10.
4420
 *   - Uppercase and lowercase chars.
4421
 *   - Number or special char.
4422
 *   - Not contain username, name or mail part.
4423
 *   - Different from previous password.
4424
 * 
4425
 * @param string $password - Password to ckeck.
4426
 * @return bool - true if the password is strong, false otherwise.
4427
 */
4428
function isPasswordStrong($password) {
4429
    $session = SessionManager::getSession();
4430
4431
    // Password can't contain login, name or lastname
4432
    $forbiddenWords = [
4433
        $session->get('user-login'),
4434
        $session->get('user-name'),
4435
        $session->get('user-lastname'),
4436
    ];
4437
4438
    // Cut out the email
4439
    if ($email = $session->get('user-email')) {
4440
        $emailParts = explode('@', $email);
4441
4442
        if (count($emailParts) === 2) {
4443
            // Mail username (removed @domain.tld)
4444
            $forbiddenWords[] = $emailParts[0];
4445
4446
            // Organisation name (removed username@ and .tld)
4447
            $domain = explode('.', $emailParts[1]);
4448
            if (count($domain) > 1)
4449
                $forbiddenWords[] = $domain[0];
4450
        }
4451
    }
4452
4453
    // Search forbidden words in password
4454
    foreach ($forbiddenWords as $word) {
4455
        if (empty($word))
4456
            continue;
4457
4458
        // Stop if forbidden word found in password
4459
        if (stripos($password, $word) !== false)
4460
            return false;
4461
    }
4462
4463
    // Get password complexity
4464
    $length = strlen($password);
4465
    $hasUppercase = preg_match('/[A-Z]/', $password);
4466
    $hasLowercase = preg_match('/[a-z]/', $password);
4467
    $hasNumber = preg_match('/[0-9]/', $password);
4468
    $hasSpecialChar = preg_match('/[\W_]/', $password);
4469
4470
    // Get current user hash
4471
    $userHash = DB::queryFirstRow(
4472
        "SELECT pw FROM " . prefixtable('users') . " WHERE id = %d;",
4473
        $session->get('user-id')
4474
    )['pw'];
4475
4476
    $passwordManager = new PasswordManager();
4477
    
4478
    return $length >= 8
4479
           && $hasUppercase
4480
           && $hasLowercase
4481
           && ($hasNumber || $hasSpecialChar)
4482
           && !$passwordManager->verifyPassword($userHash, $password);
4483
}
4484
4485
4486
/**
4487
 * Converts a value to a string, handling various types and cases.
4488
 *
4489
 * @param mixed $value La valeur à convertir
4490
 * @param string $default Valeur par défaut si la conversion n'est pas possible
4491
 * @return string
4492
 */
4493
function safeString($value, string $default = ''): string
4494
{
4495
    // Simple cases
4496
    if (is_string($value)) {
4497
        return $value;
4498
    }
4499
    
4500
    if (is_scalar($value)) {
4501
        return (string) $value;
4502
    }
4503
    
4504
    // Special cases
4505
    if (is_null($value)) {
4506
        return $default;
4507
    }
4508
    
4509
    if (is_array($value)) {
4510
        return empty($value) ? $default : json_encode($value, JSON_UNESCAPED_UNICODE);
4511
    }
4512
    
4513
    if (is_object($value)) {
4514
        // Vérifie si l'objet implémente __toString()
4515
        if (method_exists($value, '__toString')) {
4516
            return (string) $value;
4517
        }
4518
        
4519
        // Alternative: serialize ou json selon le contexte
4520
        return get_class($value) . (method_exists($value, 'getId') ? '#' . $value->getId() : '');
4521
    }
4522
    
4523
    if (is_resource($value)) {
4524
        return 'Resource#' . get_resource_id($value) . ' of type ' . get_resource_type($value);
4525
    }
4526
    
4527
    // Cas par défaut
4528
    return $default;
4529
}
4530