Issues (43)

Security Analysis    no vulnerabilities found

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

sources/main.functions.php (10 issues)

Code
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
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
    DB::insert(
1432
        prefixTable('log_system'),
1433
        [
1434
            'type' => $type,
1435
            'date' => time(),
1436
            'label' => $label,
1437
            'qui' => $who,
1438
            'field_1' => $field_1 === null ? '' : $field_1,
1439
        ]
1440
    );
1441
    // If SYSLOG
1442
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1443
        if ($type === 'user_mngt') {
1444
            send_syslog(
1445
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1446
                $SETTINGS['syslog_host'],
1447
                $SETTINGS['syslog_port'],
1448
                'teampass'
1449
            );
1450
        } else {
1451
            send_syslog(
1452
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1453
                $SETTINGS['syslog_host'],
1454
                $SETTINGS['syslog_port'],
1455
                'teampass'
1456
            );
1457
        }
1458
    }
1459
}
1460
1461
/**
1462
 * Log events.
1463
 *
1464
 * @param array  $SETTINGS        Teampass settings
1465
 * @param int    $item_id         Item id
1466
 * @param string $item_label      Item label
1467
 * @param int    $id_user         User id
1468
 * @param string $action          Code for reason
1469
 * @param string $login           User login
1470
 * @param string $raison          Code for reason
1471
 * @param string $encryption_type Encryption on
1472
 * @param string $time Encryption Time
1473
 * @param string $old_value       Old value
1474
 * 
1475
 * @return void
1476
 */
1477
function logItems(
1478
    array $SETTINGS,
1479
    int $item_id,
1480
    string $item_label,
1481
    int $id_user,
1482
    string $action,
1483
    ?string $login = null,
1484
    ?string $raison = null,
1485
    ?string $encryption_type = null,
1486
    ?string $time = null,
1487
    ?string $old_value = null
1488
): void {
1489
    // Load class DB
1490
    loadClasses('DB');
1491
1492
    // Insert log in DB
1493
    DB::insert(
1494
        prefixTable('log_items'),
1495
        [
1496
            'id_item' => $item_id,
1497
            'date' => is_null($time) === true ? time() : $time,
1498
            'id_user' => $id_user,
1499
            'action' => $action,
1500
            'raison' => $raison,
1501
            'old_value' => $old_value,
1502
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1503
        ]
1504
    );
1505
    // Timestamp the last change
1506
    if (in_array($action, ['at_creation', 'at_modifiation', 'at_delete', 'at_import'], true)) {
1507
        DB::update(
1508
            prefixTable('misc'),
1509
            [
1510
                'valeur' => time(),
1511
                'updated_at' => time(),
1512
            ],
1513
            'type = %s AND intitule = %s',
1514
            'timestamp',
1515
            'last_item_change'
1516
        );
1517
    }
1518
1519
    // SYSLOG
1520
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1521
        // Extract reason
1522
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1523
        // Get item info if not known
1524
        if (empty($item_label) === true) {
1525
            $dataItem = DB::queryFirstRow(
1526
                'SELECT id, id_tree, label
1527
                FROM ' . prefixTable('items') . '
1528
                WHERE id = %i',
1529
                $item_id
1530
            );
1531
            $item_label = $dataItem['label'];
1532
        }
1533
1534
        send_syslog(
1535
            'action=' . str_replace('at_', '', $action) .
1536
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1537
                ' itemno=' . $item_id .
1538
                ' user=' . (is_null($login) === true ? '' : addslashes((string) $login)) .
1539
                ' itemname="' . addslashes($item_label) . '"',
1540
            $SETTINGS['syslog_host'],
1541
            $SETTINGS['syslog_port'],
1542
            'teampass'
1543
        );
1544
    }
1545
1546
    // send notification if enabled
1547
    //notifyOnChange($item_id, $action, $SETTINGS);
1548
}
1549
1550
/**
1551
 * Prepare notification email to subscribers.
1552
 *
1553
 * @param int    $item_id  Item id
1554
 * @param string $label    Item label
1555
 * @param array  $changes  List of changes
1556
 * @param array  $SETTINGS Teampass settings
1557
 * 
1558
 * @return void
1559
 */
