dataSanitizer()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 10
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

1877
            /** @scrutinizer ignore-type */ $password
Loading history...
1878
        );
1879
    } elseif ($type === 'encrypt') {
1880
        // Encrypt file
1881
        $err = defuseFileEncrypt(
1882
            $source_file,
1883
            $target_file,
1884
            $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

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