Issues (29)

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 (3 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-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use LdapRecord\Connection;
33
use Elegant\Sanitizer\Sanitizer;
34
use voku\helper\AntiXSS;
35
use Hackzilla\PasswordGenerator\Generator\ComputerPasswordGenerator;
36
use Hackzilla\PasswordGenerator\RandomGenerator\Php7RandomGenerator;
37
use TeampassClasses\SessionManager\SessionManager;
38
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
39
use TeampassClasses\Language\Language;
40
use TeampassClasses\NestedTree\NestedTree;
41
use Defuse\Crypto\Key;
42
use Defuse\Crypto\Crypto;
43
use Defuse\Crypto\KeyProtectedByPassword;
44
use Defuse\Crypto\File as CryptoFile;
45
use Defuse\Crypto\Exception as CryptoException;
46
use TeampassClasses\PasswordManager\PasswordManager;
47
use Symfony\Component\Process\PhpExecutableFinder;
48
use TeampassClasses\Encryption\Encryption;
49
use TeampassClasses\ConfigManager\ConfigManager;
50
use TeampassClasses\EmailService\EmailService;
51
use TeampassClasses\EmailService\EmailSettings;
52
53
header('Content-type: text/html; charset=utf-8');
54
header('Cache-Control: no-cache, must-revalidate');
55
56
loadClasses('DB');
57
$session = SessionManager::getSession();
58
59
// Load config if $SETTINGS not defined
60
$configManager = new ConfigManager();
61
$SETTINGS = $configManager->getAllSettings();
62
63
/**
64
 * 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 : ''));
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 *
699
        FROM ' . prefixTable('items') . ' as i
700
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
701
        AND l.action = %s
702
        AND i.inactif = %i',
703
        'at_creation',
704
        0
705
    );
706
    foreach ($rows as $record) {
707
        if (empty($record['id_tree']) === false) {
708
            // Get all TAGS
709
            $tags = '';
710
            $itemTags = DB::query(
711
                'SELECT tag
712
                FROM ' . prefixTable('tags') . '
713
                WHERE item_id = %i AND tag != ""',
714
                $record['id']
715
            );
716
            foreach ($itemTags as $itemTag) {
717
                $tags .= $itemTag['tag'] . ' ';
718
            }
719
720
            // Get renewal period
721
            $resNT = DB::queryFirstRow(
722
                'SELECT renewal_period
723
                FROM ' . prefixTable('nested_tree') . '
724
                WHERE id = %i',
725
                $record['id_tree']
726
            );
727
            // form id_tree to full foldername
728
            $folder = [];
729
            $arbo = $tree->getPath($record['id_tree'], true);
730
            foreach ($arbo as $elem) {
731
                // Check if title is the ID of a user
732
                if (is_numeric($elem->title) === true) {
733
                    // Is this a User id?
734
                    $user = DB::queryFirstRow(
735
                        'SELECT login
736
                        FROM ' . prefixTable('users') . '
737
                        WHERE id = %i',
738
                        $elem->title
739
                    );
740
                    if ($user !== null) {
741
                        $elem->title = $user['login'];
742
                    }
743
                }
744
                // Build path
745
                array_push($folder, stripslashes($elem->title));
746
            }
747
            // store data
748
            DB::insert(
749
                prefixTable('cache'),
750
                [
751
                    'id' => $record['id'],
752
                    'label' => $record['label'],
753
                    'description' => $record['description'] ?? '',
754
                    'url' => isset($record['url']) && ! empty($record['url']) ? $record['url'] : '0',
755
                    'tags' => $tags,
756
                    'id_tree' => $record['id_tree'],
757
                    'perso' => $record['perso'],
758
                    'restricted_to' => isset($record['restricted_to']) && ! empty($record['restricted_to']) ? $record['restricted_to'] : '0',
759
                    'login' => $record['login'] ?? '',
760
                    'folder' => implode(' » ', $folder),
761
                    'author' => $record['id_user'],
762
                    'renewal_period' => $resNT['renewal_period'] ?? '0',
763
                    'timestamp' => $record['date'],
764
                ]
765
            );
766
        }
767
    }
768
}
769
770
/**
771
 * Cache table - update existing value.
772
 *
773
 * @param int    $ident    Ident format
774
 * 
775
 * @return void
776
 */
777
function cacheTableUpdate(?int $ident = null): void
778
{
779
    $session = SessionManager::getSession();
780
    loadClasses('DB');
781
782
    //Load Tree
783
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
784
    // get new value from db
785
    $data = DB::queryFirstRow(
786
        'SELECT label, description, id_tree, perso, restricted_to, login, url
787
        FROM ' . prefixTable('items') . '
788
        WHERE id=%i',
789
        $ident
790
    );
791
    // Get all TAGS
792
    $tags = '';
793
    $itemTags = DB::query(
794
        'SELECT tag
795
            FROM ' . prefixTable('tags') . '
796
            WHERE item_id = %i AND tag != ""',
797
        $ident
798
    );
799
    foreach ($itemTags as $itemTag) {
800
        $tags .= $itemTag['tag'] . ' ';
801
    }
802
    // form id_tree to full foldername
803
    $folder = [];
804
    $arbo = $tree->getPath($data['id_tree'], true);
805
    foreach ($arbo as $elem) {
806
        // Check if title is the ID of a user
807
        if (is_numeric($elem->title) === true) {
808
            // Is this a User id?
809
            $user = DB::queryFirstRow(
810
                'SELECT id, login
811
                FROM ' . prefixTable('users') . '
812
                WHERE id = %i',
813
                $elem->title
814
            );
815
            if ($user !== null) {
816
                $elem->title = $user['login'];
817
            }
818
        }
819
        // Build path
820
        array_push($folder, stripslashes($elem->title));
821
    }
822
    // finaly update
823
    DB::update(
824
        prefixTable('cache'),
825
        [
826
            'label' => $data['label'],
827
            'description' => $data['description'],
828
            'tags' => $tags,
829
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
830
            'id_tree' => $data['id_tree'],
831
            'perso' => $data['perso'],
832
            'restricted_to' => isset($data['restricted_to']) && ! empty($data['restricted_to']) ? $data['restricted_to'] : '0',
833
            'login' => $data['login'] ?? '',
834
            'folder' => implode(' » ', $folder),
835
            'author' => $session->get('user-id'),
836
        ],
837
        'id = %i',
838
        $ident
839
    );
840
}
841
842
/**
843
 * Cache table - add new value.
844
 *
845
 * @param int    $ident    Ident format
846
 * 
847
 * @return void
848
 */
849
function cacheTableAdd(?int $ident = null): void
850
{
851
    $session = SessionManager::getSession();
852
    $globalsUserId = $session->get('user-id');
853
854
    // Load class DB
855
    loadClasses('DB');
856
857
    //Load Tree
858
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
859
    // get new value from db
860
    $data = DB::queryFirstRow(
861
        'SELECT i.label, i.description, i.id_tree as id_tree, i.perso, i.restricted_to, i.id, i.login, i.url, l.date
862
        FROM ' . prefixTable('items') . ' as i
863
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
864
        WHERE i.id = %i
865
        AND l.action = %s',
866
        $ident,
867
        'at_creation'
868
    );
869
    // Get all TAGS
870
    $tags = '';
871
    $itemTags = DB::query(
872
        'SELECT tag
873
            FROM ' . prefixTable('tags') . '
874
            WHERE item_id = %i AND tag != ""',
875
        $ident
876
    );
877
    foreach ($itemTags as $itemTag) {
878
        $tags .= $itemTag['tag'] . ' ';
879
    }
880
    // form id_tree to full foldername
881
    $folder = [];
882
    $arbo = $tree->getPath($data['id_tree'], true);
883
    foreach ($arbo as $elem) {
884
        // Check if title is the ID of a user
885
        if (is_numeric($elem->title) === true) {
886
            // Is this a User id?
887
            $user = DB::queryFirstRow(
888
                'SELECT id, login
889
                FROM ' . prefixTable('users') . '
890
                WHERE id = %i',
891
                $elem->title
892
            );
893
            if ($user !== null) {
894
                $elem->title = $user['login'];
895
            }
896
        }
897
        // Build path
898
        array_push($folder, stripslashes($elem->title));
899
    }
900
    // finaly update
901
    DB::insert(
902
        prefixTable('cache'),
903
        [
904
            'id' => $data['id'],
905
            'label' => $data['label'],
906
            'description' => $data['description'],
907
            'tags' => empty($tags) === false ? $tags : 'None',
908
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
909
            'id_tree' => $data['id_tree'],
910
            'perso' => isset($data['perso']) && empty($data['perso']) === false && $data['perso'] !== 'None' ? $data['perso'] : '0',
911
            'restricted_to' => isset($data['restricted_to']) && empty($data['restricted_to']) === false ? $data['restricted_to'] : '0',
912
            'login' => $data['login'] ?? '',
913
            'folder' => implode(' » ', $folder),
914
            'author' => $globalsUserId,
915
            'timestamp' => $data['date'],
916
        ]
917
    );
918
}
919
920
/**
921
 * Do statistics.
922
 *
923
 * @param array $SETTINGS Teampass settings
924
 *
925
 * @return array
926
 */
927
function getStatisticsData(array $SETTINGS): array
928
{
929
    DB::query(
930
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
931
        0
932
    );
933
    $counter_folders = DB::count();
934
    DB::query(
935
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
936
        1
937
    );
938
    $counter_folders_perso = DB::count();
939
    DB::query(
940
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
941
        0
942
    );
943
    $counter_items = DB::count();
944
        DB::query(
945
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
946
        1
947
    );
948
    $counter_items_perso = DB::count();
949
        DB::query(
950
        'SELECT id FROM ' . prefixTable('users') . ' WHERE login NOT IN (%s, %s, %s)',
951
        'OTV', 'TP', 'API'
952
    );
953
    $counter_users = DB::count();
954
        DB::query(
955
        'SELECT id FROM ' . prefixTable('users') . ' WHERE admin = %i',
956
        1
957
    );
958
    $admins = DB::count();
959
    DB::query(
960
        'SELECT id FROM ' . prefixTable('users') . ' WHERE gestionnaire = %i',
961
        1
962
    );
963
    $managers = DB::count();
964
    DB::query(
965
        'SELECT id FROM ' . prefixTable('users') . ' WHERE read_only = %i',
966
        1
967
    );
968
    $readOnly = DB::count();
969
    // list the languages
970
    $usedLang = [];
971
    $tp_languages = DB::query(
972
        'SELECT name FROM ' . prefixTable('languages')
973
    );
974
    foreach ($tp_languages as $tp_language) {
975
        DB::query(
976
            'SELECT * FROM ' . prefixTable('users') . ' WHERE user_language = %s',
977
            $tp_language['name']
978
        );
979
        $usedLang[$tp_language['name']] = round((DB::count() * 100 / $counter_users), 0);
980
    }
981
982
    // get list of ips
983
    $usedIp = [];
984
    $tp_ips = DB::query(
985
        'SELECT user_ip FROM ' . prefixTable('users')
986
    );
987
    foreach ($tp_ips as $ip) {
988
        if (array_key_exists($ip['user_ip'], $usedIp)) {
989
            $usedIp[$ip['user_ip']] += $usedIp[$ip['user_ip']];
990
        } elseif (! empty($ip['user_ip']) && $ip['user_ip'] !== 'none') {
991
            $usedIp[$ip['user_ip']] = 1;
992
        }
993
    }
994
995
    return [
996
        'error' => '',
997
        'stat_phpversion' => phpversion(),
998
        'stat_folders' => $counter_folders,
999
        'stat_folders_shared' => intval($counter_folders) - intval($counter_folders_perso),
1000
        'stat_items' => $counter_items,
1001
        'stat_items_shared' => intval($counter_items) - intval($counter_items_perso),
1002
        'stat_users' => $counter_users,
1003
        'stat_admins' => $admins,
1004
        'stat_managers' => $managers,
1005
        'stat_ro' => $readOnly,
1006
        'stat_kb' => $SETTINGS['enable_kb'],
1007
        'stat_pf' => $SETTINGS['enable_pf_feature'],
1008
        'stat_fav' => $SETTINGS['enable_favourites'],
1009
        'stat_teampassversion' => TP_VERSION,
1010
        'stat_ldap' => $SETTINGS['ldap_mode'],
1011
        'stat_agses' => $SETTINGS['agses_authentication_enabled'],
1012
        'stat_duo' => $SETTINGS['duo'],
1013
        'stat_suggestion' => $SETTINGS['enable_suggestion'],
1014
        'stat_api' => $SETTINGS['api'],
1015
        'stat_customfields' => $SETTINGS['item_extra_fields'],
1016
        'stat_syslog' => $SETTINGS['syslog_enable'],
1017
        'stat_2fa' => $SETTINGS['google_authentication'],
1018
        'stat_stricthttps' => $SETTINGS['enable_sts'],
1019
        'stat_mysqlversion' => DB::serverVersion(),
1020
        'stat_languages' => $usedLang,
1021
        'stat_country' => $usedIp,
1022
    ];
1023
}
1024
1025
/**
1026
 * Permits to prepare the way to send the email
1027
 * 
1028
 * @param string $subject       email subject
1029
 * @param string $body          email message
1030
 * @param string $email         email
1031
 * @param string $receiverName  Receiver name
1032
 * @param string $encryptedUserPassword      encryptedUserPassword
1033
 *
1034
 * @return void
1035
 */
1036
function prepareSendingEmail(
1037
    $subject,
1038
    $body,
1039
    $email,
1040
    $receiverName = '',
1041
    $encryptedUserPassword = ''
1042
): void 
1043
{
1044
    DB::insert(
1045
        prefixTable('background_tasks'),
1046
        array(
1047
            'created_at' => time(),
1048
            'process_type' => 'send_email',
1049
            'arguments' => json_encode([
1050
                'subject' => html_entity_decode($subject, ENT_QUOTES | ENT_HTML5, 'UTF-8'),
1051
                'receivers' => $email,
1052
                'body' => $body,
1053
                'receiver_name' => $receiverName,
1054
                'encryptedUserPassword' => $encryptedUserPassword,
1055
            ], JSON_HEX_QUOT | JSON_HEX_TAG),
1056
        )
1057
    );
1058
}
1059
1060
/**
1061
 * Returns the email body.
1062
 *
1063
 * @param string $textMail Text for the email
1064
 */
1065
function emailBody(string $textMail): string
1066
{
1067
    return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
1068
    w3.org/TR/html4/loose.dtd"><html>
1069
    <head><title>Email Template</title>
1070
    <style type="text/css">
1071
    body { background-color: #f0f0f0; padding: 10px 0; margin:0 0 10px =0; }
1072
    </style></head>
1073
    <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">
1074
    <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#f0f0f0" style="border-spacing: 0;">
1075
    <tr><td style="border-collapse: collapse;"><br>
1076
        <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#17357c" style="border-spacing: 0; margin-bottom: 25px;">
1077
        <tr><td style="border-collapse: collapse; padding: 11px 20px;">
1078
            <div style="max-width:150px; max-height:34px; color:#f0f0f0; font-weight:bold;">Teampass</div>
1079
        </td></tr></table></td>
1080
    </tr>
1081
    <tr><td align="center" valign="top" bgcolor="#f0f0f0" style="border-collapse: collapse; background-color: #f0f0f0;">
1082
        <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;">
1083
        <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;">
1084
        <br><div style="float:right;">' .
1085
        $textMail .
1086
        '<br><br></td></tr></table>
1087
    </td></tr></table>
1088
    <br></body></html>';
1089
}
1090
1091
/**
1092
 * Convert date to timestamp.
1093
 *
1094
 * @param string $date        The date
1095
 * @param string $date_format Date format
1096
 *
1097
 * @return int
1098
 */
1099
function dateToStamp(string $date, string $date_format): int
1100
{
1101
    $date = date_parse_from_format($date_format, $date);
1102
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1103
        return mktime(
1104
            empty($date['hour']) === false ? $date['hour'] : 23,
1105
            empty($date['minute']) === false ? $date['minute'] : 59,
1106
            empty($date['second']) === false ? $date['second'] : 59,
1107
            $date['month'],
1108
            $date['day'],
1109
            $date['year']
1110
        );
1111
    }
1112
    return 0;
1113
}
1114
1115
/**
1116
 * Is this a date.
1117
 *
1118
 * @param string $date Date
1119
 *
1120
 * @return bool
1121
 */
1122
function isDate(string $date): bool
1123
{
1124
    return strtotime($date) !== false;
1125
}
1126
1127
/**
1128
 * Check if isUTF8().
1129
 *
1130
 * @param string|array $string Is the string
1131
 *
1132
 * @return int is the string in UTF8 format
1133
 */
1134
function isUTF8($string): int
1135
{
1136
    if (is_array($string) === true) {
1137
        $string = $string['string'];
1138
    }
1139
1140
    return preg_match(
1141
        '%^(?:
1142
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1143
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1144
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1145
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1146
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1147
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1148
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1149
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1150
        )*$%xs',
1151
        $string
1152
    );
1153
}
1154
1155
/**
1156
 * Prepare an array to UTF8 format before JSON_encode.
1157
 *
1158
 * @param array $array Array of values
1159
 *
1160
 * @return array
1161
 */