1560
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1561
{
1562
    $session = SessionManager::getSession();
1563
    $lang = new Language($session->get('user-language') ?? 'english');
1564
    $globalsUserId = $session->get('user-id');
1565
    $globalsLastname = $session->get('user-lastname');
1566
    $globalsName = $session->get('user-name');
1567
    // send email to user that what to be notified
1568
    $notification = DB::queryFirstField(
1569
        'SELECT email
1570
        FROM ' . prefixTable('notification') . ' AS n
1571
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1572
        WHERE n.item_id = %i AND n.user_id != %i',
1573
        $item_id,
1574
        $globalsUserId
1575
    );
1576
    if (DB::count() > 0) {
1577
        // Prepare path
1578
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1579
        // Get list of changes
1580
        $htmlChanges = '<ul>';
1581
        foreach ($changes as $change) {
1582
            $htmlChanges .= '<li>' . $change . '</li>';
1583
        }
1584
        $htmlChanges .= '</ul>';
1585
        // send email
1586
        DB::insert(
1587
            prefixTable('emails'),
1588
            [
1589
                'timestamp' => time(),
1590
                'subject' => $lang->get('email_subject_item_updated'),
1591
                'body' => str_replace(
1592
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1593
                    [$label, $path, (string) $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1594
                    $lang->get('email_body_item_updated')
1595
                ),
1596
                'receivers' => implode(',', $notification),
1597
                'status' => '',
1598
            ]
1599
        );
1600
    }
1601
}
1602
1603
/**
1604
 * Returns the Item + path.
1605
 *
1606
 * @param int    $id_tree  Node id
1607
 * @param string $label    Label
1608
 * @param array  $SETTINGS TP settings
1609
 * 
1610
 * @return string
1611
 */
1612
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1613
{
1614
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1615
    $arbo = $tree->getPath($id_tree, true);
1616
    $path = '';
1617
    foreach ($arbo as $elem) {
1618
        if (empty($path) === true) {
1619
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1620
        } else {
1621
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1622
        }
1623
    }
1624
1625
    // Build text to show user
1626
    if (empty($label) === false) {
1627
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1628
    }
1629
    return empty($path) === true ? '' : $path;
1630
}
1631
1632
/**
1633
 * Get the client ip address.
1634
 *
1635
 * @return string IP address
1636
 */
1637
function getClientIpServer(): string
1638
{
1639
    if (getenv('HTTP_CLIENT_IP')) {
1640
        $ipaddress = getenv('HTTP_CLIENT_IP');
1641
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1642
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1643
    } elseif (getenv('HTTP_X_FORWARDED')) {
1644
        $ipaddress = getenv('HTTP_X_FORWARDED');
1645
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1646
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1647
    } elseif (getenv('HTTP_FORWARDED')) {
1648
        $ipaddress = getenv('HTTP_FORWARDED');
1649
    } elseif (getenv('REMOTE_ADDR')) {
1650
        $ipaddress = getenv('REMOTE_ADDR');
1651
    } else {
1652
        $ipaddress = 'UNKNOWN';
1653
    }
1654
1655
    return $ipaddress;
1656
}
1657
1658
/**
1659
 * Escape all HTML, JavaScript, and CSS.
1660
 *
1661
 * @param string $input    The input string
1662
 * @param string $encoding Which character encoding are we using?
1663
 * 
1664
 * @return string
1665
 */
1666
function noHTML(string $input, string $encoding = 'UTF-8'): string
1667
{
1668
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1669
}
1670
1671
/**
1672
 * Rebuilds the Teampass config file.
1673
 *
1674
 * @param string $configFilePath Path to the config file.
1675
 * @param array  $settings       Teampass settings.
1676
 *
1677
 * @return string|bool
1678
 */
1679
function rebuildConfigFile(string $configFilePath, array $settings)
1680
{
1681
    // Perform a copy if the file exists
1682
    if (file_exists($configFilePath)) {
1683
        $backupFilePath = $configFilePath . '.' . date('Y_m_d_His', time());
1684
        if (!copy($configFilePath, $backupFilePath)) {
1685
            return "ERROR: Could not copy file '$configFilePath'";
1686
        }
1687
    }
1688
1689
    // Regenerate the config file
1690
    $data = ["<?php\n", "global \$SETTINGS;\n", "\$SETTINGS = array (\n"];
1691
    $rows = DB::query('SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s', 'admin');
1692
    foreach ($rows as $record) {
1693
        $value = getEncryptedValue($record['valeur'], $record['is_encrypted']);
1694
        $data[] = "    '{$record['intitule']}' => '". htmlspecialchars_decode($value, ENT_COMPAT) . "',\n";
1695
    }
1696
    $data[] = ");\n";
1697
    $data = array_unique($data);
1698
1699
    // Update the file
1700
    file_put_contents($configFilePath, implode('', $data));
1701
1702
    return true;
1703
}
1704
1705
/**
1706
 * Returns the encrypted value if needed.
1707
 *
1708
 * @param string $value       Value to encrypt.
1709
 * @param int   $isEncrypted Is the value encrypted?
1710
 *
1711
 * @return string
1712
 */
1713
function getEncryptedValue(string $value, int $isEncrypted): string
1714
{
1715
    return $isEncrypted ? cryption($value, '', 'encrypt')['string'] : $value;
1716
}
1717
1718
/**
1719
 * Permits to replace &#92; to permit correct display
1720
 *
1721
 * @param string $input Some text
1722
 * 
1723
 * @return string
1724
 */
1725
function handleBackslash(string $input): string
1726
{
1727
    return str_replace('&amp;#92;', '&#92;', $input);
1728
}
1729
1730
/**
1731
 * Permits to load settings
1732
 * 
1733
 * @return void
1734
*/
1735
function loadSettings(): void
1736
{
1737
    global $SETTINGS;
1738
    /* LOAD CPASSMAN SETTINGS */
1739
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1740
        $SETTINGS = [];
1741
        $SETTINGS['duplicate_folder'] = 0;
1742
        //by default, this is set to 0;
1743
        $SETTINGS['duplicate_item'] = 0;
1744
        //by default, this is set to 0;
1745
        $SETTINGS['number_of_used_pw'] = 5;
1746
        //by default, this value is set to 5;
1747
        $settings = [];
1748
        $rows = DB::query(
1749
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1750
            [
1751
                'type' => 'admin',
1752
                'type2' => 'settings',
1753
            ]
1754
        );
1755
        foreach ($rows as $record) {
1756
            if ($record['type'] === 'admin') {
1757
                $SETTINGS[$record['intitule']] = $record['valeur'];
1758
            } else {
1759
                $settings[$record['intitule']] = $record['valeur'];
1760
            }
1761
        }
1762
        $SETTINGS['loaded'] = 1;
1763
        $SETTINGS['default_session_expiration_time'] = 5;
1764
    }
1765
}
1766
1767
/**
1768
 * check if folder has custom fields.
1769
 * Ensure that target one also has same custom fields
1770
 * 
1771
 * @param int $source_id
1772
 * @param int $target_id 
1773
 * 
1774
 * @return bool
1775
*/
1776
function checkCFconsistency(int $source_id, int $target_id): bool
1777
{
1778
    $source_cf = [];
1779
    $rows = DB::query(
1780
        'SELECT id_category
1781
            FROM ' . prefixTable('categories_folders') . '
1782
            WHERE id_folder = %i',
1783
        $source_id
1784
    );
1785
    foreach ($rows as $record) {
1786
        array_push($source_cf, $record['id_category']);
1787
    }
1788
1789
    $target_cf = [];
1790
    $rows = DB::query(
1791
        'SELECT id_category
1792
            FROM ' . prefixTable('categories_folders') . '
1793
            WHERE id_folder = %i',
1794
        $target_id
1795
    );
1796
    foreach ($rows as $record) {
1797
        array_push($target_cf, $record['id_category']);
1798
    }
1799
1800
    $cf_diff = array_diff($source_cf, $target_cf);
1801
    if (count($cf_diff) > 0) {
1802
        return false;
1803
    }
1804
1805
    return true;
1806
}
1807
1808
/**
1809
 * Will encrypte/decrypt a fil eusing Defuse.
1810
 *
1811
 * @param string $type        can be either encrypt or decrypt
1812
 * @param string $source_file path to source file
1813
 * @param string $target_file path to target file
1814
 * @param array  $SETTINGS    Settings
1815
 * @param string $password    A password
1816
 *
1817
 * @return string|bool
1818
 */
