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

logItems()   F

Complexity

Conditions 27
Paths > 20000

Size

Total Lines 133
Code Lines 72

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 27
eloc 72
c 1
b 0
f 0
nc 43778
nop 10
dl 0
loc 133
rs 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      main.functions.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-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