1162
function utf8Converter(array $array): array
1163
{
1164
    array_walk_recursive(
1165
        $array,
1166
        static function (&$item): void {
1167
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1168
                $item = mb_convert_encoding($item, 'ISO-8859-1', 'UTF-8');
1169
            }
1170
        }
1171
    );
1172
    return $array;
1173
}
1174
1175
/**
1176
 * Permits to prepare data to be exchanged.
1177
 *
1178
 * @param array|string $data Text
1179
 * @param string       $type Parameter
1180
 * @param string       $key  Optional key
1181
 *
1182
 * @return string|array
1183
 */
1184
function prepareExchangedData($data, string $type, ?string $key = null)
1185
{
1186
    $session = SessionManager::getSession();
1187
    $key = empty($key) ? $session->get('key') : $key;
1188
    
1189
    // Perform
1190
    if ($type === 'encode' && is_array($data) === true) {
1191
        // json encoding
1192
        $data = json_encode(
1193
            $data,
1194
            JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1195
        );
1196
        
1197
        // Now encrypt
1198
        if ((int) $session->get('encryptClientServer') === 1) {
1199
            $data = Encryption::encrypt(
1200
                $data,
1201
                $key
1202
            );
1203
        }
1204
1205
        return $data;
1206
    }
1207
1208
    if ($type === 'decode' && is_array($data) === false) {
1209
        // Decrypt if needed
1210
        if ((int) $session->get('encryptClientServer') === 1) {
1211
            $data = (string) Encryption::decrypt(
1212
                (string) $data,
1213
                $key
1214
            );
1215
        } else {
1216
            // Double html encoding received
1217
            $data = html_entity_decode(html_entity_decode(/** @scrutinizer ignore-type */$data)); // @codeCoverageIgnore Is always a string (not an array)
1218
        }
1219
1220
        // Check if $data is a valid string before json_decode
1221
        if (is_string($data) && !empty($data)) {
1222
            // Return data array
1223
            return json_decode($data, true);
1224
        }
1225
    }
1226
1227
    return '';
1228
}
1229
1230
1231
/**
1232
 * Create a thumbnail.
1233
 *
1234
 * @param string  $src           Source
1235
 * @param string  $dest          Destination
1236
 * @param int $desired_width Size of width
1237
 * 
1238
 * @return void|string|bool
1239
 */
