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

identUserGetPFList()   C

Complexity

Conditions 12
Paths 48

Size

Total Lines 79
Code Lines 42

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 12
eloc 42
nc 48
nop 12
dl 0
loc 79
rs 6.9666
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

1888
            /** @scrutinizer ignore-type */ $password
Loading history...
1889
        );
1890
    } elseif ($type === 'encrypt') {
1891
        // Encrypt file
1892
        $err = defuseFileEncrypt(
1893
            $source_file,
1894
            $target_file,
1895
            $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

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