1819
function prepareFileWithDefuse(
1820
    string $type,
1821
    string $source_file,
1822
    string $target_file,
1823
    ?string $password = null
1824
) {
1825
    // Load AntiXSS
1826
    $antiXss = new AntiXSS();
1827
    // Protect against bad inputs
1828
    if (is_array($source_file) === true || is_array($target_file) === true) {
1829
        return 'error_cannot_be_array';
1830
    }
1831
1832
    // Sanitize
1833
    $source_file = $antiXss->xss_clean($source_file);
1834
    $target_file = $antiXss->xss_clean($target_file);
1835
    if (empty($password) === true || is_null($password) === true) {
1836
        // get KEY to define password
1837
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
1838
        $password = Key::loadFromAsciiSafeString($ascii_key);
1839
    }
1840
1841
    $err = '';
1842
    if ($type === 'decrypt') {
1843
        // Decrypt file
1844
        $err = defuseFileDecrypt(
1845
            $source_file,
1846
            $target_file,
1847
            $password
0 ignored issues
show
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

1847
            /** @scrutinizer ignore-type */ $password
Loading history...
1848
        );
1849
    } elseif ($type === 'encrypt') {
1850
        // Encrypt file
1851
        $err = defuseFileEncrypt(
1852
            $source_file,
1853
            $target_file,
1854
            $password
0 ignored issues
show
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

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

5522
        /** @scrutinizer ignore-type */ dataSanitizer($fieldsToProcess, $filters)
Loading history...
5523
    );
5524
}
5525
5526
5527
// <--
5528
/**
5529
 * Get or regenerate temporary key based on lifetime
5530
 * Creates a new key if current one is older than lifetime, otherwise returns existing key
5531
 * 
5532
 * @param int $userId User ID
5533
 * @param int $lifetimeSeconds Key lifetime in seconds (default: 3600 = 1 hour)
5534
 * 
5535
 * @return string Valid temporary key (existing or newly generated)
5536
 */
5537
function getOrRotateKeyTempo(int $userId, int $lifetimeSeconds = 3600): string
5538
{
5539
    $userData = DB::queryFirstRow(
5540
        'SELECT key_tempo, key_tempo_created_at FROM %l WHERE id=%i',
5541
        prefixTable('users'),
5542
        $userId
5543
    );
5544
    
5545
    // No key exists or no timestamp - generate new one
5546
    if (!$userData || empty($userData['key_tempo']) || $userData['key_tempo_created_at'] === null) {
5547
        return generateNewKeyTempo($userId);
5548
    }
5549
    
5550
    // Check if key is expired
5551
    $age = time() - (int)$userData['key_tempo_created_at'];
5552
    
5553
    if ($age > $lifetimeSeconds) {
5554
        // Key expired - generate new one
5555
        return generateNewKeyTempo($userId);
5556
    }
5557
    
5558
    // Key still valid - return existing
5559
    return $userData['key_tempo'];
5560
}
5561
5562
/**
5563
 * Generate a new temporary key with timestamp
5564
 * 
5565
 * @param int $userId User ID
5566
 * 
5567
 * @return string Generated key
5568
 */
5569
function generateNewKeyTempo(int $userId): string
5570
{
5571
    $keyTempo = bin2hex(random_bytes(16));
5572
    $createdAt = time();
5573
    
5574
    DB::update(
5575
        prefixTable('users'),
5576
        [
5577
            'key_tempo' => $keyTempo,
5578
            'key_tempo_created_at' => $createdAt
5579
        ],
5580
        'id=%i',
5581
        $userId
5582
    );
5583
    
5584
    return $keyTempo;
5585
}
5586
// -->