1240
function makeThumbnail(string $src, string $dest, int $desired_width)
1241
{
1242
    /* read the source image */
1243
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1244
        $source_image = imagecreatefrompng($src);
1245
        if ($source_image === false) {
1246
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1247
        }
1248
    } else {
1249
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1250
    }
1251
1252
    // Get height and width
1253
    $width = imagesx($source_image);
1254
    $height = imagesy($source_image);
1255
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1256
    $desired_height = (int) floor($height * $desired_width / $width);
1257
    /* create a new, "virtual" image */
1258
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1259
    if ($virtual_image === false) {
1260
        return false;
1261
    }
1262
    /* copy source image at a resized size */
1263
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1264
    /* create the physical thumbnail image to its destination */
1265
    imagejpeg($virtual_image, $dest);
1266
}
1267
1268
/**
1269
 * Check table prefix in SQL query.
1270
 *
1271
 * @param string $table Table name
1272
 * 
1273
 * @return string
1274
 */
1275
function prefixTable(string $table): string
1276
{
1277
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1278
    return $safeTable;
1279
}
1280
1281
/**
1282
 * GenerateCryptKey
1283
 *
1284
 * @param int     $size      Length
1285
 * @param bool $secure Secure
1286
 * @param bool $numerals Numerics
1287
 * @param bool $uppercase Uppercase letters
1288
 * @param bool $symbols Symbols
1289
 * @param bool $lowercase Lowercase
1290
 * 
1291
 * @return string
1292
 */
