Passed
Pull Request — master (#5044)
by
unknown
06:32
created

logEvents()   F

Complexity

Conditions 16
Paths 392

Size

Total Lines 63
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 16
eloc 35
c 1
b 0
f 0
nc 392
nop 6
dl 0
loc 63
rs 2.3333

How to fix   Long Method    Complexity   

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:

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

1936
            /** @scrutinizer ignore-type */ $password
Loading history...
1937
        );
1938
    } elseif ($type === 'encrypt') {
1939
        // Encrypt file
1940
        $err = defuseFileEncrypt(
1941
            $source_file,
1942
            $target_file,
1943
            $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

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

5611
        /** @scrutinizer ignore-type */ dataSanitizer($fieldsToProcess, $filters)
Loading history...
5612
    );
5613
}
5614
5615
5616
// <--
5617
/**
5618
 * Get or regenerate temporary key based on lifetime
5619
 * Creates a new key if current one is older than lifetime, otherwise returns existing key
5620
 * 
5621
 * @param int $userId User ID
5622
 * @param int $lifetimeSeconds Key lifetime in seconds (default: 3600 = 1 hour)
5623
 * 
5624
 * @return string Valid temporary key (existing or newly generated)
5625
 */
5626
function getOrRotateKeyTempo(int $userId, int $lifetimeSeconds = 3600): string
5627
{
5628
    $userData = DB::queryFirstRow(
5629
        'SELECT key_tempo, key_tempo_created_at FROM %l WHERE id=%i',
5630
        prefixTable('users'),
5631
        $userId
5632
    );
5633
    
5634
    // No key exists or no timestamp - generate new one
5635
    if (!$userData || empty($userData['key_tempo']) || $userData['key_tempo_created_at'] === null) {
5636
        return generateNewKeyTempo($userId);
5637
    }
5638
    
5639
    // Check if key is expired
5640
    $age = time() - (int)$userData['key_tempo_created_at'];
5641
    
5642
    if ($age > $lifetimeSeconds) {
5643
        // Key expired - generate new one
5644
        return generateNewKeyTempo($userId);
5645
    }
5646
    
5647
    // Key still valid - return existing
5648
    return $userData['key_tempo'];
5649
}
5650
5651
/**
5652
 * Generate a new temporary key with timestamp
5653
 * 
5654
 * @param int $userId User ID
5655
 * 
5656
 * @return string Generated key
5657
 */
5658
function generateNewKeyTempo(int $userId): string
5659
{
5660
    $keyTempo = bin2hex(random_bytes(16));
5661
    $createdAt = time();
5662
    
5663
    DB::update(
5664
        prefixTable('users'),
5665
        [
5666
            'key_tempo' => $keyTempo,
5667
            'key_tempo_created_at' => $createdAt
5668
        ],
5669
        'id=%i',
5670
        $userId
5671
    );
5672
    
5673
    return $keyTempo;
5674
}
5675
// -->
5676