1293
function GenerateCryptKey(
1294
    int $size = 20,
1295
    bool $secure = false,
1296
    bool $numerals = false,
1297
    bool $uppercase = false,
1298
    bool $symbols = false,
1299
    bool $lowercase = false
1300
): string {
1301
    $generator = new ComputerPasswordGenerator();
1302
    $generator->setRandomGenerator(new Php7RandomGenerator());
1303
    
1304
    // Manage size
1305
    $generator->setLength((int) $size);
1306
    if ($secure === true) {
1307
        $generator->setSymbols(true);
1308
        $generator->setLowercase(true);
1309
        $generator->setUppercase(true);
1310
        $generator->setNumbers(true);
1311
    } else {
1312
        $generator->setLowercase($lowercase);
1313
        $generator->setUppercase($uppercase);
1314
        $generator->setNumbers($numerals);
1315
        $generator->setSymbols($symbols);
1316
    }
1317
1318
    return $generator->generatePasswords()[0];
1319
}
1320
1321
/**
1322
 * GenerateGenericPassword
1323
 *
1324
 * @param int     $size      Length
1325
 * @param bool $secure Secure
1326
 * @param bool $numerals Numerics
1327
 * @param bool $uppercase Uppercase letters
1328
 * @param bool $symbols Symbols
1329
 * @param bool $lowercase Lowercase
1330
 * @param array   $SETTINGS  SETTINGS
1331
 * 
1332
 * @return string
1333
 */
1334
function generateGenericPassword(
1335
    int $size,
1336
    bool $secure,
1337
    bool $lowercase,
1338
    bool $capitalize,
1339
    bool $numerals,
1340
    bool $symbols,
1341
    array $SETTINGS
1342
): string
1343
{
1344
    if ((int) $size > (int) $SETTINGS['pwd_maximum_length']) {
1345
        return prepareExchangedData(
1346
            array(
1347
                'error_msg' => 'Password length is too long! ',
1348
                'error' => 'true',
1349
            ),
1350
            'encode'
1351
        );
1352
    }
1353
    // Load libraries
1354
    $generator = new ComputerPasswordGenerator();
1355
    $generator->setRandomGenerator(new Php7RandomGenerator());
1356
1357
    // Manage size
1358
    $generator->setLength(($size <= 0) ? 10 : $size);
1359
1360
    if ($secure === true) {
1361
        $generator->setSymbols(true);
1362
        $generator->setLowercase(true);
1363
        $generator->setUppercase(true);
1364
        $generator->setNumbers(true);
1365
    } else {
1366
        $generator->setLowercase($lowercase);
1367
        $generator->setUppercase($capitalize);
1368
        $generator->setNumbers($numerals);
1369
        $generator->setSymbols($symbols);
1370
    }
1371
1372
    return prepareExchangedData(
1373
        array(
1374
            'key' => $generator->generatePasswords(),
1375
            'error' => '',
1376
        ),
1377
        'encode'
1378
    );
1379
}
1380
1381
/**
1382
 * Send sysLOG message
1383
 *
1384
 * @param string    $message
1385
 * @param string    $host
1386
 * @param int       $port
1387
 * @param string    $component
1388
 * 
1389
 * @return void
1390
*/
1391
function send_syslog($message, $host, $port, $component = 'teampass'): void
1392
{
1393
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1394
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1395
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1396
    socket_close($sock);
1397
}
1398
1399
/**
1400
 * Permits to log events into DB
1401
 *
1402
 * @param array  $SETTINGS Teampass settings
1403
 * @param string $type     Type
1404
 * @param string $label    Label
1405
 * @param string $who      Who
1406
 * @param string $login    Login
1407
 * @param string|int $field_1  Field
1408
 * 
1409
 * @return void
1410
 */
1411
function logEvents(
1412
    array $SETTINGS, 
1413
    string $type, 
1414
    string $label, 
1415
    string $who, 
1416
    ?string $login = null, 
1417
    $field_1 = null
1418
): void
1419
{
1420
    if (empty($who)) {
1421
        $who = getClientIpServer();
1422
    }
1423
1424
    // Load class DB
1425
    loadClasses('DB');
1426
1427
    DB::insert(
1428
        prefixTable('log_system'),
1429
        [
1430
            'type' => $type,
1431
            'date' => time(),
1432
            'label' => $label,
1433
            'qui' => $who,
1434
            'field_1' => $field_1 === null ? '' : $field_1,
1435
        ]
1436
    );
1437
    // If SYSLOG
1438
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1439
        if ($type === 'user_mngt') {
1440
            send_syslog(
1441
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1442
                $SETTINGS['syslog_host'],
1443
                $SETTINGS['syslog_port'],
1444
                'teampass'
1445
            );
1446
        } else {
1447
            send_syslog(
1448
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1449
                $SETTINGS['syslog_host'],
1450
                $SETTINGS['syslog_port'],
1451
                'teampass'
1452
            );
1453
        }
1454
    }
1455
}
1456
1457
/**
1458
 * Log events.
1459
 *
1460
 * @param array  $SETTINGS        Teampass settings
1461
 * @param int    $item_id         Item id
1462
 * @param string $item_label      Item label
1463
 * @param int    $id_user         User id
1464
 * @param string $action          Code for reason
1465
 * @param string $login           User login
1466
 * @param string $raison          Code for reason
1467
 * @param string $encryption_type Encryption on
1468
 * @param string $time Encryption Time
1469
 * @param string $old_value       Old value
1470
 * 
1471
 * @return void
1472
 */
1473
function logItems(
1474
    array $SETTINGS,
1475
    int $item_id,
1476
    string $item_label,
1477
    int $id_user,
1478
    string $action,
1479
    ?string $login = null,
1480
    ?string $raison = null,
1481
    ?string $encryption_type = null,
1482
    ?string $time = null,
1483
    ?string $old_value = null
1484
): void {
1485
    // Load class DB
1486
    loadClasses('DB');
1487
1488
    // Insert log in DB
1489
    DB::insert(
1490
        prefixTable('log_items'),
1491
        [
1492
            'id_item' => $item_id,
1493
            'date' => is_null($time) === true ? time() : $time,
1494
            'id_user' => $id_user,
1495
            'action' => $action,
1496
            'raison' => $raison,
1497
            'old_value' => $old_value,
1498
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1499
        ]
1500
    );
1501
    // Timestamp the last change
1502
    if (in_array($action, ['at_creation', 'at_modifiation', 'at_delete', 'at_import'], true)) {
1503
        DB::update(
1504
            prefixTable('misc'),
1505
            [
1506
                'valeur' => time(),
1507
                'updated_at' => time(),
1508
            ],
1509
            'type = %s AND intitule = %s',
1510
            'timestamp',
1511
            'last_item_change'
1512
        );
1513
    }
1514
1515
    // SYSLOG
1516
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1517
        // Extract reason
1518
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1519
        // Get item info if not known
1520
        if (empty($item_label) === true) {
1521
            $dataItem = DB::queryFirstRow(
1522
                'SELECT id, id_tree, label
1523
                FROM ' . prefixTable('items') . '
1524
                WHERE id = %i',
1525
                $item_id
1526
            );
1527
            $item_label = $dataItem['label'];
1528
        }
1529
1530
        send_syslog(
1531
            'action=' . str_replace('at_', '', $action) .
1532
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1533
                ' itemno=' . $item_id .
1534
                ' user=' . (is_null($login) === true ? '' : addslashes((string) $login)) .
1535
                ' itemname="' . addslashes($item_label) . '"',
1536
            $SETTINGS['syslog_host'],
1537
            $SETTINGS['syslog_port'],
1538
            'teampass'
1539
        );
1540
    }
1541
1542
    // send notification if enabled
1543
    //notifyOnChange($item_id, $action, $SETTINGS);
1544
}
1545
1546
/**
1547
 * Prepare notification email to subscribers.
1548
 *
1549
 * @param int    $item_id  Item id
1550
 * @param string $label    Item label
1551
 * @param array  $changes  List of changes
1552
 * @param array  $SETTINGS Teampass settings
1553
 * 
1554
 * @return void
1555
 */
1556
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1557
{
1558
    $session = SessionManager::getSession();
1559
    $lang = new Language($session->get('user-language') ?? 'english');
1560
    $globalsUserId = $session->get('user-id');
1561
    $globalsLastname = $session->get('user-lastname');
1562
    $globalsName = $session->get('user-name');
1563
    // send email to user that what to be notified
1564
    $notification = DB::queryFirstField(
1565
        'SELECT email
1566
        FROM ' . prefixTable('notification') . ' AS n
1567
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1568
        WHERE n.item_id = %i AND n.user_id != %i',
1569
        $item_id,
1570
        $globalsUserId
1571
    );
1572
    if (DB::count() > 0) {
1573
        // Prepare path
1574
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1575
        // Get list of changes
1576
        $htmlChanges = '<ul>';
1577
        foreach ($changes as $change) {
1578
            $htmlChanges .= '<li>' . $change . '</li>';
1579
        }
1580
        $htmlChanges .= '</ul>';
1581
        // send email
1582
        DB::insert(
1583
            prefixTable('emails'),
1584
            [
1585
                'timestamp' => time(),
1586
                'subject' => $lang->get('email_subject_item_updated'),
1587
                'body' => str_replace(
1588
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1589
                    [$label, $path, (string) $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1590
                    $lang->get('email_body_item_updated')
1591
                ),
1592
                'receivers' => implode(',', $notification),
1593
                'status' => '',
1594
            ]
1595
        );
1596
    }
1597
}
1598
1599
/**
1600
 * Returns the Item + path.
1601
 *
1602
 * @param int    $id_tree  Node id
1603
 * @param string $label    Label
1604
 * @param array  $SETTINGS TP settings
1605
 * 
1606
 * @return string
1607
 */
1608
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1609
{
1610
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1611
    $arbo = $tree->getPath($id_tree, true);
1612
    $path = '';
1613
    foreach ($arbo as $elem) {
1614
        if (empty($path) === true) {
1615
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1616
        } else {
1617
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1618
        }
1619
    }
1620
1621
    // Build text to show user
1622
    if (empty($label) === false) {
1623
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1624
    }
1625
    return empty($path) === true ? '' : $path;
1626
}
1627
1628
/**
1629
 * Get the client ip address.
1630
 *
1631
 * @return string IP address
1632
 */
1633
function getClientIpServer(): string
1634
{
1635
    if (getenv('HTTP_CLIENT_IP')) {
1636
        $ipaddress = getenv('HTTP_CLIENT_IP');
1637
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1638
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1639
    } elseif (getenv('HTTP_X_FORWARDED')) {
1640
        $ipaddress = getenv('HTTP_X_FORWARDED');
1641
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1642
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1643
    } elseif (getenv('HTTP_FORWARDED')) {
1644
        $ipaddress = getenv('HTTP_FORWARDED');
1645
    } elseif (getenv('REMOTE_ADDR')) {
1646
        $ipaddress = getenv('REMOTE_ADDR');
1647
    } else {
1648
        $ipaddress = 'UNKNOWN';
1649
    }
1650
1651
    return $ipaddress;
1652
}
1653
1654
/**
1655
 * Escape all HTML, JavaScript, and CSS.
1656
 *
1657
 * @param string $input    The input string
1658
 * @param string $encoding Which character encoding are we using?
1659
 * 
1660
 * @return string
1661
 */
1662
function noHTML(string $input, string $encoding = 'UTF-8'): string
1663
{
1664
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1665
}
1666
1667
/**
1668
 * Rebuilds the Teampass config file.
1669
 *
1670
 * @param string $configFilePath Path to the config file.
1671
 * @param array  $settings       Teampass settings.
1672
 *
1673
 * @return string|bool
1674
 */
1675
function rebuildConfigFile(string $configFilePath, array $settings)
1676
{
1677
    // Perform a copy if the file exists
1678
    if (file_exists($configFilePath)) {
1679
        $backupFilePath = $configFilePath . '.' . date('Y_m_d_His', time());
1680
        if (!copy($configFilePath, $backupFilePath)) {
1681
            return "ERROR: Could not copy file '$configFilePath'";
1682
        }
1683
    }
1684
1685
    // Regenerate the config file
1686
    $data = ["<?php\n", "global \$SETTINGS;\n", "\$SETTINGS = array (\n"];
1687
    $rows = DB::query('SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s', 'admin');
1688
    foreach ($rows as $record) {
1689
        $value = getEncryptedValue($record['valeur'], $record['is_encrypted']);
1690
        $data[] = "    '{$record['intitule']}' => '". htmlspecialchars_decode($value, ENT_COMPAT) . "',\n";
1691
    }
1692
    $data[] = ");\n";
1693
    $data = array_unique($data);
1694
1695
    // Update the file
1696
    file_put_contents($configFilePath, implode('', $data));
1697
1698
    return true;
1699
}
1700
1701
/**
1702
 * Returns the encrypted value if needed.
1703
 *
1704
 * @param string $value       Value to encrypt.
1705
 * @param int   $isEncrypted Is the value encrypted?
1706
 *
1707
 * @return string
1708
 */
1709
function getEncryptedValue(string $value, int $isEncrypted): string
1710
{
1711
    return $isEncrypted ? cryption($value, '', 'encrypt')['string'] : $value;
1712
}
1713
1714
/**
1715
 * Permits to replace &#92; to permit correct display
1716
 *
1717
 * @param string $input Some text
1718
 * 
1719
 * @return string
1720
 */
1721
function handleBackslash(string $input): string
1722
{
1723
    return str_replace('&amp;#92;', '&#92;', $input);
1724
}
1725
1726
/**
1727
 * Permits to load settings
1728
 * 
1729
 * @return void
1730
*/
1731
function loadSettings(): void
1732
{
1733
    global $SETTINGS;
1734
    /* LOAD CPASSMAN SETTINGS */
1735
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1736
        $SETTINGS = [];
1737
        $SETTINGS['duplicate_folder'] = 0;
1738
        //by default, this is set to 0;
1739
        $SETTINGS['duplicate_item'] = 0;
1740
        //by default, this is set to 0;
1741
        $SETTINGS['number_of_used_pw'] = 5;
1742
        //by default, this value is set to 5;
1743
        $settings = [];
1744
        $rows = DB::query(
1745
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1746
            [
1747
                'type' => 'admin',
1748
                'type2' => 'settings',
1749
            ]
1750
        );
1751
        foreach ($rows as $record) {
1752
            if ($record['type'] === 'admin') {
1753
                $SETTINGS[$record['intitule']] = $record['valeur'];
1754
            } else {
1755
                $settings[$record['intitule']] = $record['valeur'];
1756
            }
1757
        }
1758
        $SETTINGS['loaded'] = 1;
1759
        $SETTINGS['default_session_expiration_time'] = 5;
1760
    }
1761
}
1762
1763
/**
1764
 * check if folder has custom fields.
1765
 * Ensure that target one also has same custom fields
1766
 * 
1767
 * @param int $source_id
1768
 * @param int $target_id 
1769
 * 
1770
 * @return bool
1771
*/
1772
function checkCFconsistency(int $source_id, int $target_id): bool
1773
{
1774
    $source_cf = [];
1775
    $rows = DB::query(
1776
        'SELECT id_category
1777
            FROM ' . prefixTable('categories_folders') . '
1778
            WHERE id_folder = %i',
1779
        $source_id
1780
    );
1781
    foreach ($rows as $record) {
1782
        array_push($source_cf, $record['id_category']);
1783
    }
1784
1785
    $target_cf = [];
1786
    $rows = DB::query(
1787
        'SELECT id_category
1788
            FROM ' . prefixTable('categories_folders') . '
1789
            WHERE id_folder = %i',
1790
        $target_id
1791
    );
1792
    foreach ($rows as $record) {
1793
        array_push($target_cf, $record['id_category']);
1794
    }
1795
1796
    $cf_diff = array_diff($source_cf, $target_cf);
1797
    if (count($cf_diff) > 0) {
1798
        return false;
1799
    }
1800
1801
    return true;
1802
}
1803
1804
/**
1805
 * Will encrypte/decrypt a fil eusing Defuse.
1806
 *
1807
 * @param string $type        can be either encrypt or decrypt
1808
 * @param string $source_file path to source file
1809
 * @param string $target_file path to target file
1810
 * @param array  $SETTINGS    Settings
1811
 * @param string $password    A password
1812
 *
1813
 * @return string|bool
1814
 */
1815
function prepareFileWithDefuse(
1816
    string $type,
1817
    string $source_file,
1818
    string $target_file,
1819
    ?string $password = null
1820
) {
1821
    // Load AntiXSS
1822
    $antiXss = new AntiXSS();
1823
    // Protect against bad inputs
1824
    if (is_array($source_file) === true || is_array($target_file) === true) {
1825
        return 'error_cannot_be_array';
1826
    }
1827
1828
    // Sanitize
1829
    $source_file = $antiXss->xss_clean($source_file);
1830
    $target_file = $antiXss->xss_clean($target_file);
1831
    if (empty($password) === true || is_null($password) === true) {
1832
        // get KEY to define password
1833
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
1834
        $password = Key::loadFromAsciiSafeString($ascii_key);
1835
    }
1836
1837
    $err = '';
1838
    if ($type === 'decrypt') {
1839
        // Decrypt file
1840
        $err = defuseFileDecrypt(
1841
            $source_file,
1842
            $target_file,
1843
            $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

1843
            /** @scrutinizer ignore-type */ $password
Loading history...
1844
        );
1845
    } elseif ($type === 'encrypt') {
1846
        // Encrypt file
1847
        $err = defuseFileEncrypt(
1848
            $source_file,
1849
            $target_file,
1850
            $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

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