Issues (24)

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 (1 issue)

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       $idFonctions          [description]
259
 *
260
 * @return bool
261
 */
262
function identifyUserRights(
263
    $groupesVisiblesUser,
264
    $groupesInterditsUser,
265
    $isAdmin,
266
    $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
            $SETTINGS, /** @scrutinizer ignore-type */
277
            $tree
278
        )
279
        :
280
        identUser(
281
            $groupesVisiblesUser,
282
            $groupesInterditsUser,
283
            $idFonctions,
284
            $SETTINGS, /** @scrutinizer ignore-type */
285
            $tree
286
        );
287
288
    // update user's timestamp
289
    DB::update(
290
        prefixTable('users'),
291
        [
292
            'timestamp' => time(),
293
        ],
294
        'id=%i',
295
        $session->get('user-id')
296
    );
297
298
    return true;
299
}
300
301
/**
302
 * Identify administrator.
303
 *
304
 * @param string $idFonctions Roles of user
305
 * @param array  $SETTINGS    Teampass settings
306
 * @param object $tree        Tree of folders
307
 *
308
 * @return bool
309
 */
310
function identAdmin($idFonctions, $SETTINGS, $tree)
311
{
312
    
313
    $session = SessionManager::getSession();
314
    $groupesVisibles = [];
315
    $session->set('user-personal_folders', []);
316
    $session->set('user-accessible_folders', []);
317
    $session->set('user-no_access_folders', []);
318
    $session->set('user-personal_visible_folders', []);
319
    $session->set('user-read_only_folders', []);
320
    $session->set('system-list_restricted_folders_for_items', []);
321
    $session->set('system-list_folders_editable_by_role', []);
322
    $session->set('user-list_folders_limited', []);
323
    $session->set('user-forbiden_personal_folders', []);
324
    
325
    // Get list of Folders
326
    $rows = DB::query('SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i', 0);
327
    foreach ($rows as $record) {
328
        array_push($groupesVisibles, $record['id']);
329
    }
330
    $session->set('user-accessible_folders', $groupesVisibles);
331
332
    // get complete list of ROLES
333
    $tmp = explode(';', $idFonctions);
334
    $rows = DB::query(
335
        'SELECT * FROM ' . prefixTable('roles_title') . '
336
        ORDER BY title ASC'
337
    );
338
    foreach ($rows as $record) {
339
        if (! empty($record['id']) && ! in_array($record['id'], $tmp)) {
340
            array_push($tmp, $record['id']);
341
        }
342
    }
343
    $session->set('user-roles', implode(';', $tmp));
344
    $session->set('user-admin', 1);
345
    // Check if admin has created Folders and Roles
346
    DB::query('SELECT * FROM ' . prefixTable('nested_tree') . '');
347
    $session->set('user-nb_folders', DB::count());
348
    DB::query('SELECT * FROM ' . prefixTable('roles_title'));
349
    $session->set('user-nb_roles', DB::count());
350
351
    return true;
352
}
353
354
/**
355
 * Permits to convert an element to array.
356
 *
357
 * @param string|array $element Any value to be returned as array
358
 *
359
 * @return array
360
 */
361
function convertToArray($element): ?array
362
{
363
    if (is_string($element) === true) {
364
        if (empty($element) === true) {
365
            return [];
366
        }
367
        return explode(
368
            ';',
369
            trimElement($element, ';')
370
        );
371
    }
372
    return $element;
373
}
374
375
/**
376
 * Defines the rights the user has.
377
 *
378
 * @param string|array $allowedFolders  Allowed folders
379
 * @param string|array $noAccessFolders Not allowed folders
380
 * @param string|array $userRoles       Roles of user
381
 * @param array        $SETTINGS        Teampass settings
382
 * @param object       $tree            Tree of folders
383
 * 
384
 * @return bool
385
 */
386
function identUser(
387
    $allowedFolders,
388
    $noAccessFolders,
389
    $userRoles,
390
    array $SETTINGS,
391
    object $tree
392
) {
393
    $session = SessionManager::getSession();
394
    // Init
395
    $session->set('user-accessible_folders', []);
396
    $session->set('user-personal_folders', []);
397
    $session->set('user-no_access_folders', []);
398
    $session->set('user-personal_visible_folders', []);
399
    $session->set('user-read_only_folders', []);
400
    $session->set('user-user-roles', $userRoles);
401
    $session->set('user-admin', 0);
402
    // init
403
    $personalFolders = [];
404
    $readOnlyFolders = [];
405
    $noAccessPersonalFolders = [];
406
    $restrictedFoldersForItems = [];
407
    $foldersLimited = [];
408
    $foldersLimitedFull = [];
409
    $allowedFoldersByRoles = [];
410
    $globalsUserId = $session->get('user-id');
411
    $globalsPersonalFolders = $session->get('user-personal_folder_enabled');
412
    // Ensure consistency in array format
413
    $noAccessFolders = convertToArray($noAccessFolders);
414
    $userRoles = convertToArray($userRoles);
415
    $allowedFolders = convertToArray($allowedFolders);
416
    $session->set('user-allowed_folders_by_definition', $allowedFolders);
417
    
418
    // Get list of folders depending on Roles
419
    $arrays = identUserGetFoldersFromRoles(
420
        $userRoles,
421
        $allowedFoldersByRoles,
422
        $readOnlyFolders,
423
        $allowedFolders
424
    );
425
    $allowedFoldersByRoles = $arrays['allowedFoldersByRoles'];
426
    $readOnlyFolders = $arrays['readOnlyFolders'];
427
428
    // Does this user is allowed to see other items
429
    $inc = 0;
430
    $rows = DB::query(
431
        'SELECT id, id_tree FROM ' . prefixTable('items') . '
432
            WHERE restricted_to LIKE %ss AND inactif = %s'.
433
            (count($allowedFolders) > 0 ? ' AND id_tree NOT IN ('.implode(',', $allowedFolders).')' : ''),
434
        $globalsUserId,
435
        '0'
436
    );
437
    foreach ($rows as $record) {
438
        // Exclude restriction on item if folder is fully accessible
439
        //if (in_array($record['id_tree'], $allowedFolders) === false) {
440
            $restrictedFoldersForItems[$record['id_tree']][$inc] = $record['id'];
441
            ++$inc;
442
        //}
443
    }
444
445
    // Check for the users roles if some specific rights exist on items
446
    $rows = DB::query(
447
        'SELECT i.id_tree, r.item_id
448
        FROM ' . prefixTable('items') . ' as i
449
        INNER JOIN ' . prefixTable('restriction_to_roles') . ' as r ON (r.item_id=i.id)
450
        WHERE i.id_tree <> "" '.
451
        (count($userRoles) > 0 ? 'AND r.role_id IN %li ' : '').
452
        'ORDER BY i.id_tree ASC',
453
        $userRoles
454
    );
455
    $inc = 0;
456
    foreach ($rows as $record) {
457
        //if (isset($record['id_tree'])) {
458
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
459
            array_push($foldersLimitedFull, $record['id_tree']);
460
            ++$inc;
461
        //}
462
    }
463
464
    // Get list of Personal Folders
465
    $arrays = identUserGetPFList(
466
        $globalsPersonalFolders,
467
        $allowedFolders,
468
        $globalsUserId,
469
        $personalFolders,
470
        $noAccessPersonalFolders,
471
        $foldersLimitedFull,
472
        $allowedFoldersByRoles,
473
        array_keys($restrictedFoldersForItems),
474
        $readOnlyFolders,
475
        $noAccessFolders,
476
        isset($SETTINGS['enable_pf_feature']) === true ? $SETTINGS['enable_pf_feature'] : 0,
477
        $tree
478
    );
479
    $allowedFolders = $arrays['allowedFolders'];
480
    $personalFolders = $arrays['personalFolders'];
481
    $noAccessPersonalFolders = $arrays['noAccessPersonalFolders'];
482
483
    // Return data
484
    $session->set('user-accessible_folders', array_unique(array_merge($allowedFolders, $personalFolders), SORT_NUMERIC));
485
    $session->set('user-read_only_folders', $readOnlyFolders);
486
    $session->set('user-no_access_folders', $noAccessFolders);
487
    $session->set('user-personal_folders', $personalFolders);
488
    $session->set('user-list_folders_limited', $foldersLimited);
489
    $session->set('system-list_folders_editable_by_role', $allowedFoldersByRoles, 'SESSION');
490
    $session->set('system-list_restricted_folders_for_items', $restrictedFoldersForItems);
491
    $session->set('user-forbiden_personal_folders', $noAccessPersonalFolders);
492
    // Folders and Roles numbers
493
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('nested_tree') . '');
494
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('nested_tree') . '');
495
    $session->set('user-nb_folders', DB::count());
496
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('roles_title'));
497
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('roles_title'));
498
    $session->set('user-nb_roles', DB::count());
499
    // check if change proposals on User's items
500
    if (isset($SETTINGS['enable_suggestion']) === true && (int) $SETTINGS['enable_suggestion'] === 1) {
501
        $countNewItems = DB::query(
502
            'SELECT COUNT(*)
503
            FROM ' . prefixTable('items_change') . ' AS c
504
            LEFT JOIN ' . prefixTable('log_items') . ' AS i ON (c.item_id = i.id_item)
505
            WHERE i.action = %s AND i.id_user = %i',
506
            'at_creation',
507
            $globalsUserId
508
        );
509
        $session->set('user-nb_item_change_proposals', $countNewItems);
510
    } else {
511
        $session->set('user-nb_item_change_proposals', 0);
512
    }
513
514
    return true;
515
}
516
517
/**
518
 * Get list of folders depending on Roles
519
 * 
520
 * @param array $userRoles
521
 * @param array $allowedFoldersByRoles
522
 * @param array $readOnlyFolders
523
 * @param array $allowedFolders
524
 * 
525
 * @return array
526
 */
527
function identUserGetFoldersFromRoles(array $userRoles, array $allowedFoldersByRoles = [], array $readOnlyFolders = [], array $allowedFolders = []) : array
528
{
529
    $rows = DB::query(
530
        'SELECT *
531
        FROM ' . prefixTable('roles_values') . '
532
        WHERE type IN %ls'.(count($userRoles) > 0 ? ' AND role_id IN %li' : ''),
533
        ['W', 'ND', 'NE', 'NDNE', 'R'],
534
        $userRoles,
535
    );
536
    foreach ($rows as $record) {
537
        if ($record['type'] === 'R') {
538
            array_push($readOnlyFolders, $record['folder_id']);
539
        } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
540
            array_push($allowedFoldersByRoles, $record['folder_id']);
541
        }
542
    }
543
    $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
544
    $readOnlyFolders = array_unique($readOnlyFolders);
545
    
546
    // Clean arrays
547
    foreach ($allowedFoldersByRoles as $value) {
548
        $key = array_search($value, $readOnlyFolders);
549
        if ($key !== false) {
550
            unset($readOnlyFolders[$key]);
551
        }
552
    }
553
    return [
554
        'readOnlyFolders' => $readOnlyFolders,
555
        'allowedFoldersByRoles' => $allowedFoldersByRoles
556
    ];
557
}
558
559
/**
560
 * Get list of Personal Folders
561
 * 
562
 * @param int $globalsPersonalFolders
563
 * @param array $allowedFolders
564
 * @param int $globalsUserId
565
 * @param array $personalFolders
566
 * @param array $noAccessPersonalFolders
567
 * @param array $foldersLimitedFull
568
 * @param array $allowedFoldersByRoles
569
 * @param array $restrictedFoldersForItems
570
 * @param array $readOnlyFolders
571
 * @param array $noAccessFolders
572
 * @param int $enablePfFeature
573
 * @param object $tree
574
 * 
575
 * @return array
576
 */
577
function identUserGetPFList(
578
    $globalsPersonalFolders,
579
    $allowedFolders,
580
    $globalsUserId,
581
    $personalFolders,
582
    $noAccessPersonalFolders,
583
    $foldersLimitedFull,
584
    $allowedFoldersByRoles,
585
    $restrictedFoldersForItems,
586
    $readOnlyFolders,
587
    $noAccessFolders,
588
    $enablePfFeature,
589
    $tree
590
)
591
{
592
    if (
593
        (int) $enablePfFeature === 1
594
        && (int) $globalsPersonalFolders === 1
595
    ) {
596
        $persoFld = DB::queryFirstRow(
597
            'SELECT id
598
            FROM ' . prefixTable('nested_tree') . '
599
            WHERE title = %s AND personal_folder = %i'.
600
            (count($allowedFolders) > 0 ? ' AND id NOT IN ('.implode(',', $allowedFolders).')' : ''),
601
            $globalsUserId,
602
            1
603
        );
604
        if (empty($persoFld['id']) === false) {
605
            array_push($personalFolders, $persoFld['id']);
606
            array_push($allowedFolders, $persoFld['id']);
607
            // get all descendants
608
            $ids = $tree->getDescendants($persoFld['id'], false, false, true);
609
            foreach ($ids as $id) {
610
                //array_push($allowedFolders, $id);
611
                array_push($personalFolders, $id);
612
            }
613
        }
614
    }
615
    
616
    // Exclude all other PF
617
    $where = new WhereClause('and');
618
    $where->add('personal_folder=%i', 1);
619
    if (count($personalFolders) > 0) {
620
        $where->add('id NOT IN ('.implode(',', $personalFolders).')');
621
    }
622
    if (
623
        (int) $enablePfFeature === 1
624
        && (int) $globalsPersonalFolders === 1
625
    ) {
626
        $where->add('title=%s', $globalsUserId);
627
        $where->negateLast();
628
    }
629
    $persoFlds = DB::query(
630
        'SELECT id
631
        FROM ' . prefixTable('nested_tree') . '
632
        WHERE %l',
633
        $where
634
    );
635
    foreach ($persoFlds as $persoFldId) {
636
        array_push($noAccessPersonalFolders, $persoFldId['id']);
637
    }
638
639
    // All folders visibles
640
    $allowedFolders = array_unique(array_merge(
641
        $allowedFolders,
642
        $foldersLimitedFull,
643
        $allowedFoldersByRoles,
644
        $restrictedFoldersForItems,
645
        $readOnlyFolders
646
    ), SORT_NUMERIC);
647
    // Exclude from allowed folders all the specific user forbidden folders
648
    if (count($noAccessFolders) > 0) {
649
        $allowedFolders = array_diff($allowedFolders, $noAccessFolders);
650
    }
651
652
    return [
653
        'allowedFolders' => array_diff(array_diff($allowedFolders, $noAccessPersonalFolders), $personalFolders),
654
        'personalFolders' => $personalFolders,
655
        'noAccessPersonalFolders' => $noAccessPersonalFolders
656
    ];
657
}
658
659
660
/**
661
 * Update the CACHE table.
662
 *
663
 * @param string $action   What to do
664
 * @param array  $SETTINGS Teampass settings
665
 * @param int    $ident    Ident format
666
 * 
667
 * @return void
668
 */
669
function updateCacheTable(string $action, ?int $ident = null): void
670
{
671
    if ($action === 'reload') {
672
        // Rebuild full cache table
673
        cacheTableRefresh();
674
    } elseif ($action === 'update_value' && is_null($ident) === false) {
675
        // UPDATE an item
676
        cacheTableUpdate($ident);
677
    } elseif ($action === 'add_value' && is_null($ident) === false) {
678
        // ADD an item
679
        cacheTableAdd($ident);
680
    } elseif ($action === 'delete_value' && is_null($ident) === false) {
681
        // DELETE an item
682
        DB::delete(prefixTable('cache'), 'id = %i', $ident);
683
    }
684
}
685
686
/**
687
 * Cache table - refresh.
688
 *
689
 * @return void
690
 */
691
function cacheTableRefresh(): void
692
{
693
    // Load class DB
694
    loadClasses('DB');
695
696
    //Load Tree
697
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
698
    // truncate table
699
    DB::query('TRUNCATE TABLE ' . prefixTable('cache'));
700
    // reload date
701
    $rows = DB::query(
702
        'SELECT *
703
        FROM ' . prefixTable('items') . ' as i
704
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
705
        AND l.action = %s
706
        AND i.inactif = %i',
707
        'at_creation',
708
        0
709
    );
710
    foreach ($rows as $record) {
711
        if (empty($record['id_tree']) === false) {
712
            // Get all TAGS
713
            $tags = '';
714
            $itemTags = DB::query(
715
                'SELECT tag
716
                FROM ' . prefixTable('tags') . '
717
                WHERE item_id = %i AND tag != ""',
718
                $record['id']
719
            );
720
            foreach ($itemTags as $itemTag) {
721
                $tags .= $itemTag['tag'] . ' ';
722
            }
723
724
            // Get renewal period
725
            $resNT = DB::queryFirstRow(
726
                'SELECT renewal_period
727
                FROM ' . prefixTable('nested_tree') . '
728
                WHERE id = %i',
729
                $record['id_tree']
730
            );
731
            // form id_tree to full foldername
732
            $folder = [];
733
            $arbo = $tree->getPath($record['id_tree'], true);
734
            foreach ($arbo as $elem) {
735
                // Check if title is the ID of a user
736
                if (is_numeric($elem->title) === true) {
737
                    // Is this a User id?
738
                    $user = DB::queryFirstRow(
739
                        'SELECT id, login
740
                        FROM ' . prefixTable('users') . '
741
                        WHERE id = %i',
742
                        $elem->title
743
                    );
744
                    if (count($user) > 0) {
745
                        $elem->title = $user['login'];
746
                    }
747
                }
748
                // Build path
749
                array_push($folder, stripslashes($elem->title));
750
            }
751
            // store data
752
            DB::insert(
753
                prefixTable('cache'),
754
                [
755
                    'id' => $record['id'],
756
                    'label' => $record['label'],
757
                    'description' => $record['description'] ?? '',
758
                    'url' => isset($record['url']) && ! empty($record['url']) ? $record['url'] : '0',
759
                    'tags' => $tags,
760
                    'id_tree' => $record['id_tree'],
761
                    'perso' => $record['perso'],
762
                    'restricted_to' => isset($record['restricted_to']) && ! empty($record['restricted_to']) ? $record['restricted_to'] : '0',
763
                    'login' => $record['login'] ?? '',
764
                    'folder' => implode(' » ', $folder),
765
                    'author' => $record['id_user'],
766
                    'renewal_period' => $resNT['renewal_period'] ?? '0',
767
                    'timestamp' => $record['date'],
768
                ]
769
            );
770
        }
771
    }
772
}
773
774
/**
775
 * Cache table - update existing value.
776
 *
777
 * @param int    $ident    Ident format
778
 * 
779
 * @return void
780
 */
781
function cacheTableUpdate(?int $ident = null): void
782
{
783
    $session = SessionManager::getSession();
784
    loadClasses('DB');
785
786
    //Load Tree
787
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
788
    // get new value from db
789
    $data = DB::queryFirstRow(
790
        'SELECT label, description, id_tree, perso, restricted_to, login, url
791
        FROM ' . prefixTable('items') . '
792
        WHERE id=%i',
793
        $ident
794
    );
795
    // Get all TAGS
796
    $tags = '';
797
    $itemTags = DB::query(
798
        'SELECT tag
799
            FROM ' . prefixTable('tags') . '
800
            WHERE item_id = %i AND tag != ""',
801
        $ident
802
    );
803
    foreach ($itemTags as $itemTag) {
804
        $tags .= $itemTag['tag'] . ' ';
805
    }
806
    // form id_tree to full foldername
807
    $folder = [];
808
    $arbo = $tree->getPath($data['id_tree'], true);
809
    foreach ($arbo as $elem) {
810
        // Check if title is the ID of a user
811
        if (is_numeric($elem->title) === true) {
812
            // Is this a User id?
813
            $user = DB::queryFirstRow(
814
                'SELECT id, login
815
                FROM ' . prefixTable('users') . '
816
                WHERE id = %i',
817
                $elem->title
818
            );
819
            if (count($user) > 0) {
820
                $elem->title = $user['login'];
821
            }
822
        }
823
        // Build path
824
        array_push($folder, stripslashes($elem->title));
825
    }
826
    // finaly update
827
    DB::update(
828
        prefixTable('cache'),
829
        [
830
            'label' => $data['label'],
831
            'description' => $data['description'],
832
            'tags' => $tags,
833
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
834
            'id_tree' => $data['id_tree'],
835
            'perso' => $data['perso'],
836
            'restricted_to' => isset($data['restricted_to']) && ! empty($data['restricted_to']) ? $data['restricted_to'] : '0',
837
            'login' => $data['login'] ?? '',
838
            'folder' => implode(' » ', $folder),
839
            'author' => $session->get('user-id'),
840
        ],
841
        'id = %i',
842
        $ident
843
    );
844
}
845
846
/**
847
 * Cache table - add new value.
848
 *
849
 * @param int    $ident    Ident format
850
 * 
851
 * @return void
852
 */
853
function cacheTableAdd(?int $ident = null): void
854
{
855
    $session = SessionManager::getSession();
856
    $globalsUserId = $session->get('user-id');
857
858
    // Load class DB
859
    loadClasses('DB');
860
861
    //Load Tree
862
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
863
    // get new value from db
864
    $data = DB::queryFirstRow(
865
        'SELECT i.label, i.description, i.id_tree as id_tree, i.perso, i.restricted_to, i.id, i.login, i.url, l.date
866
        FROM ' . prefixTable('items') . ' as i
867
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
868
        WHERE i.id = %i
869
        AND l.action = %s',
870
        $ident,
871
        'at_creation'
872
    );
873
    // Get all TAGS
874
    $tags = '';
875
    $itemTags = DB::query(
876
        'SELECT tag
877
            FROM ' . prefixTable('tags') . '
878
            WHERE item_id = %i AND tag != ""',
879
        $ident
880
    );
881
    foreach ($itemTags as $itemTag) {
882
        $tags .= $itemTag['tag'] . ' ';
883
    }
884
    // form id_tree to full foldername
885
    $folder = [];
886
    $arbo = $tree->getPath($data['id_tree'], true);
887
    foreach ($arbo as $elem) {
888
        // Check if title is the ID of a user
889
        if (is_numeric($elem->title) === true) {
890
            // Is this a User id?
891
            $user = DB::queryFirstRow(
892
                'SELECT id, login
893
                FROM ' . prefixTable('users') . '
894
                WHERE id = %i',
895
                $elem->title
896
            );
897
            if (count($user) > 0) {
898
                $elem->title = $user['login'];
899
            }
900
        }
901
        // Build path
902
        array_push($folder, stripslashes($elem->title));
903
    }
904
    // finaly update
905
    DB::insert(
906
        prefixTable('cache'),
907
        [
908
            'id' => $data['id'],
909
            'label' => $data['label'],
910
            'description' => $data['description'],
911
            'tags' => empty($tags) === false ? $tags : 'None',
912
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
913
            'id_tree' => $data['id_tree'],
914
            'perso' => isset($data['perso']) && empty($data['perso']) === false && $data['perso'] !== 'None' ? $data['perso'] : '0',
915
            'restricted_to' => isset($data['restricted_to']) && empty($data['restricted_to']) === false ? $data['restricted_to'] : '0',
916
            'login' => $data['login'] ?? '',
917
            'folder' => implode(' » ', $folder),
918
            'author' => $globalsUserId,
919
            'timestamp' => $data['date'],
920
        ]
921
    );
922
}
923
924
/**
925
 * Do statistics.
926
 *
927
 * @param array $SETTINGS Teampass settings
928
 *
929
 * @return array
930
 */
931
function getStatisticsData(array $SETTINGS): array
932
{
933
    DB::query(
934
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
935
        0
936
    );
937
    $counter_folders = DB::count();
938
    DB::query(
939
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
940
        1
941
    );
942
    $counter_folders_perso = DB::count();
943
    DB::query(
944
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
945
        0
946
    );
947
    $counter_items = DB::count();
948
        DB::query(
949
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
950
        1
951
    );
952
    $counter_items_perso = DB::count();
953
        DB::query(
954
        'SELECT id FROM ' . prefixTable('users') . ' WHERE login NOT IN (%s, %s, %s)',
955
        'OTV', 'TP', 'API'
956
    );
957
    $counter_users = DB::count();
958
        DB::query(
959
        'SELECT id FROM ' . prefixTable('users') . ' WHERE admin = %i',
960
        1
961
    );
962
    $admins = DB::count();
963
    DB::query(
964
        'SELECT id FROM ' . prefixTable('users') . ' WHERE gestionnaire = %i',
965
        1
966
    );
967
    $managers = DB::count();
968
    DB::query(
969
        'SELECT id FROM ' . prefixTable('users') . ' WHERE read_only = %i',
970
        1
971
    );
972
    $readOnly = DB::count();
973
    // list the languages
974
    $usedLang = [];
975
    $tp_languages = DB::query(
976
        'SELECT name FROM ' . prefixTable('languages')
977
    );
978
    foreach ($tp_languages as $tp_language) {
979
        DB::query(
980
            'SELECT * FROM ' . prefixTable('users') . ' WHERE user_language = %s',
981
            $tp_language['name']
982
        );
983
        $usedLang[$tp_language['name']] = round((DB::count() * 100 / $counter_users), 0);
984
    }
985
986
    // get list of ips
987
    $usedIp = [];
988
    $tp_ips = DB::query(
989
        'SELECT user_ip FROM ' . prefixTable('users')
990
    );
991
    foreach ($tp_ips as $ip) {
992
        if (array_key_exists($ip['user_ip'], $usedIp)) {
993
            $usedIp[$ip['user_ip']] += $usedIp[$ip['user_ip']];
994
        } elseif (! empty($ip['user_ip']) && $ip['user_ip'] !== 'none') {
995
            $usedIp[$ip['user_ip']] = 1;
996
        }
997
    }
998
999
    return [
1000
        'error' => '',
1001
        'stat_phpversion' => phpversion(),
1002
        'stat_folders' => $counter_folders,
1003
        'stat_folders_shared' => intval($counter_folders) - intval($counter_folders_perso),
1004
        'stat_items' => $counter_items,
1005
        'stat_items_shared' => intval($counter_items) - intval($counter_items_perso),
1006
        'stat_users' => $counter_users,
1007
        'stat_admins' => $admins,
1008
        'stat_managers' => $managers,
1009
        'stat_ro' => $readOnly,
1010
        'stat_kb' => $SETTINGS['enable_kb'],
1011
        'stat_pf' => $SETTINGS['enable_pf_feature'],
1012
        'stat_fav' => $SETTINGS['enable_favourites'],
1013
        'stat_teampassversion' => TP_VERSION,
1014
        'stat_ldap' => $SETTINGS['ldap_mode'],
1015
        'stat_agses' => $SETTINGS['agses_authentication_enabled'],
1016
        'stat_duo' => $SETTINGS['duo'],
1017
        'stat_suggestion' => $SETTINGS['enable_suggestion'],
1018
        'stat_api' => $SETTINGS['api'],
1019
        'stat_customfields' => $SETTINGS['item_extra_fields'],
1020
        'stat_syslog' => $SETTINGS['syslog_enable'],
1021
        'stat_2fa' => $SETTINGS['google_authentication'],
1022
        'stat_stricthttps' => $SETTINGS['enable_sts'],
1023
        'stat_mysqlversion' => DB::serverVersion(),
1024
        'stat_languages' => $usedLang,
1025
        'stat_country' => $usedIp,
1026
    ];
1027
}
1028
1029
/**
1030
 * Permits to prepare the way to send the email
1031
 * 
1032
 * @param string $subject       email subject
1033
 * @param string $body          email message
1034
 * @param string $email         email
1035
 * @param string $receiverName  Receiver name
1036
 * @param string $encryptedUserPassword      encryptedUserPassword
1037
 *
1038
 * @return void
1039
 */
1040
function prepareSendingEmail(
1041
    $subject,
1042
    $body,
1043
    $email,
1044
    $receiverName = '',
1045
    $encryptedUserPassword = ''
1046
): void 
1047
{
1048
    DB::insert(
1049
        prefixTable('background_tasks'),
1050
        array(
1051
            'created_at' => time(),
1052
            'process_type' => 'send_email',
1053
            'arguments' => json_encode([
1054
                'subject' => $subject,
1055
                'receivers' => $email,
1056
                'body' => $body,
1057
                'receiver_name' => $receiverName,
1058
                'encryptedUserPassword' => $encryptedUserPassword,
1059
            ], JSON_HEX_QUOT | JSON_HEX_TAG),
1060
        )
1061
    );
1062
}
1063
1064
/**
1065
 * Returns the email body.
1066
 *
1067
 * @param string $textMail Text for the email
1068
 */
1069
function emailBody(string $textMail): string
1070
{
1071
    return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
1072
    w3.org/TR/html4/loose.dtd"><html>
1073
    <head><title>Email Template</title>
1074
    <style type="text/css">
1075
    body { background-color: #f0f0f0; padding: 10px 0; margin:0 0 10px =0; }
1076
    </style></head>
1077
    <body style="-ms-text-size-adjust: none; size-adjust: none; margin: 0; padding: 10px 0; background-color: #f0f0f0;" bgcolor="#f0f0f0" leftmargin="0" topmargin="0" marginwidth="0" marginheight="0">
1078
    <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#f0f0f0" style="border-spacing: 0;">
1079
    <tr><td style="border-collapse: collapse;"><br>
1080
        <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#17357c" style="border-spacing: 0; margin-bottom: 25px;">
1081
        <tr><td style="border-collapse: collapse; padding: 11px 20px;">
1082
            <div style="max-width:150px; max-height:34px; color:#f0f0f0; font-weight:bold;">Teampass</div>
1083
        </td></tr></table></td>
1084
    </tr>
1085
    <tr><td align="center" valign="top" bgcolor="#f0f0f0" style="border-collapse: collapse; background-color: #f0f0f0;">
1086
        <table width="600" cellpadding="0" cellspacing="0" border="0" class="container" bgcolor="#ffffff" style="border-spacing: 0; border-bottom: 1px solid #e0e0e0; box-shadow: 0 0 3px #ddd; color: #434343; font-family: Helvetica, Verdana, sans-serif;">
1087
        <tr><td class="container-padding" bgcolor="#ffffff" style="border-collapse: collapse; border-left: 1px solid #e0e0e0; background-color: #ffffff; padding-left: 30px; padding-right: 30px;">
1088
        <br><div style="float:right;">' .
1089
        $textMail .
1090
        '<br><br></td></tr></table>
1091
    </td></tr></table>
1092
    <br></body></html>';
1093
}
1094
1095
/**
1096
 * Convert date to timestamp.
1097
 *
1098
 * @param string $date        The date
1099
 * @param string $date_format Date format
1100
 *
1101
 * @return int
1102
 */
1103
function dateToStamp(string $date, string $date_format): int
1104
{
1105
    $date = date_parse_from_format($date_format, $date);
1106
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1107
        return mktime(
1108
            empty($date['hour']) === false ? $date['hour'] : 23,
1109
            empty($date['minute']) === false ? $date['minute'] : 59,
1110
            empty($date['second']) === false ? $date['second'] : 59,
1111
            $date['month'],
1112
            $date['day'],
1113
            $date['year']
1114
        );
1115
    }
1116
    return 0;
1117
}
1118
1119
/**
1120
 * Is this a date.
1121
 *
1122
 * @param string $date Date
1123
 *
1124
 * @return bool
1125
 */
1126
function isDate(string $date): bool
1127
{
1128
    return strtotime($date) !== false;
1129
}
1130
1131
/**
1132
 * Check if isUTF8().
1133
 *
1134
 * @param string|array $string Is the string
1135
 *
1136
 * @return int is the string in UTF8 format
1137
 */
1138
function isUTF8($string): int
1139
{
1140
    if (is_array($string) === true) {
1141
        $string = $string['string'];
1142
    }
1143
1144
    return preg_match(
1145
        '%^(?:
1146
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1147
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1148
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1149
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1150
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1151
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1152
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1153
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1154
        )*$%xs',
1155
        $string
1156
    );
1157
}
1158
1159
/**
1160
 * Prepare an array to UTF8 format before JSON_encode.
1161
 *
1162
 * @param array $array Array of values
1163
 *
1164
 * @return array
1165
 */
1166
function utf8Converter(array $array): array
1167
{
1168
    array_walk_recursive(
1169
        $array,
1170
        static function (&$item): void {
1171
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1172
                $item = mb_convert_encoding($item, 'ISO-8859-1', 'UTF-8');
1173
            }
1174
        }
1175
    );
1176
    return $array;
1177
}
1178
1179
/**
1180
 * Permits to prepare data to be exchanged.
1181
 *
1182
 * @param array|string $data Text
1183
 * @param string       $type Parameter
1184
 * @param string       $key  Optional key
1185
 *
1186
 * @return string|array
1187
 */
1188
function prepareExchangedData($data, string $type, ?string $key = null)
1189
{
1190
    $session = SessionManager::getSession();
1191
    $key = empty($key) ? $session->get('key') : $key;
1192
    
1193
    // Perform
1194
    if ($type === 'encode' && is_array($data) === true) {
1195
        // json encoding
1196
        $data = json_encode(
1197
            $data,
1198
            JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1199
        );
1200
        
1201
        // Now encrypt
1202
        if ((int) $session->get('encryptClientServer') === 1) {
1203
            $data = Encryption::encrypt(
1204
                $data,
1205
                $key
1206
            );
1207
        }
1208
1209
        return $data;
1210
    }
1211
1212
    if ($type === 'decode' && is_array($data) === false) {
1213
        // Decrypt if needed
1214
        if ((int) $session->get('encryptClientServer') === 1) {
1215
            $data = (string) Encryption::decrypt(
1216
                (string) $data,
1217
                $key
1218
            );
1219
        } else {
1220
            // Double html encoding received
1221
            $data = html_entity_decode(html_entity_decode(/** @scrutinizer ignore-type */$data)); // @codeCoverageIgnore Is always a string (not an array)
1222
        }
1223
1224
        // Check if $data is a valid string before json_decode
1225
        if (is_string($data) && !empty($data)) {
1226
            // Return data array
1227
            return json_decode($data, true);
1228
        }
1229
    }
1230
1231
    return '';
1232
}
1233
1234
1235
/**
1236
 * Create a thumbnail.
1237
 *
1238
 * @param string  $src           Source
1239
 * @param string  $dest          Destination
1240
 * @param int $desired_width Size of width
1241
 * 
1242
 * @return void|string|bool
1243
 */
1244
function makeThumbnail(string $src, string $dest, int $desired_width)
1245
{
1246
    /* read the source image */
1247
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1248
        $source_image = imagecreatefrompng($src);
1249
        if ($source_image === false) {
1250
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1251
        }
1252
    } else {
1253
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1254
    }
1255
1256
    // Get height and width
1257
    $width = imagesx($source_image);
1258
    $height = imagesy($source_image);
1259
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1260
    $desired_height = (int) floor($height * $desired_width / $width);
1261
    /* create a new, "virtual" image */
1262
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1263
    if ($virtual_image === false) {
1264
        return false;
1265
    }
1266
    /* copy source image at a resized size */
1267
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1268
    /* create the physical thumbnail image to its destination */
1269
    imagejpeg($virtual_image, $dest);
1270
}
1271
1272
/**
1273
 * Check table prefix in SQL query.
1274
 *
1275
 * @param string $table Table name
1276
 * 
1277
 * @return string
1278
 */
1279
function prefixTable(string $table): string
1280
{
1281
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1282
    return $safeTable;
1283
}
1284
1285
/**
1286
 * GenerateCryptKey
1287
 *
1288
 * @param int     $size      Length
1289
 * @param bool $secure Secure
1290
 * @param bool $numerals Numerics
1291
 * @param bool $uppercase Uppercase letters
1292
 * @param bool $symbols Symbols
1293
 * @param bool $lowercase Lowercase
1294
 * 
1295
 * @return string
1296
 */
1297
function GenerateCryptKey(
1298
    int $size = 20,
1299
    bool $secure = false,
1300
    bool $numerals = false,
1301
    bool $uppercase = false,
1302
    bool $symbols = false,
1303
    bool $lowercase = false
1304
): string {
1305
    $generator = new ComputerPasswordGenerator();
1306
    $generator->setRandomGenerator(new Php7RandomGenerator());
1307
    
1308
    // Manage size
1309
    $generator->setLength((int) $size);
1310
    if ($secure === true) {
1311
        $generator->setSymbols(true);
1312
        $generator->setLowercase(true);
1313
        $generator->setUppercase(true);
1314
        $generator->setNumbers(true);
1315
    } else {
1316
        $generator->setLowercase($lowercase);
1317
        $generator->setUppercase($uppercase);
1318
        $generator->setNumbers($numerals);
1319
        $generator->setSymbols($symbols);
1320
    }
1321
1322
    return $generator->generatePasswords()[0];
1323
}
1324
1325
/**
1326
 * GenerateGenericPassword
1327
 *
1328
 * @param int     $size      Length
1329
 * @param bool $secure Secure
1330
 * @param bool $numerals Numerics
1331
 * @param bool $uppercase Uppercase letters
1332
 * @param bool $symbols Symbols
1333
 * @param bool $lowercase Lowercase
1334
 * @param array   $SETTINGS  SETTINGS
1335
 * 
1336
 * @return string
1337
 */
1338
function generateGenericPassword(
1339
    int $size,
1340
    bool $secure,
1341
    bool $lowercase,
1342
    bool $capitalize,
1343
    bool $numerals,
1344
    bool $symbols,
1345
    array $SETTINGS
1346
): string
1347
{
1348
    if ((int) $size > (int) $SETTINGS['pwd_maximum_length']) {
1349
        return prepareExchangedData(
1350
            array(
1351
                'error_msg' => 'Password length is too long! ',
1352
                'error' => 'true',
1353
            ),
1354
            'encode'
1355
        );
1356
    }
1357
    // Load libraries
1358
    $generator = new ComputerPasswordGenerator();
1359
    $generator->setRandomGenerator(new Php7RandomGenerator());
1360
1361
    // Manage size
1362
    $generator->setLength(($size <= 0) ? 10 : $size);
1363
1364
    if ($secure === true) {
1365
        $generator->setSymbols(true);
1366
        $generator->setLowercase(true);
1367
        $generator->setUppercase(true);
1368
        $generator->setNumbers(true);
1369
    } else {
1370
        $generator->setLowercase($lowercase);
1371
        $generator->setUppercase($capitalize);
1372
        $generator->setNumbers($numerals);
1373
        $generator->setSymbols($symbols);
1374
    }
1375
1376
    return prepareExchangedData(
1377
        array(
1378
            'key' => $generator->generatePasswords(),
1379
            'error' => '',
1380
        ),
1381
        'encode'
1382
    );
1383
}
1384
1385
/**
1386
 * Send sysLOG message
1387
 *
1388
 * @param string    $message
1389
 * @param string    $host
1390
 * @param int       $port
1391
 * @param string    $component
1392
 * 
1393
 * @return void
1394
*/
1395
function send_syslog($message, $host, $port, $component = 'teampass'): void
1396
{
1397
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1398
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1399
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1400
    socket_close($sock);
1401
}
1402
1403
/**
1404
 * Permits to log events into DB
1405
 *
1406
 * @param array  $SETTINGS Teampass settings
1407
 * @param string $type     Type
1408
 * @param string $label    Label
1409
 * @param string $who      Who
1410
 * @param string $login    Login
1411
 * @param string|int $field_1  Field
1412
 * 
1413
 * @return void
1414
 */
1415
function logEvents(
1416
    array $SETTINGS, 
1417
    string $type, 
1418
    string $label, 
1419
    string $who, 
1420
    ?string $login = null, 
1421
    $field_1 = null
1422
): void
1423
{
1424
    if (empty($who)) {
1425
        $who = getClientIpServer();
1426
    }
1427
1428
    // Load class DB
1429
    loadClasses('DB');
1430
1431
    DB::insert(
1432
        prefixTable('log_system'),
1433
        [
1434
            'type' => $type,
1435
            'date' => time(),
1436
            'label' => $label,
1437
            'qui' => $who,
1438
            'field_1' => $field_1 === null ? '' : $field_1,
1439
        ]
1440
    );
1441
    // If SYSLOG
1442
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1443
        if ($type === 'user_mngt') {
1444
            send_syslog(
1445
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1446
                $SETTINGS['syslog_host'],
1447
                $SETTINGS['syslog_port'],
1448
                'teampass'
1449
            );
1450
        } else {
1451
            send_syslog(
1452
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1453
                $SETTINGS['syslog_host'],
1454
                $SETTINGS['syslog_port'],
1455
                'teampass'
1456
            );
1457
        }
1458
    }
1459
}
1460
1461
/**
1462
 * Log events.
1463
 *
1464
 * @param array  $SETTINGS        Teampass settings
1465
 * @param int    $item_id         Item id
1466
 * @param string $item_label      Item label
1467
 * @param int    $id_user         User id
1468
 * @param string $action          Code for reason
1469
 * @param string $login           User login
1470
 * @param string $raison          Code for reason
1471
 * @param string $encryption_type Encryption on
1472
 * @param string $time Encryption Time
1473
 * @param string $old_value       Old value
1474
 * 
1475
 * @return void
1476
 */
1477
function logItems(
1478
    array $SETTINGS,
1479
    int $item_id,
1480
    string $item_label,
1481
    int $id_user,
1482
    string $action,
1483
    ?string $login = null,
1484
    ?string $raison = null,
1485
    ?string $encryption_type = null,
1486
    ?string $time = null,
1487
    ?string $old_value = null
1488
): void {
1489
    // Load class DB
1490
    loadClasses('DB');
1491
1492
    // Insert log in DB
1493
    DB::insert(
1494
        prefixTable('log_items'),
1495
        [
1496
            'id_item' => $item_id,
1497
            'date' => is_null($time) === true ? time() : $time,
1498
            'id_user' => $id_user,
1499
            'action' => $action,
1500
            'raison' => $raison,
1501
            'old_value' => $old_value,
1502
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1503
        ]
1504
    );
1505
    // Timestamp the last change
1506
    if (in_array($action, ['at_creation', 'at_modifiation', 'at_delete', 'at_import'], true)) {
1507
        DB::update(
1508
            prefixTable('misc'),
1509
            [
1510
                'valeur' => time(),
1511
                'updated_at' => time(),
1512
            ],
1513
            'type = %s AND intitule = %s',
1514
            'timestamp',
1515
            'last_item_change'
1516
        );
1517
    }
1518
1519
    // SYSLOG
1520
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1521
        // Extract reason
1522
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1523
        // Get item info if not known
1524
        if (empty($item_label) === true) {
1525
            $dataItem = DB::queryFirstRow(
1526
                'SELECT id, id_tree, label
1527
                FROM ' . prefixTable('items') . '
1528
                WHERE id = %i',
1529
                $item_id
1530
            );
1531
            $item_label = $dataItem['label'];
1532
        }
1533
1534
        send_syslog(
1535
            'action=' . str_replace('at_', '', $action) .
1536
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1537
                ' itemno=' . $item_id .
1538
                ' user=' . (is_null($login) === true ? '' : addslashes((string) $login)) .
1539
                ' itemname="' . addslashes($item_label) . '"',
1540
            $SETTINGS['syslog_host'],
1541
            $SETTINGS['syslog_port'],
1542
            'teampass'
1543
        );
1544
    }
1545
1546
    // send notification if enabled
1547
    //notifyOnChange($item_id, $action, $SETTINGS);
1548
}
1549
1550
/**
1551
 * Prepare notification email to subscribers.
1552
 *
1553
 * @param int    $item_id  Item id
1554
 * @param string $label    Item label
1555
 * @param array  $changes  List of changes
1556
 * @param array  $SETTINGS Teampass settings
1557
 * 
1558
 * @return void
1559
 */
1560
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1561
{
1562
    $session = SessionManager::getSession();
1563
    $lang = new Language($session->get('user-language') ?? 'english');
1564
    $globalsUserId = $session->get('user-id');
1565
    $globalsLastname = $session->get('user-lastname');
1566
    $globalsName = $session->get('user-name');
1567
    // send email to user that what to be notified
1568
    $notification = DB::queryFirstField(
1569
        'SELECT email
1570
        FROM ' . prefixTable('notification') . ' AS n
1571
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1572
        WHERE n.item_id = %i AND n.user_id != %i',
1573
        $item_id,
1574
        $globalsUserId
1575
    );
1576
    if (DB::count() > 0) {
1577
        // Prepare path
1578
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1579
        // Get list of changes
1580
        $htmlChanges = '<ul>';
1581
        foreach ($changes as $change) {
1582
            $htmlChanges .= '<li>' . $change . '</li>';
1583
        }
1584
        $htmlChanges .= '</ul>';
1585
        // send email
1586
        DB::insert(
1587
            prefixTable('emails'),
1588
            [
1589
                'timestamp' => time(),
1590
                'subject' => $lang->get('email_subject_item_updated'),
1591
                'body' => str_replace(
1592
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1593
                    [$label, $path, (string) $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1594
                    $lang->get('email_body_item_updated')
1595
                ),
1596
                'receivers' => implode(',', $notification),
1597
                'status' => '',
1598
            ]
1599
        );
1600
    }
1601
}
1602
1603
/**
1604
 * Returns the Item + path.
1605
 *
1606
 * @param int    $id_tree  Node id
1607
 * @param string $label    Label
1608
 * @param array  $SETTINGS TP settings
1609
 * 
1610
 * @return string
1611
 */
1612
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1613
{
1614
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1615
    $arbo = $tree->getPath($id_tree, true);
1616
    $path = '';
1617
    foreach ($arbo as $elem) {
1618
        if (empty($path) === true) {
1619
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1620
        } else {
1621
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1622
        }
1623
    }
1624
1625
    // Build text to show user
1626
    if (empty($label) === false) {
1627
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1628
    }
1629
    return empty($path) === true ? '' : $path;
1630
}
1631
1632
/**
1633
 * Get the client ip address.
1634
 *
1635
 * @return string IP address
1636
 */
1637
function getClientIpServer(): string
1638
{
1639
    if (getenv('HTTP_CLIENT_IP')) {
1640
        $ipaddress = getenv('HTTP_CLIENT_IP');
1641
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1642
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1643
    } elseif (getenv('HTTP_X_FORWARDED')) {
1644
        $ipaddress = getenv('HTTP_X_FORWARDED');
1645
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1646
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1647
    } elseif (getenv('HTTP_FORWARDED')) {
1648
        $ipaddress = getenv('HTTP_FORWARDED');
1649
    } elseif (getenv('REMOTE_ADDR')) {
1650
        $ipaddress = getenv('REMOTE_ADDR');
1651
    } else {
1652
        $ipaddress = 'UNKNOWN';
1653
    }
1654
1655
    return $ipaddress;
1656
}
1657
1658
/**
1659
 * Escape all HTML, JavaScript, and CSS.
1660
 *
1661
 * @param string $input    The input string
1662
 * @param string $encoding Which character encoding are we using?
1663
 * 
1664
 * @return string
1665
 */
1666
function noHTML(string $input, string $encoding = 'UTF-8'): string
1667
{
1668
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1669
}
1670
1671
/**
1672
 * Rebuilds the Teampass config file.
1673
 *
1674
 * @param string $configFilePath Path to the config file.
1675
 * @param array  $settings       Teampass settings.
1676
 *
1677
 * @return string|bool
1678
 */
1679
function rebuildConfigFile(string $configFilePath, array $settings)
1680
{
1681
    // Perform a copy if the file exists
1682
    if (file_exists($configFilePath)) {
1683
        $backupFilePath = $configFilePath . '.' . date('Y_m_d_His', time());
1684
        if (!copy($configFilePath, $backupFilePath)) {
1685
            return "ERROR: Could not copy file '$configFilePath'";
1686
        }
1687
    }
1688
1689
    // Regenerate the config file
1690
    $data = ["<?php\n", "global \$SETTINGS;\n", "\$SETTINGS = array (\n"];
1691
    $rows = DB::query('SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s', 'admin');
1692
    foreach ($rows as $record) {
1693
        $value = getEncryptedValue($record['valeur'], $record['is_encrypted']);
1694
        $data[] = "    '{$record['intitule']}' => '". htmlspecialchars_decode($value, ENT_COMPAT) . "',\n";
1695
    }
1696
    $data[] = ");\n";
1697
    $data = array_unique($data);
1698
1699
    // Update the file
1700
    file_put_contents($configFilePath, implode('', $data));
1701
1702
    return true;
1703
}
1704
1705
/**
1706
 * Returns the encrypted value if needed.
1707
 *
1708
 * @param string $value       Value to encrypt.
1709
 * @param int   $isEncrypted Is the value encrypted?
1710
 *
1711
 * @return string
1712
 */
1713
function getEncryptedValue(string $value, int $isEncrypted): string
1714
{
1715
    return $isEncrypted ? cryption($value, '', 'encrypt')['string'] : $value;
1716
}
1717
1718
/**
1719
 * Permits to replace &#92; to permit correct display
1720
 *
1721
 * @param string $input Some text
1722
 * 
1723
 * @return string
1724
 */
1725
function handleBackslash(string $input): string
1726
{
1727
    return str_replace('&amp;#92;', '&#92;', $input);
1728
}
1729
1730
/**
1731
 * Permits to load settings
1732
 * 
1733
 * @return void
1734
*/
1735
function loadSettings(): void
1736
{
1737
    global $SETTINGS;
1738
    /* LOAD CPASSMAN SETTINGS */
1739
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1740
        $SETTINGS = [];
1741
        $SETTINGS['duplicate_folder'] = 0;
1742
        //by default, this is set to 0;
1743
        $SETTINGS['duplicate_item'] = 0;
1744
        //by default, this is set to 0;
1745
        $SETTINGS['number_of_used_pw'] = 5;
1746
        //by default, this value is set to 5;
1747
        $settings = [];
1748
        $rows = DB::query(
1749
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1750
            [
1751
                'type' => 'admin',
1752
                'type2' => 'settings',
1753
            ]
1754
        );
1755
        foreach ($rows as $record) {
1756
            if ($record['type'] === 'admin') {
1757
                $SETTINGS[$record['intitule']] = $record['valeur'];
1758
            } else {
1759
                $settings[$record['intitule']] = $record['valeur'];
1760
            }
1761
        }
1762
        $SETTINGS['loaded'] = 1;
1763
        $SETTINGS['default_session_expiration_time'] = 5;
1764
    }
1765
}
1766
1767
/**
1768
 * check if folder has custom fields.
1769
 * Ensure that target one also has same custom fields
1770
 * 
1771
 * @param int $source_id
1772
 * @param int $target_id 
1773
 * 
1774
 * @return bool
1775
*/
1776
function checkCFconsistency(int $source_id, int $target_id): bool
1777
{
1778
    $source_cf = [];
1779
    $rows = DB::query(
1780
        'SELECT id_category
1781
            FROM ' . prefixTable('categories_folders') . '
1782
            WHERE id_folder = %i',
1783
        $source_id
1784
    );
1785
    foreach ($rows as $record) {
1786
        array_push($source_cf, $record['id_category']);
1787
    }
1788
1789
    $target_cf = [];
1790
    $rows = DB::query(
1791
        'SELECT id_category
1792
            FROM ' . prefixTable('categories_folders') . '
1793
            WHERE id_folder = %i',
1794
        $target_id
1795
    );
1796
    foreach ($rows as $record) {
1797
        array_push($target_cf, $record['id_category']);
1798
    }
1799
1800
    $cf_diff = array_diff($source_cf, $target_cf);
1801
    if (count($cf_diff) > 0) {
1802
        return false;
1803
    }
1804
1805
    return true;
1806
}
1807
1808
/**
1809
 * Will encrypte/decrypt a fil eusing Defuse.
1810
 *
1811
 * @param string $type        can be either encrypt or decrypt
1812
 * @param string $source_file path to source file
1813
 * @param string $target_file path to target file
1814
 * @param array  $SETTINGS    Settings
1815
 * @param string $password    A password
1816
 *
1817
 * @return string|bool
1818
 */
1819
function prepareFileWithDefuse(
1820
    string $type,
1821
    string $source_file,
1822
    string $target_file,
1823
    ?string $password = null
1824
) {
1825
    // Load AntiXSS
1826
    $antiXss = new AntiXSS();
1827
    // Protect against bad inputs
1828
    if (is_array($source_file) === true || is_array($target_file) === true) {
1829
        return 'error_cannot_be_array';
1830
    }
1831
1832
    // Sanitize
1833
    $source_file = $antiXss->xss_clean($source_file);
1834
    $target_file = $antiXss->xss_clean($target_file);
1835
    if (empty($password) === true || is_null($password) === true) {
1836
        // get KEY to define password
1837
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
1838
        $password = Key::loadFromAsciiSafeString($ascii_key);
1839
    }
1840
1841
    $err = '';
1842
    if ($type === 'decrypt') {
1843
        // Decrypt file
1844
        $err = defuseFileDecrypt(
1845
            $source_file,
1846
            $target_file,
1847
            $password
1848
        );
1849
    } elseif ($type === 'encrypt') {
1850
        // Encrypt file
1851
        $err = defuseFileEncrypt(
1852
            $source_file,
1853
            $target_file,
1854
            $password
1855
        );
1856
    }
1857
1858
    // return error
1859
    return $err === true ? $err : '';
1860
}
1861
1862
/**
1863
 * Encrypt a file with Defuse.
1864
 *
1865
 * @param string $source_file path to source file
1866
 * @param string $target_file path to target file
1867
 * @param array  $SETTINGS    Settings
1868
 * @param string $password    A password
1869
 *
1870
 * @return string|bool
1871
 */
1872
function defuseFileEncrypt(
1873
    string $source_file,
1874
    string $target_file,
1875
    ?string $password = null
1876
) {
1877
    $err = '';
1878
    try {
1879
        CryptoFile::encryptFileWithPassword(
1880
            $source_file,
1881
            $target_file,
1882
            $password
1883
        );
1884
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
1885
        $err = 'wrong_key';
1886
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
1887
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
1888
        $err = 'environment_error';
1889
    } catch (CryptoException\IOException $ex) {
1890
        error_log('TEAMPASS-Error-General: ' . $ex->getMessage());
1891
        $err = 'general_error';
1892
    }
1893
1894
    // return error
1895
    return empty($err) === false ? $err : true;
1896
}
1897
1898
/**
1899
 * Decrypt a file with Defuse.
1900
 *
1901
 * @param string $source_file path to source file
1902
 * @param string $target_file path to target file
1903
 * @param string $password    A password
1904
 *
1905
 * @return string|bool
1906
 */
1907
function defuseFileDecrypt(
1908
    string $source_file,
1909
    string $target_file,
1910
    ?string $password = null
1911
) {
1912
    $err = '';
1913
    try {
1914
        CryptoFile::decryptFileWithPassword(
1915
            $source_file,
1916
            $target_file,
1917
            $password
1918
        );
1919
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
1920
        $err = 'wrong_key';
1921
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
1922
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
1923
        $err = 'environment_error';
1924
    } catch (CryptoException\IOException $ex) {
1925
        error_log('TEAMPASS-Error-General: ' . $ex->getMessage());
1926
        $err = 'general_error';
1927
    }
1928
1929
    // return error
1930
    return empty($err) === false ? $err : true;
1931
}
1932
1933
/*
1934
* NOT TO BE USED
1935
*/
1936
/**
1937
 * Undocumented function.
1938
 *
1939
 * @param string $text Text to debug
1940
 */
1941
function debugTeampass(string $text): void
1942
{
1943
    $debugFile = fopen('D:/wamp64/www/TeamPass/debug.txt', 'r+');
1944
    if ($debugFile !== false) {
1945
        fputs($debugFile, $text);
1946
        fclose($debugFile);
1947
    }
1948
}
1949
1950
/**
1951
 * DELETE the file with expected command depending on server type.
1952
 *
1953
 * @param string $file     Path to file
1954
 * @param array  $SETTINGS Teampass settings
1955
 *
1956
 * @return void
1957
 */
1958
function fileDelete(string $file, array $SETTINGS): void
1959
{
1960
    // Load AntiXSS
1961
    $antiXss = new AntiXSS();
1962
    $file = $antiXss->xss_clean($file);
1963
    if (is_file($file)) {
1964
        unlink($file);
1965
    }
1966
}
1967
1968
/**
1969
 * Permits to extract the file extension.
1970
 *
1971
 * @param string $file File name
1972
 *
1973
 * @return string
1974
 */
1975
function getFileExtension(string $file): string
1976
{
1977
    if (strpos($file, '.') === false) {
1978
        return $file;
1979
    }
1980
1981
    return substr($file, strrpos($file, '.') + 1);
1982
}
1983
1984
/**
1985
 * Chmods files and folders with different permissions.
1986
 *
1987
 * This is an all-PHP alternative to using: \n
1988
 * <tt>exec("find ".$path." -type f -exec chmod 644 {} \;");</tt> \n
1989
 * <tt>exec("find ".$path." -type d -exec chmod 755 {} \;");</tt>
1990
 *
1991
 * @author Jeppe Toustrup (tenzer at tenzer dot dk)
1992
  *
1993
 * @param string $path      An either relative or absolute path to a file or directory which should be processed.
1994
 * @param int    $filePerm The permissions any found files should get.
1995
 * @param int    $dirPerm  The permissions any found folder should get.
1996
 *
1997
 * @return bool Returns TRUE if the path if found and FALSE if not.
1998
 *
1999
 * @warning The permission levels has to be entered in octal format, which
2000
 * normally means adding a zero ("0") in front of the permission level. \n
2001
 * More info at: http://php.net/chmod.
2002
*/
2003
2004
function recursiveChmod(
2005
    string $path,
2006
    int $filePerm = 0644,
2007
    int  $dirPerm = 0755
2008
) {
2009
    // Check if the path exists
2010
    $path = basename($path);
2011
    if (! file_exists($path)) {
2012
        return false;
2013
    }
2014
2015
    // See whether this is a file
2016
    if (is_file($path)) {
2017
        // Chmod the file with our given filepermissions
2018
        try {
2019
            chmod($path, $filePerm);
2020
        } catch (Exception $e) {
2021
            return false;
2022
        }
2023
    // If this is a directory...
2024
    } elseif (is_dir($path)) {
2025
        // Then get an array of the contents
2026
        $foldersAndFiles = scandir($path);
2027
        // Remove "." and ".." from the list
2028
        $entries = array_slice($foldersAndFiles, 2);
2029
        // Parse every result...
2030
        foreach ($entries as $entry) {
2031
            // And call this function again recursively, with the same permissions
2032
            recursiveChmod($path.'/'.$entry, $filePerm, $dirPerm);
2033
        }
2034
2035
        // When we are done with the contents of the directory, we chmod the directory itself
2036
        try {
2037
            chmod($path, $filePerm);
2038
        } catch (Exception $e) {
2039
            return false;
2040
        }
2041
    }
2042
2043
    // Everything seemed to work out well, return true
2044
    return true;
2045
}
2046
2047
/**
2048
 * Check if user can access to this item.
2049
 *
2050
 * @param int   $item_id ID of item
2051
 * @param array $SETTINGS
2052
 *
2053
 * @return bool|string
2054
 */
2055
function accessToItemIsGranted(int $item_id, array $SETTINGS)
2056
{
2057
    
2058
    $session = SessionManager::getSession();
2059
    $session_groupes_visibles = $session->get('user-accessible_folders');
2060
    $session_list_restricted_folders_for_items = $session->get('system-list_restricted_folders_for_items');
2061
    // Load item data
2062
    $data = DB::queryFirstRow(
2063
        'SELECT id_tree
2064
        FROM ' . prefixTable('items') . '
2065
        WHERE id = %i',
2066
        $item_id
2067
    );
2068
    // Check if user can access this folder
2069
    if (in_array($data['id_tree'], $session_groupes_visibles) === false) {
2070
        // Now check if this folder is restricted to user
2071
        if (isset($session_list_restricted_folders_for_items[$data['id_tree']]) === true
2072
            && in_array($item_id, $session_list_restricted_folders_for_items[$data['id_tree']]) === false
2073
        ) {
2074
            return 'ERR_FOLDER_NOT_ALLOWED';
2075
        }
2076
    }
2077
2078
    return true;
2079
}
2080
2081
/**
2082
 * Creates a unique key.
2083
 *
2084
 * @param int $lenght Key lenght
2085
 *
2086
 * @return string
2087
 */
2088
function uniqidReal(int $lenght = 13): string
2089
{
2090
    if (function_exists('random_bytes')) {
2091
        $bytes = random_bytes(intval(ceil($lenght / 2)));
2092
    } elseif (function_exists('openssl_random_pseudo_bytes')) {
2093
        $bytes = openssl_random_pseudo_bytes(intval(ceil($lenght / 2)));
2094
    } else {
2095
        throw new Exception('no cryptographically secure random function available');
2096
    }
2097
2098
    return substr(bin2hex($bytes), 0, $lenght);
2099
}
2100
2101
/**
2102
 * Obfuscate an email.
2103
 *
2104
 * @param string $email Email address
2105
 *
2106
 * @return string
2107
 */
2108
function obfuscateEmail(string $email): string
2109
{
2110
    $email = explode("@", $email);
2111
    $name = $email[0];
2112
    if (strlen($name) > 3) {
2113
        $name = substr($name, 0, 2);
2114
        for ($i = 0; $i < strlen($email[0]) - 3; $i++) {
2115
            $name .= "*";
2116
        }
2117
        $name .= substr($email[0], -1, 1);
2118
    }
2119
    $host = explode(".", $email[1])[0];
2120
    if (strlen($host) > 3) {
2121
        $host = substr($host, 0, 1);
2122
        for ($i = 0; $i < strlen(explode(".", $email[1])[0]) - 2; $i++) {
2123
            $host .= "*";
2124
        }
2125
        $host .= substr(explode(".", $email[1])[0], -1, 1);
2126
    }
2127
    $email = $name . "@" . $host . "." . explode(".", $email[1])[1];
2128
    return $email;
2129
}
2130
2131
/**
2132
 * Get id and title from role_titles table.
2133
 *
2134
 * @return array
2135
 */
2136
function getRolesTitles(): array
2137
{
2138
    // Load class DB
2139
    loadClasses('DB');
2140
    
2141
    // Insert log in DB
2142
    return DB::query(
2143
        'SELECT id, title
2144
        FROM ' . prefixTable('roles_title')
2145
    );
2146
}
2147
2148
/**
2149
 * Undocumented function.
2150
 *
2151
 * @param int $bytes Size of file
2152
 *
2153
 * @return string
2154
 */
2155
function formatSizeUnits(int $bytes): string
2156
{
2157
    if ($bytes >= 1073741824) {
2158
        $bytes = number_format($bytes / 1073741824, 2) . ' GB';
2159
    } elseif ($bytes >= 1048576) {
2160
        $bytes = number_format($bytes / 1048576, 2) . ' MB';
2161
    } elseif ($bytes >= 1024) {
2162
        $bytes = number_format($bytes / 1024, 2) . ' KB';
2163
    } elseif ($bytes > 1) {
2164
        $bytes .= ' bytes';
2165
    } elseif ($bytes === 1) {
2166
        $bytes .= ' byte';
2167
    } else {
2168
        $bytes = '0 bytes';
2169
    }
2170
2171
    return $bytes;
2172
}
2173
2174
/**
2175
 * Generate user pair of keys.
2176
 *
2177
 * @param string $userPwd User password
2178
 *
2179
 * @return array
2180
 */
2181
function generateUserKeys(string $userPwd, ?array $SETTINGS = null): array
2182
{
2183
    // Sanitize
2184
    $antiXss = new AntiXSS();
2185
    $userPwd = $antiXss->xss_clean($userPwd);
2186
    // Load classes
2187
    $rsa = new Crypt_RSA();
2188
    $cipher = new Crypt_AES();
2189
    // Create the private and public key
2190
    $res = $rsa->createKey(4096);
2191
    // Encrypt the privatekey
2192
    $cipher->setPassword($userPwd);
2193
    $privatekey = $cipher->encrypt($res['privatekey']);
2194
2195
    $result = [
2196
        'private_key' => base64_encode($privatekey),
2197
        'public_key' => base64_encode($res['publickey']),
2198
        'private_key_clear' => base64_encode($res['privatekey']),
2199
    ];
2200
2201
    // Generate transparent recovery data if enabled
2202
    if ($SETTINGS !== null) {
2203
        // Generate unique seed for this user
2204
        $userSeed = bin2hex(openssl_random_pseudo_bytes(32));
2205
2206
        // Derive backup encryption key
2207
        $derivedKey = deriveBackupKey($userSeed, $result['public_key'], $SETTINGS);
2208
2209
        // Encrypt private key with derived key (backup)
2210
        $cipherBackup = new Crypt_AES();
2211
        $cipherBackup->setPassword($derivedKey);
2212
        $privatekeyBackup = $cipherBackup->encrypt($res['privatekey']);
2213
2214
        // Generate integrity hash
2215
        $integrityHash = null;
0 ignored issues
show
The assignment to $integrityHash is dead and can be removed.
Loading history...
2216
        $serverSecret = getServerSecret();
2217
        $integrityHash = generateKeyIntegrityHash($userSeed, $result['public_key'], $serverSecret);
2218
2219
        $result['user_seed'] = $userSeed;
2220
        $result['private_key_backup'] = base64_encode($privatekeyBackup);
2221
        $result['key_integrity_hash'] = $integrityHash;
2222
    }
2223
2224
    return $result;
2225
}
2226
2227
/**
2228
 * Permits to decrypt the user's privatekey.
2229
 *
2230
 * @param string $userPwd        User password
2231
 * @param string $userPrivateKey User private key
2232
 *
2233
 * @return string|object
2234
 */
2235
function decryptPrivateKey(string $userPwd, string $userPrivateKey)
2236
{
2237
    // Sanitize
2238
    $antiXss = new AntiXSS();
2239
    $userPwd = $antiXss->xss_clean($userPwd);
2240
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2241
2242
    if (empty($userPwd) === false) {
2243
        // Load classes
2244
        $cipher = new Crypt_AES();
2245
        // Encrypt the privatekey
2246
        $cipher->setPassword($userPwd);
2247
        try {
2248
            return base64_encode((string) $cipher->decrypt(base64_decode($userPrivateKey)));
2249
        } catch (Exception $e) {
2250
            return $e;
2251
        }
2252
    }
2253
    return '';
2254
}
2255
2256
/**
2257
 * Permits to encrypt the user's privatekey.
2258
 *
2259
 * @param string $userPwd        User password
2260
 * @param string $userPrivateKey User private key
2261
 *
2262
 * @return string
2263
 */
2264
function encryptPrivateKey(string $userPwd, string $userPrivateKey): string
2265
{
2266
    // Sanitize
2267
    $antiXss = new AntiXSS();
2268
    $userPwd = $antiXss->xss_clean($userPwd);
2269
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2270
2271
    if (empty($userPwd) === false) {
2272
        // Load classes
2273
        $cipher = new Crypt_AES();
2274
        // Encrypt the privatekey
2275
        $cipher->setPassword($userPwd);        
2276
        try {
2277
            return base64_encode($cipher->encrypt(base64_decode($userPrivateKey)));
2278
        } catch (Exception $e) {
2279
            return $e->getMessage();
2280
        }
2281
    }
2282
    return '';
2283
}
2284
2285
/**
2286
 * Derives a backup encryption key from user seed and public key.
2287
 * Uses PBKDF2 with 100k iterations for strong key derivation.
2288
 *
2289
 * @param string $userSeed User's unique derivation seed (64 hex chars)
2290
 * @param string $publicKey User's public RSA key (base64 encoded)
2291
 * @param array $SETTINGS Teampass settings
2292
 *
2293
 * @return string Derived key (32 bytes, raw binary)
2294
 */
2295
function deriveBackupKey(string $userSeed, string $publicKey, array $SETTINGS): string
2296
{
2297
    // Sanitize inputs
2298
    $antiXss = new AntiXSS();
2299
    $userSeed = $antiXss->xss_clean($userSeed);
2300
    $publicKey = $antiXss->xss_clean($publicKey);
2301
2302
    // Use public key hash as salt for key derivation
2303
    $salt = hash('sha256', $publicKey, true);
2304
2305
    // Get PBKDF2 iterations from settings (default 100000)
2306
    $iterations = isset($SETTINGS['transparent_key_recovery_pbkdf2_iterations'])
2307
        ? (int) $SETTINGS['transparent_key_recovery_pbkdf2_iterations']
2308
        : 100000;
2309
2310
    // PBKDF2 key derivation with SHA256
2311
    return hash_pbkdf2(
2312
        'sha256',
2313
        hex2bin($userSeed),
2314
        $salt,
2315
        $iterations,
2316
        32, // 256 bits key length
2317
        true // raw binary output
2318
    );
2319
}
2320
2321
/**
2322
 * Generates key integrity hash to detect tampering.
2323
 *
2324
 * @param string $userSeed User derivation seed
2325
 * @param string $publicKey User public key
2326
 * @param string $serverSecret Server-wide secret key
2327
 *
2328
 * @return string HMAC hash (64 hex chars)
2329
 */
2330
function generateKeyIntegrityHash(string $userSeed, string $publicKey, string $serverSecret): string
2331
{
2332
    return hash_hmac('sha256', $userSeed . $publicKey, $serverSecret);
2333
}
2334
2335
/**
2336
 * Verifies key integrity to detect SQL injection or tampering.
2337
 *
2338
 * @param array $userInfo User information from database
2339
 * @param string $serverSecret Server-wide secret key
2340
 *
2341
 * @return bool True if integrity is valid
2342
 */
2343
function verifyKeyIntegrity(array $userInfo, string $serverSecret): bool
2344
{
2345
    // Skip check if no integrity hash stored (legacy users)
2346
    if (empty($userInfo['key_integrity_hash'])) {
2347
        return true;
2348
    }
2349
2350
    if (empty($userInfo['user_derivation_seed']) || empty($userInfo['public_key'])) {
2351
        return false;
2352
    }
2353
2354
    $expectedHash = generateKeyIntegrityHash(
2355
        $userInfo['user_derivation_seed'],
2356
        $userInfo['public_key'],
2357
        $serverSecret
2358
    );
2359
2360
    return hash_equals($expectedHash, $userInfo['key_integrity_hash']);
2361
}
2362
2363
/**
2364
 * Gets server secret for integrity checks.
2365
 * Reads from file or generates if not exists.
2366
 * *
2367
 * @return string Server secret key
2368
 */
2369
function getServerSecret(): string
2370
{
2371
    $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
2372
    $key = Key::loadFromAsciiSafeString($ascii_key);
2373
    return $key->saveToAsciiSafeString();
2374
}
2375
2376
/**
2377
 * Attempts transparent recovery when password change is detected.
2378
 *
2379
 * @param array $userInfo User information from database
2380
 * @param string $newPassword New password (clear)
2381
 * @param array $SETTINGS Teampass settings
2382
 *
2383
 * @return array Result with private key or error
2384
 */
2385
function attemptTransparentRecovery(array $userInfo, string $newPassword, array $SETTINGS): array
2386
{
2387
    $session = SessionManager::getSession();
2388
    try {
2389
        // Check if user has recovery data
2390
        if (empty($userInfo['user_derivation_seed']) || empty($userInfo['private_key_backup'])) {
2391
            return [
2392
                'success' => false,
2393
                'error' => 'no_recovery_data',
2394
                'private_key_clear' => '',
2395
            ];
2396
        }
2397
2398
        // Verify key integrity
2399
        $serverSecret = getServerSecret();
2400
        if (!verifyKeyIntegrity($userInfo, $serverSecret)) {
2401
            // Critical security event - integrity check failed
2402
            logEvents(
2403
                $SETTINGS,
2404
                'security_alert',
2405
                'key_integrity_check_failed',
2406
                (string) $userInfo['id'],
2407
                'User: ' . $userInfo['login']
2408
            );
2409
            return [
2410
                'success' => false,
2411
                'error' => 'integrity_check_failed',
2412
                'private_key_clear' => '',
2413
            ];
2414
        }
2415
2416
        // Derive backup key
2417
        $derivedKey = deriveBackupKey(
2418
            $userInfo['user_derivation_seed'],
2419
            $userInfo['public_key'],
2420
            $SETTINGS
2421
        );
2422
2423
        // Decrypt private key using derived key
2424
        $cipher = new Crypt_AES();
2425
        $cipher->setPassword($derivedKey);
2426
        $privateKeyClear = base64_encode($cipher->decrypt(base64_decode($userInfo['private_key_backup'])));
2427
2428
        // Re-encrypt with new password
2429
        $newPrivateKeyEncrypted = encryptPrivateKey($newPassword, $privateKeyClear);
2430
2431
        // Re-encrypt backup with derived key (refresh)
2432
        $cipher->setPassword($derivedKey);
2433
        $newPrivateKeyBackup = base64_encode($cipher->encrypt(base64_decode($privateKeyClear)));
2434
        
2435
        // Update database
2436
        DB::update(
2437
            prefixTable('users'),
2438
            [
2439
                'private_key' => $newPrivateKeyEncrypted,
2440
                'private_key_backup' => $newPrivateKeyBackup,
2441
                'last_pw_change' => time(),
2442
                'special' => 'none',
2443
            ],
2444
            'id = %i',
2445
            $userInfo['id']
2446
        );
2447
2448
        // Log success
2449
        logEvents(
2450
            $SETTINGS,
2451
            'user_connection',
2452
            'auto_reencryption_success',
2453
            (string) $userInfo['id'],
2454
            'User: ' . $userInfo['login']
2455
        );
2456
2457
        // Store in session for immediate use
2458
        $session->set('user-private_key', $privateKeyClear);
2459
        $session->set('user-private_key_recovered', true);
2460
2461
        return [
2462
            'success' => true,
2463
            'error' => '',
2464
        ];
2465
2466
    } catch (Exception $e) {
2467
        // Log failure
2468
        logEvents(
2469
            $SETTINGS,
2470
            'security_alert',
2471
            'auto_reencryption_failed',
2472
            (string) $userInfo['id'],
2473
            'User: ' . $userInfo['login'] . ' - Error: ' . $e->getMessage()
2474
        );
2475
2476
        return [
2477
            'success' => false,
2478
            'error' => 'decryption_failed: ' . $e->getMessage(),
2479
            'private_key_clear' => '',
2480
        ];
2481
    }
2482
}
2483
2484
/**
2485
 * Handles external password change (LDAP/OAuth2) with automatic re-encryption.
2486
 *
2487
 * @param int $userId User ID
2488
 * @param string $newPassword New password (clear)
2489
 * @param array $userInfo User information from database
2490
 * @param array $SETTINGS Teampass settings
2491
 *
2492
 * @return bool True if  handled successfully
2493
 */
2494
function handleExternalPasswordChange(int $userId, string $newPassword, array $userInfo, array $SETTINGS): bool
2495
{
2496
    // Check if password was changed recently (< 30s) to avoid duplicate processing
2497
    if (!empty($userInfo['last_pw_change'])) {
2498
        $timeSinceChange = time() - (int) $userInfo['last_pw_change'];
2499
        if ($timeSinceChange < 30) { // 30 seconds
2500
            return true; // Already processed
2501
        }
2502
    }
2503
2504
    // Try to decrypt with new password first (maybe already updated)
2505
    try {
2506
        $testDecrypt = decryptPrivateKey($newPassword, $userInfo['private_key']);
2507
        if (!empty($testDecrypt)) {
2508
            // Password already works, just update timestamp
2509
            DB::update(
2510
                prefixTable('users'),
2511
                ['last_pw_change' => time()],
2512
                'id = %i',
2513
                $userId
2514
            );
2515
            return true;
2516
        }
2517
    } catch (Exception $e) {
2518
        // Expected - old password doesn't work, continue with recovery
2519
    }
2520
2521
    // Attempt transparent recovery
2522
    $result = attemptTransparentRecovery($userInfo, $newPassword, $SETTINGS);
2523
2524
    if ($result['success']) {
2525
        return true;
2526
    }
2527
2528
    // Recovery failed - disable user and alert admins
2529
    DB::update(
2530
        prefixTable('users'),
2531
        [
2532
            'disabled' => 1,
2533
            'special' => 'recrypt-private-key',
2534
        ],
2535
        'id = %i',
2536
        $userId
2537
    );
2538
2539
    // Log critical event
2540
    logEvents(
2541
        $SETTINGS,
2542
        'security_alert',
2543
        'auto_reencryption_critical_failure',
2544
        (string) $userId,
2545
        'User: ' . $userInfo['login'] . ' - disabled due to key recovery failure'
2546
    );
2547
2548
    return false;
2549
}
2550
2551
/**
2552
 * Encrypts a string using AES.
2553
 *
2554
 * @param string $data String to encrypt
2555
 * @param string $key
2556
 *
2557
 * @return array
2558
 */
2559
function doDataEncryption(string $data, ?string $key = null): array
2560
{
2561
    // Sanitize
2562
    $antiXss = new AntiXSS();
2563
    $data = $antiXss->xss_clean($data);
2564
    
2565
    // Load classes
2566
    $cipher = new Crypt_AES(CRYPT_AES_MODE_CBC);
2567
    // Generate an object key
2568
    $objectKey = is_null($key) === true ? uniqidReal(KEY_LENGTH) : $antiXss->xss_clean($key);
2569
    // Set it as password
2570
    $cipher->setPassword($objectKey);
2571
    return [
2572
        'encrypted' => base64_encode($cipher->encrypt($data)),
2573
        'objectKey' => base64_encode($objectKey),
2574
    ];
2575
}
2576
2577
/**
2578
 * Decrypts a string using AES.
2579
 *
2580
 * @param string $data Encrypted data
2581
 * @param string $key  Key to uncrypt
2582
 *
2583
 * @return string
2584
 */
2585
function doDataDecryption(string $data, string $key): string
2586
{
2587
    // Sanitize
2588
    $antiXss = new AntiXSS();
2589
    $data = $antiXss->xss_clean($data);
2590
    $key = $antiXss->xss_clean($key);
2591
2592
    // Load classes
2593
    $cipher = new Crypt_AES();
2594
    // Set the object key
2595
    $cipher->setPassword(base64_decode($key));
2596
    return base64_encode((string) $cipher->decrypt(base64_decode($data)));
2597
}
2598
2599
/**
2600
 * Encrypts using RSA a string using a public key.
2601
 *
2602
 * @param string $key       Key to be encrypted
2603
 * @param string $publicKey User public key
2604
 *
2605
 * @return string
2606
 */
2607
function encryptUserObjectKey(string $key, string $publicKey): string
2608
{
2609
    // Empty password
2610
    if (empty($key)) return '';
2611
2612
    // Sanitize
2613
    $antiXss = new AntiXSS();
2614
    $publicKey = $antiXss->xss_clean($publicKey);
2615
    // Load classes
2616
    $rsa = new Crypt_RSA();
2617
    // Load the public key
2618
    $decodedPublicKey = base64_decode($publicKey, true);
2619
    if ($decodedPublicKey === false) {
2620
        throw new InvalidArgumentException("Error while decoding key.");
2621
    }
2622
    $rsa->loadKey($decodedPublicKey);
2623
    // Encrypt
2624
    $encrypted = $rsa->encrypt(base64_decode($key));
2625
    if (empty($encrypted)) {  // Check if key is empty or null
2626
        throw new RuntimeException("Error while encrypting key.");
2627
    }
2628
    // Return
2629
    return base64_encode($encrypted);
2630
}
2631
2632
/**
2633
 * Decrypts using RSA an encrypted string using a private key.
2634
 *
2635
 * @param string $key        Encrypted key
2636
 * @param string $privateKey User private key
2637
 *
2638
 * @return string
2639
 */
2640
function decryptUserObjectKey(string $key, string $privateKey): string
2641
{
2642
    // Sanitize
2643
    $antiXss = new AntiXSS();
2644
    $privateKey = $antiXss->xss_clean($privateKey);
2645
2646
    // Load classes
2647
    $rsa = new Crypt_RSA();
2648
    // Load the private key
2649
    $decodedPrivateKey = base64_decode($privateKey, true);
2650
    if ($decodedPrivateKey === false) {
2651
        throw new InvalidArgumentException("Error while decoding private key.");
2652
    }
2653
2654
    $rsa->loadKey($decodedPrivateKey);
2655
2656
    // Decrypt
2657
    try {
2658
        $decodedKey = base64_decode($key, true);
2659
        if ($decodedKey === false) {
2660
            throw new InvalidArgumentException("Error while decoding key.");
2661
        }
2662
2663
        // This check is needed as decrypt() in version 2 can return false in case of error
2664
        $tmpValue = $rsa->decrypt($decodedKey);
2665
        if ($tmpValue !== false) {
2666
            return base64_encode($tmpValue);
2667
        } else {
2668
            return '';
2669
        }
2670
    } catch (Exception $e) {
2671
        if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
2672
            error_log('TEAMPASS Error - ldap - '.$e->getMessage());
2673
        }
2674
        return 'Exception: could not decrypt object';
2675
    }
2676
}
2677
2678
/**
2679
 * Encrypts a file.
2680
 *
2681
 * @param string $fileInName File name
2682
 * @param string $fileInPath Path to file
2683
 *
2684
 * @return array
2685
 */
2686
function encryptFile(string $fileInName, string $fileInPath): array
2687
{
2688
    if (defined('FILE_BUFFER_SIZE') === false) {
2689
        define('FILE_BUFFER_SIZE', 128 * 1024);
2690
    }
2691
2692
    // Load classes
2693
    $cipher = new Crypt_AES();
2694
2695
    // Generate an object key
2696
    $objectKey = uniqidReal(32);
2697
    // Set it as password
2698
    $cipher->setPassword($objectKey);
2699
    // Prevent against out of memory
2700
    $cipher->enableContinuousBuffer();
2701
2702
    // Encrypt the file content
2703
    $filePath = filter_var($fileInPath . '/' . $fileInName, FILTER_SANITIZE_URL);
2704
    $fileContent = file_get_contents($filePath);
2705
    $plaintext = $fileContent;
2706
    $ciphertext = $cipher->encrypt($plaintext);
2707
2708
    // Save new file
2709
    // deepcode ignore InsecureHash: is simply used to get a unique name
2710
    $hash = uniqid('', true);
2711
    $fileOut = $fileInPath . '/' . TP_FILE_PREFIX . $hash;
2712
    file_put_contents($fileOut, $ciphertext);
2713
    unlink($fileInPath . '/' . $fileInName);
2714
    return [
2715
        'fileHash' => base64_encode($hash),
2716
        'objectKey' => base64_encode($objectKey),
2717
    ];
2718
}
2719
2720
/**
2721
 * Decrypt a file.
2722
 *
2723
 * @param string $fileName File name
2724
 * @param string $filePath Path to file
2725
 * @param string $key      Key to use
2726
 *
2727
 * @return string|array
2728
 */
2729
function decryptFile(string $fileName, string $filePath, string $key): string|array
2730
{
2731
    if (! defined('FILE_BUFFER_SIZE')) {
2732
        define('FILE_BUFFER_SIZE', 128 * 1024);
2733
    }
2734
    
2735
    // Load classes
2736
    $cipher = new Crypt_AES();
2737
    $antiXSS = new AntiXSS();
2738
    
2739
    // Get file name
2740
    $safeFileName = $antiXSS->xss_clean(base64_decode($fileName));
2741
2742
    // Set the object key
2743
    $cipher->setPassword(base64_decode($key));
2744
    // Prevent against out of memory
2745
    $cipher->enableContinuousBuffer();
2746
    $cipher->disablePadding();
2747
    // Get file content
2748
    $safeFilePath = realpath($filePath . '/' . TP_FILE_PREFIX . $safeFileName);
2749
    if ($safeFilePath !== false && file_exists($safeFilePath)) {
2750
        $ciphertext = file_get_contents(filter_var($safeFilePath, FILTER_SANITIZE_URL));
2751
    } else {
2752
        // Handle the error: file doesn't exist or path is invalid
2753
        return [
2754
            'error' => true,
2755
            'message' => 'This file has not been found.',
2756
        ];
2757
    }
2758
2759
    if (WIP) error_log('DEBUG: File image url -> '.filter_var($safeFilePath, FILTER_SANITIZE_URL));
2760
2761
    // Decrypt file content and return
2762
    return base64_encode($cipher->decrypt($ciphertext));
2763
}
2764
2765
/**
2766
 * Generate a simple password
2767
 *
2768
 * @param int $length Length of string
2769
 * @param bool $symbolsincluded Allow symbols
2770
 *
2771
 * @return string
2772
 */
2773
function generateQuickPassword(int $length = 16, bool $symbolsincluded = true): string
2774
{
2775
    // Generate new user password
2776
    $small_letters = range('a', 'z');
2777
    $big_letters = range('A', 'Z');
2778
    $digits = range(0, 9);
2779
    $symbols = $symbolsincluded === true ?
2780
        ['#', '_', '-', '@', '$', '+', '!'] : [];
2781
    $res = array_merge($small_letters, $big_letters, $digits, $symbols);
2782
    $count = count($res);
2783
    // first variant
2784
2785
    $random_string = '';
2786
    for ($i = 0; $i < $length; ++$i) {
2787
        $random_string .= $res[random_int(0, $count - 1)];
2788
    }
2789
2790
    return $random_string;
2791
}
2792
2793
/**
2794
 * Permit to store the sharekey of an object for users.
2795
 *
2796
 * @param string $object_name             Type for table selection
2797
 * @param int    $post_folder_is_personal Personal
2798
 * @param int    $post_object_id          Object
2799
 * @param string $objectKey               Object key
2800
 * @param array  $SETTINGS                Teampass settings
2801
 * @param int    $user_id                 User ID if needed
2802
 * @param bool   $onlyForUser             If is TRUE, then the sharekey is only for the user
2803
 * @param bool   $deleteAll               If is TRUE, then all existing entries are deleted
2804
 * @param array  $objectKeyArray          Array of objects
2805
 * @param int    $all_users_except_id     All users except this one
2806
 * @param int    $apiUserId               API User ID
2807
 *
2808
 * @return void
2809
 */
2810
function storeUsersShareKey(
2811
    string $object_name,
2812
    int $post_folder_is_personal,
2813
    int $post_object_id,
2814
    string $objectKey,
2815
    bool $onlyForUser = false,
2816
    bool $deleteAll = true,
2817
    array $objectKeyArray = [],
2818
    int $all_users_except_id = -1,
2819
    int $apiUserId = -1
2820
): void {
2821
    
2822
    $session = SessionManager::getSession();
2823
    loadClasses('DB');
2824
2825
    // Delete existing entries for this object
2826
    if ($deleteAll === true) {
2827
        DB::delete(
2828
            $object_name,
2829
            'object_id = %i',
2830
            $post_object_id
2831
        );
2832
    }
2833
2834
    // Get the user ID
2835
    $userId = ($apiUserId === -1) ? (int) $session->get('user-id') : $apiUserId;
2836
    
2837
    // $onlyForUser is only dynamically set by external calls
2838
    if (
2839
        $onlyForUser === true || (int) $post_folder_is_personal === 1
2840
    ) {
2841
        // Only create the sharekey for a user
2842
        $user = DB::queryFirstRow(
2843
            'SELECT public_key
2844
            FROM ' . prefixTable('users') . '
2845
            WHERE id = %i
2846
            AND public_key != ""',
2847
            $userId
2848
        );
2849
2850
        if (empty($objectKey) === false) {
2851
            DB::insert(
2852
                $object_name,
2853
                [
2854
                    'object_id' => (int) $post_object_id,
2855
                    'user_id' => $userId,
2856
                    'share_key' => encryptUserObjectKey(
2857
                        $objectKey,
2858
                        $user['public_key']
2859
                    ),
2860
                ]
2861
            );
2862
        } else if (count($objectKeyArray) > 0) {
2863
            foreach ($objectKeyArray as $object) {
2864
                DB::insert(
2865
                    $object_name,
2866
                    [
2867
                        'object_id' => (int) $object['objectId'],
2868
                        'user_id' => $userId,
2869
                        'share_key' => encryptUserObjectKey(
2870
                            $object['objectKey'],
2871
                            $user['public_key']
2872
                        ),
2873
                    ]
2874
                );
2875
            }
2876
        }
2877
    } else {
2878
        // Create sharekey for each user
2879
        $user_ids = [OTV_USER_ID, SSH_USER_ID, API_USER_ID];
2880
        if ($all_users_except_id !== -1) {
2881
            array_push($user_ids, (int) $all_users_except_id);
2882
        }
2883
        $users = DB::query(
2884
            'SELECT id, public_key
2885
            FROM ' . prefixTable('users') . '
2886
            WHERE id NOT IN %li
2887
            AND public_key != ""',
2888
            $user_ids
2889
        );
2890
        //DB::debugmode(false);
2891
        foreach ($users as $user) {
2892
            // Insert in DB the new object key for this item by user
2893
            if (count($objectKeyArray) === 0) {
2894
                if (WIP === true) error_log('TEAMPASS Debug - storeUsersShareKey case1 - ' . $object_name . ' - ' . $post_object_id . ' - ' . $user['id'] . ' - ' . $objectKey);
2895
                DB::insert(
2896
                    $object_name,
2897
                    [
2898
                        'object_id' => $post_object_id,
2899
                        'user_id' => (int) $user['id'],
2900
                        'share_key' => encryptUserObjectKey(
2901
                            $objectKey,
2902
                            $user['public_key']
2903
                        ),
2904
                    ]
2905
                );
2906
            } else {
2907
                foreach ($objectKeyArray as $object) {
2908
                    if (WIP === true) error_log('TEAMPASS Debug - storeUsersShareKey case2 - ' . $object_name . ' - ' . $object['objectId'] . ' - ' . $user['id'] . ' - ' . $object['objectKey']);
2909
                    DB::insert(
2910
                        $object_name,
2911
                        [
2912
                            'object_id' => (int) $object['objectId'],
2913
                            'user_id' => (int) $user['id'],
2914
                            'share_key' => encryptUserObjectKey(
2915
                                $object['objectKey'],
2916
                                $user['public_key']
2917
                            ),
2918
                        ]
2919
                    );
2920
                }
2921
            }
2922
        }
2923
    }
2924
}
2925
2926
/**
2927
 * Is this string base64 encoded?
2928
 *
2929
 * @param string $str Encoded string?
2930
 *
2931
 * @return bool
2932
 */
2933
function isBase64(string $str): bool
2934
{
2935
    $str = (string) trim($str);
2936
    if (! isset($str[0])) {
2937
        return false;
2938
    }
2939
2940
    $base64String = (string) base64_decode($str, true);
2941
    if ($base64String && base64_encode($base64String) === $str) {
2942
        return true;
2943
    }
2944
2945
    return false;
2946
}
2947
2948
/**
2949
 * Undocumented function
2950
 *
2951
 * @param string $field Parameter
2952
 *
2953
 * @return array|bool|resource|string
2954
 */
2955
function filterString(string $field)
2956
{
2957
    // Sanitize string
2958
    $field = filter_var(trim($field), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
2959
    if (empty($field) === false) {
2960
        // Load AntiXSS
2961
        $antiXss = new AntiXSS();
2962
        // Return
2963
        return $antiXss->xss_clean($field);
2964
    }
2965
2966
    return false;
2967
}
2968
2969
/**
2970
 * CHeck if provided credentials are allowed on server
2971
 *
2972
 * @param string $login    User Login
2973
 * @param string $password User Pwd
2974
 * @param array  $SETTINGS Teampass settings
2975
 *
2976
 * @return bool
2977
 */
2978
function ldapCheckUserPassword(string $login, string $password, array $SETTINGS): bool
2979
{
2980
    // Build ldap configuration array
2981
    $config = [
2982
        // Mandatory Configuration Options
2983
        'hosts' => [$SETTINGS['ldap_hosts']],
2984
        'base_dn' => $SETTINGS['ldap_bdn'],
2985
        'username' => $SETTINGS['ldap_username'],
2986
        'password' => $SETTINGS['ldap_password'],
2987
2988
        // Optional Configuration Options
2989
        'port' => $SETTINGS['ldap_port'],
2990
        'use_ssl' => (int) $SETTINGS['ldap_ssl'] === 1 ? true : false,
2991
        'use_tls' => (int) $SETTINGS['ldap_tls'] === 1 ? true : false,
2992
        'version' => 3,
2993
        'timeout' => 5,
2994
        'follow_referrals' => false,
2995
2996
        // Custom LDAP Options
2997
        'options' => [
2998
            // See: http://php.net/ldap_set_option
2999
            LDAP_OPT_X_TLS_REQUIRE_CERT => (isset($SETTINGS['ldap_tls_certificate_check']) ? $SETTINGS['ldap_tls_certificate_check'] : LDAP_OPT_X_TLS_HARD),
3000
        ],
3001
    ];
3002
    
3003
    $connection = new Connection($config);
3004
    // Connect to LDAP
3005
    try {
3006
        $connection->connect();
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
    // Authenticate user
3018
    try {
3019
        if ($SETTINGS['ldap_type'] === 'ActiveDirectory') {
3020
            $connection->auth()->attempt($login, $password, $stayAuthenticated = true);
3021
        } else {
3022
            $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);
3023
        }
3024
    } catch (\LdapRecord\Auth\BindException $e) {
3025
        $error = $e->getDetailedError();
3026
        if ($error && defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
3027
            error_log('TEAMPASS Error - LDAP - '.$error->getErrorCode()." - ".$error->getErrorMessage(). " - ".$error->getDiagnosticMessage());
3028
        }
3029
        // deepcode ignore ServerLeak: No important data is sent
3030
        echo 'An error occurred.';
3031
        return false;
3032
    }
3033
3034
    return true;
3035
}
3036
3037
/**
3038
 * Removes from DB all sharekeys of this user
3039
 *
3040
 * @param int $userId User's id
3041
 * @param array   $SETTINGS Teampass settings
3042
 *
3043
 * @return bool
3044
 */
3045
function deleteUserObjetsKeys(int $userId, array $SETTINGS = []): bool
3046
{
3047
    // Return if technical accounts
3048
    if ($userId === OTV_USER_ID
3049
        || $userId === SSH_USER_ID
3050
        || $userId === API_USER_ID
3051
        || $userId === TP_USER_ID
3052
    ) {
3053
        return false;
3054
    }
3055
3056
    // Load class DB
3057
    loadClasses('DB');
3058
3059
    // Remove all item sharekeys items
3060
    // expect if personal item
3061
    DB::delete(
3062
        prefixTable('sharekeys_items'),
3063
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
3064
        $userId
3065
    );
3066
    // Remove all item sharekeys files
3067
    DB::delete(
3068
        prefixTable('sharekeys_files'),
3069
        'user_id = %i AND object_id NOT IN (
3070
            SELECT f.id 
3071
            FROM ' . prefixTable('items') . ' AS i 
3072
            INNER JOIN ' . prefixTable('files') . ' AS f ON f.id_item = i.id
3073
            WHERE i.perso = 1
3074
        )',
3075
        $userId
3076
    );
3077
    // Remove all item sharekeys fields
3078
    DB::delete(
3079
        prefixTable('sharekeys_fields'),
3080
        'user_id = %i AND object_id NOT IN (
3081
            SELECT c.id 
3082
            FROM ' . prefixTable('items') . ' AS i 
3083
            INNER JOIN ' . prefixTable('categories_items') . ' AS c ON c.item_id = i.id
3084
            WHERE i.perso = 1
3085
        )',
3086
        $userId
3087
    );
3088
    // Remove all item sharekeys logs
3089
    DB::delete(
3090
        prefixTable('sharekeys_logs'),
3091
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
3092
        $userId
3093
    );
3094
    // Remove all item sharekeys suggestions
3095
    DB::delete(
3096
        prefixTable('sharekeys_suggestions'),
3097
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
3098
        $userId
3099
    );
3100
    return false;
3101
}
3102
3103
/**
3104
 * Manage list of timezones   $SETTINGS Teampass settings
3105
 *
3106
 * @return array
3107
 */
3108
function timezone_list()
3109
{
3110
    static $timezones = null;
3111
    if ($timezones === null) {
3112
        $timezones = [];
3113
        $offsets = [];
3114
        $now = new DateTime('now', new DateTimeZone('UTC'));
3115
        foreach (DateTimeZone::listIdentifiers() as $timezone) {
3116
            $now->setTimezone(new DateTimeZone($timezone));
3117
            $offsets[] = $offset = $now->getOffset();
3118
            $timezones[$timezone] = '(' . format_GMT_offset($offset) . ') ' . format_timezone_name($timezone);
3119
        }
3120
3121
        array_multisort($offsets, $timezones);
3122
    }
3123
3124
    return $timezones;
3125
}
3126
3127
/**
3128
 * Provide timezone offset
3129
 *
3130
 * @param int $offset Timezone offset
3131
 *
3132
 * @return string
3133
 */
3134
function format_GMT_offset($offset): string
3135
{
3136
    $hours = intval($offset / 3600);
3137
    $minutes = abs(intval($offset % 3600 / 60));
3138
    return 'GMT' . ($offset ? sprintf('%+03d:%02d', $hours, $minutes) : '');
3139
}
3140
3141
/**
3142
 * Provides timezone name
3143
 *
3144
 * @param string $name Timezone name
3145
 *
3146
 * @return string
3147
 */
3148
function format_timezone_name($name): string
3149
{
3150
    $name = str_replace('/', ', ', $name);
3151
    $name = str_replace('_', ' ', $name);
3152
3153
    return str_replace('St ', 'St. ', $name);
3154
}
3155
3156
/**
3157
 * Provides info if user should use MFA based on roles
3158
 *
3159
 * @param string $userRolesIds  User roles ids
3160
 * @param string $mfaRoles      Roles for which MFA is requested
3161
 *
3162
 * @return bool
3163
 */
3164
function mfa_auth_requested_roles(string $userRolesIds, string $mfaRoles): bool
3165
{
3166
    if (empty($mfaRoles) === true) {
3167
        return true;
3168
    }
3169
3170
    $mfaRoles = array_values(json_decode($mfaRoles, true));
3171
    $userRolesIds = array_filter(explode(';', $userRolesIds));
3172
    if (count($mfaRoles) === 0 || count(array_intersect($mfaRoles, $userRolesIds)) > 0) {
3173
        return true;
3174
    }
3175
3176
    return false;
3177
}
3178
3179
/**
3180
 * Permits to clean a string for export purpose
3181
 *
3182
 * @param string $text
3183
 * @param bool $emptyCheckOnly
3184
 * 
3185
 * @return string
3186
 */
3187
function cleanStringForExport(string $text, bool $emptyCheckOnly = false): string
3188
{
3189
    if (is_null($text) === true || empty($text) === true) {
3190
        return '';
3191
    }
3192
    // only expected to check if $text was empty
3193
    elseif ($emptyCheckOnly === true) {
3194
        return $text;
3195
    }
3196
3197
    return strip_tags(
3198
        cleanString(
3199
            html_entity_decode($text, ENT_QUOTES | ENT_XHTML, 'UTF-8'),
3200
            true)
3201
        );
3202
}
3203
3204
/**
3205
 * Permits to check if user ID is valid
3206
 *
3207
 * @param integer $post_user_id
3208
 * @return bool
3209
 */
3210
function isUserIdValid($userId): bool
3211
{
3212
    if (is_null($userId) === false
3213
        && empty($userId) === false
3214
    ) {
3215
        return true;
3216
    }
3217
    return false;
3218
}
3219
3220
/**
3221
 * Check if a key exists and if its value equal the one expected
3222
 *
3223
 * @param string $key
3224
 * @param integer|string $value
3225
 * @param array $array
3226
 * 
3227
 * @return boolean
3228
 */
3229
function isKeyExistingAndEqual(
3230
    string $key,
3231
    /*PHP8 - integer|string*/$value,
3232
    array $array
3233
): bool
3234
{
3235
    if (isset($array[$key]) === true
3236
        && (is_int($value) === true ?
3237
            (int) $array[$key] === $value :
3238
            (string) $array[$key] === $value)
3239
    ) {
3240
        return true;
3241
    }
3242
    return false;
3243
}
3244
3245
/**
3246
 * Check if a variable is not set or equal to a value
3247
 *
3248
 * @param string|null $var
3249
 * @param integer|string $value
3250
 * 
3251
 * @return boolean
3252
 */
3253
function isKeyNotSetOrEqual(
3254
    /*PHP8 - string|null*/$var,
3255
    /*PHP8 - integer|string*/$value
3256
): bool
3257
{
3258
    if (isset($var) === false
3259
        || (is_int($value) === true ?
3260
            (int) $var === $value :
3261
            (string) $var === $value)
3262
    ) {
3263
        return true;
3264
    }
3265
    return false;
3266
}
3267
3268
/**
3269
 * Check if a key exists and if its value < to the one expected
3270
 *
3271
 * @param string $key
3272
 * @param integer $value
3273
 * @param array $array
3274
 * 
3275
 * @return boolean
3276
 */
3277
function isKeyExistingAndInferior(string $key, int $value, array $array): bool
3278
{
3279
    if (isset($array[$key]) === true && (int) $array[$key] < $value) {
3280
        return true;
3281
    }
3282
    return false;
3283
}
3284
3285
/**
3286
 * Check if a key exists and if its value > to the one expected
3287
 *
3288
 * @param string $key
3289
 * @param integer $value
3290
 * @param array $array
3291
 * 
3292
 * @return boolean
3293
 */
3294
function isKeyExistingAndSuperior(string $key, int $value, array $array): bool
3295
{
3296
    if (isset($array[$key]) === true && (int) $array[$key] > $value) {
3297
        return true;
3298
    }
3299
    return false;
3300
}
3301
3302
/**
3303
 * Check if values in array are set
3304
 * Return true if all set
3305
 * Return false if one of them is not set
3306
 *
3307
 * @param array $arrayOfValues
3308
 * @return boolean
3309
 */
3310
function isSetArrayOfValues(array $arrayOfValues): bool
3311
{
3312
    foreach($arrayOfValues as $value) {
3313
        if (isset($value) === false) {
3314
            return false;
3315
        }
3316
    }
3317
    return true;
3318
}
3319
3320
/**
3321
 * Check if values in array are set
3322
 * Return true if all set
3323
 * Return false if one of them is not set
3324
 *
3325
 * @param array $arrayOfValues
3326
 * @param integer|string $value
3327
 * @return boolean
3328
 */
3329
function isArrayOfVarsEqualToValue(
3330
    array $arrayOfVars,
3331
    /*PHP8 - integer|string*/$value
3332
) : bool
3333
{
3334
    foreach($arrayOfVars as $variable) {
3335
        if ($variable !== $value) {
3336
            return false;
3337
        }
3338
    }
3339
    return true;
3340
}
3341
3342
/**
3343
 * Checks if at least one variable in array is equal to value
3344
 *
3345
 * @param array $arrayOfValues
3346
 * @param integer|string $value
3347
 * @return boolean
3348
 */
3349
function isOneVarOfArrayEqualToValue(
3350
    array $arrayOfVars,
3351
    /*PHP8 - integer|string*/$value
3352
) : bool
3353
{
3354
    foreach($arrayOfVars as $variable) {
3355
        if ($variable === $value) {
3356
            return true;
3357
        }
3358
    }
3359
    return false;
3360
}
3361
3362
/**
3363
 * Checks is value is null, not set OR empty
3364
 *
3365
 * @param string|int|null $value
3366
 * @return boolean
3367
 */
3368
function isValueSetNullEmpty(string|int|null $value) : bool
3369
{
3370
    if (is_null($value) === true || empty($value) === true) {
3371
        return true;
3372
    }
3373
    return false;
3374
}
3375
3376
/**
3377
 * Checks if value is set and if empty is equal to passed boolean
3378
 *
3379
 * @param string|int $value
3380
 * @param boolean $boolean
3381
 * @return boolean
3382
 */
3383
function isValueSetEmpty($value, $boolean = true) : bool
3384
{
3385
    if (empty($value) === $boolean) {
3386
        return true;
3387
    }
3388
    return false;
3389
}
3390
3391
/**
3392
 * Ensure Complexity is translated
3393
 *
3394
 * @return void
3395
 */
3396
function defineComplexity() : void
3397
{
3398
    // Load user's language
3399
    $session = SessionManager::getSession();
3400
    $lang = new Language($session->get('user-language') ?? 'english');
3401
    
3402
    if (defined('TP_PW_COMPLEXITY') === false) {
3403
        define(
3404
            'TP_PW_COMPLEXITY',
3405
            [
3406
                TP_PW_STRENGTH_1 => array(TP_PW_STRENGTH_1, $lang->get('complex_level1'), 'fas fa-thermometer-empty text-danger'),
3407
                TP_PW_STRENGTH_2 => array(TP_PW_STRENGTH_2, $lang->get('complex_level2'), 'fas fa-thermometer-quarter text-warning'),
3408
                TP_PW_STRENGTH_3 => array(TP_PW_STRENGTH_3, $lang->get('complex_level3'), 'fas fa-thermometer-half text-warning'),
3409
                TP_PW_STRENGTH_4 => array(TP_PW_STRENGTH_4, $lang->get('complex_level4'), 'fas fa-thermometer-three-quarters text-success'),
3410
                TP_PW_STRENGTH_5 => array(TP_PW_STRENGTH_5, $lang->get('complex_level5'), 'fas fa-thermometer-full text-success'),
3411
            ]
3412
        );
3413
    }
3414
}
3415
3416
/**
3417
 * Uses Sanitizer to perform data sanitization
3418
 *
3419
 * @param array     $data
3420
 * @param array     $filters
3421
 * @return array|string
3422
 */
3423
function dataSanitizer(array $data, array $filters): array|string
3424
{
3425
    // Load Sanitizer library
3426
    $sanitizer = new Sanitizer($data, $filters);
3427
3428
    // Load AntiXSS
3429
    $antiXss = new AntiXSS();
3430
3431
    // Sanitize post and get variables
3432
    return $antiXss->xss_clean($sanitizer->sanitize());
3433
}
3434
3435
/**
3436
 * Permits to manage the cache tree for a user
3437
 *
3438
 * @param integer $user_id
3439
 * @param string $data
3440
 * @param array $SETTINGS
3441
 * @param string $field_update
3442
 * @return void
3443
 */
3444
function cacheTreeUserHandler(int $user_id, string $data, array $SETTINGS, string $field_update = '')
3445
{
3446
    // Load class DB
3447
    loadClasses('DB');
3448
3449
    // Exists ?
3450
    $userCacheId = DB::queryFirstRow(
3451
        'SELECT increment_id
3452
        FROM ' . prefixTable('cache_tree') . '
3453
        WHERE user_id = %i',
3454
        $user_id
3455
    );
3456
    
3457
    if (is_null($userCacheId) === true || count($userCacheId) === 0) {
3458
        // insert in table
3459
        DB::insert(
3460
            prefixTable('cache_tree'),
3461
            array(
3462
                'data' => $data,
3463
                'timestamp' => time(),
3464
                'user_id' => $user_id,
3465
                'visible_folders' => '',
3466
            )
3467
        );
3468
    } else {
3469
        if (empty($field_update) === true) {
3470
            DB::update(
3471
                prefixTable('cache_tree'),
3472
                [
3473
                    'timestamp' => time(),
3474
                    'data' => $data,
3475
                ],
3476
                'increment_id = %i',
3477
                $userCacheId['increment_id']
3478
            );
3479
        /* USELESS
3480
        } else {
3481
            DB::update(
3482
                prefixTable('cache_tree'),
3483
                [
3484
                    $field_update => $data,
3485
                ],
3486
                'increment_id = %i',
3487
                $userCacheId['increment_id']
3488
            );*/
3489
        }
3490
    }
3491
}
3492
3493
/**
3494
 * Permits to calculate a %
3495
 *
3496
 * @param float $nombre
3497
 * @param float $total
3498
 * @param float $pourcentage
3499
 * @return float
3500
 */
3501
function pourcentage(float $nombre, float $total, float $pourcentage): float
3502
{ 
3503
    $resultat = ($nombre/$total) * $pourcentage;
3504
    return round($resultat);
3505
}
3506
3507
/**
3508
 * Load the folders list from the cache
3509
 *
3510
 * @param string $fieldName
3511
 * @param string $sessionName
3512
 * @param boolean $forceRefresh
3513
 * @return array
3514
 */
3515
function loadFoldersListByCache(
3516
    string $fieldName,
3517
    string $sessionName,
3518
    bool $forceRefresh = false
3519
): array
3520
{
3521
    // Case when refresh is EXPECTED / MANDATORY
3522
    if ($forceRefresh === true) {
3523
        return [
3524
            'state' => false,
3525
            'data' => [],
3526
        ];
3527
    }
3528
    
3529
    $session = SessionManager::getSession();
3530
3531
    // Get last folder update
3532
    $lastFolderChange = DB::queryFirstRow(
3533
        'SELECT valeur FROM ' . prefixTable('misc') . '
3534
        WHERE type = %s AND intitule = %s',
3535
        'timestamp',
3536
        'last_folder_change'
3537
    );
3538
    if (DB::count() === 0) {
3539
        $lastFolderChange['valeur'] = 0;
3540
    }
3541
3542
    // Case when an update in the tree has been done
3543
    // Refresh is then mandatory
3544
    if ((int) $lastFolderChange['valeur'] > (int) (null !== $session->get('user-tree_last_refresh_timestamp') ? $session->get('user-tree_last_refresh_timestamp') : 0)) {
3545
        return [
3546
            'state' => false,
3547
            'data' => [],
3548
        ];
3549
    }
3550
    
3551
    // Does this user has a tree cache
3552
    $userCacheTree = DB::queryFirstRow(
3553
        'SELECT '.$fieldName.'
3554
        FROM ' . prefixTable('cache_tree') . '
3555
        WHERE user_id = %i',
3556
        $session->get('user-id')
3557
    );
3558
    if (empty($userCacheTree[$fieldName]) === false && $userCacheTree[$fieldName] !== '[]') {
3559
        return [
3560
            'state' => true,
3561
            'data' => $userCacheTree[$fieldName],
3562
            'extra' => '',
3563
        ];
3564
    }
3565
3566
    return [
3567
        'state' => false,
3568
        'data' => [],
3569
    ];
3570
}
3571
3572
3573
/**
3574
 * Permits to refresh the categories of folders
3575
 *
3576
 * @param array $folderIds
3577
 * @return void
3578
 */
3579
function handleFoldersCategories(
3580
    array $folderIds
3581
)
3582
{
3583
    // Load class DB
3584
    loadClasses('DB');
3585
3586
    $arr_data = array();
3587
3588
    // force full list of folders
3589
    if (count($folderIds) === 0) {
3590
        $folderIds = DB::queryFirstColumn(
3591
            'SELECT id
3592
            FROM ' . prefixTable('nested_tree') . '
3593
            WHERE personal_folder=%i',
3594
            0
3595
        );
3596
    }
3597
3598
    // Get complexity
3599
    defineComplexity();
3600
3601
    // update
3602
    foreach ($folderIds as $folder) {
3603
        // Do we have Categories
3604
        // get list of associated Categories
3605
        $arrCatList = array();
3606
        $rows_tmp = DB::query(
3607
            'SELECT c.id, c.title, c.level, c.type, c.masked, c.order, c.encrypted_data, c.role_visibility, c.is_mandatory,
3608
            f.id_category AS category_id
3609
            FROM ' . prefixTable('categories_folders') . ' AS f
3610
            INNER JOIN ' . prefixTable('categories') . ' AS c ON (f.id_category = c.parent_id)
3611
            WHERE id_folder=%i',
3612
            $folder
3613
        );
3614
        if (DB::count() > 0) {
3615
            foreach ($rows_tmp as $row) {
3616
                $arrCatList[$row['id']] = array(
3617
                    'id' => $row['id'],
3618
                    'title' => $row['title'],
3619
                    'level' => $row['level'],
3620
                    'type' => $row['type'],
3621
                    'masked' => $row['masked'],
3622
                    'order' => $row['order'],
3623
                    'encrypted_data' => $row['encrypted_data'],
3624
                    'role_visibility' => $row['role_visibility'],
3625
                    'is_mandatory' => $row['is_mandatory'],
3626
                    'category_id' => $row['category_id'],
3627
                );
3628
            }
3629
        }
3630
        $arr_data['categories'] = $arrCatList;
3631
3632
        // Now get complexity
3633
        $valTemp = '';
3634
        $data = DB::queryFirstRow(
3635
            'SELECT valeur
3636
            FROM ' . prefixTable('misc') . '
3637
            WHERE type = %s AND intitule=%i',
3638
            'complex',
3639
            $folder
3640
        );
3641
        if (DB::count() > 0 && empty($data['valeur']) === false) {
3642
            $valTemp = array(
3643
                'value' => $data['valeur'],
3644
                'text' => TP_PW_COMPLEXITY[$data['valeur']][1],
3645
            );
3646
        }
3647
        $arr_data['complexity'] = $valTemp;
3648
3649
        // Now get Roles
3650
        $valTemp = '';
3651
        $rows_tmp = DB::query(
3652
            'SELECT t.title
3653
            FROM ' . prefixTable('roles_values') . ' as v
3654
            INNER JOIN ' . prefixTable('roles_title') . ' as t ON (v.role_id = t.id)
3655
            WHERE v.folder_id = %i
3656
            GROUP BY title',
3657
            $folder
3658
        );
3659
        foreach ($rows_tmp as $record) {
3660
            $valTemp .= (empty($valTemp) === true ? '' : ' - ') . $record['title'];
3661
        }
3662
        $arr_data['visibilityRoles'] = $valTemp;
3663
3664
        // now save in DB
3665
        DB::update(
3666
            prefixTable('nested_tree'),
3667
            array(
3668
                'categories' => json_encode($arr_data),
3669
            ),
3670
            'id = %i',
3671
            $folder
3672
        );
3673
    }
3674
}
3675
3676
/**
3677
 * List all users that have specific roles
3678
 *
3679
 * @param array $roles
3680
 * @return array
3681
 */
3682
function getUsersWithRoles(
3683
    array $roles
3684
): array
3685
{
3686
    $session = SessionManager::getSession();
3687
    $arrUsers = array();
3688
3689
    foreach ($roles as $role) {
3690
        // loop on users and check if user has this role
3691
        $rows = DB::query(
3692
            'SELECT id, fonction_id
3693
            FROM ' . prefixTable('users') . '
3694
            WHERE id != %i AND admin = 0 AND fonction_id IS NOT NULL AND fonction_id != ""',
3695
            $session->get('user-id')
3696
        );
3697
        foreach ($rows as $user) {
3698
            $userRoles = is_null($user['fonction_id']) === false && empty($user['fonction_id']) === false ? explode(';', $user['fonction_id']) : [];
3699
            if (in_array($role, $userRoles, true) === true) {
3700
                array_push($arrUsers, $user['id']);
3701
            }
3702
        }
3703
    }
3704
3705
    return $arrUsers;
3706
}
3707
3708
3709
/**
3710
 * Get all users informations
3711
 *
3712
 * @param integer $userId
3713
 * @return array
3714
 */
3715
function getFullUserInfos(
3716
    int $userId
3717
): array
3718
{
3719
    if (empty($userId) === true) {
3720
        return array();
3721
    }
3722
3723
    $val = DB::queryFirstRow(
3724
        'SELECT *
3725
        FROM ' . prefixTable('users') . '
3726
        WHERE id = %i',
3727
        $userId
3728
    );
3729
3730
    return $val;
3731
}
3732
3733
/**
3734
 * Is required an upgrade
3735
 *
3736
 * @return boolean
3737
 */
3738
function upgradeRequired(): bool
3739
{
3740
    // Get settings.php
3741
    include_once __DIR__. '/../includes/config/settings.php';
3742
3743
    // Get timestamp in DB
3744
    $val = DB::queryFirstRow(
3745
        'SELECT valeur
3746
        FROM ' . prefixTable('misc') . '
3747
        WHERE type = %s AND intitule = %s',
3748
        'admin',
3749
        'upgrade_timestamp'
3750
    );
3751
3752
    // Check if upgrade is required
3753
    return (
3754
        is_null($val) || count($val) === 0 || !defined('UPGRADE_MIN_DATE') || 
3755
        empty($val['valeur']) || (int) $val['valeur'] < (int) UPGRADE_MIN_DATE
3756
    );
3757
}
3758
3759
/**
3760
 * Permits to change the user keys on his demand
3761
 *
3762
 * @param integer $userId
3763
 * @param string $passwordClear
3764
 * @param integer $nbItemsToTreat
3765
 * @param string $encryptionKey
3766
 * @param boolean $deleteExistingKeys
3767
 * @param boolean $sendEmailToUser
3768
 * @param boolean $encryptWithUserPassword
3769
 * @param boolean $generate_user_new_password
3770
 * @param string $emailBody
3771
 * @param boolean $user_self_change
3772
 * @param string $recovery_public_key
3773
 * @param string $recovery_private_key
3774
 * @param bool $userHasToEncryptPersonalItemsAfter
3775
 * @return string
3776
 */
3777
function handleUserKeys(
3778
    int $userId,
3779
    string $passwordClear,
3780
    int $nbItemsToTreat,
3781
    string $encryptionKey = '',
3782
    bool $deleteExistingKeys = false,
3783
    bool $sendEmailToUser = true,
3784
    bool $encryptWithUserPassword = false,
3785
    bool $generate_user_new_password = false,
3786
    string $emailBody = '',
3787
    bool $user_self_change = false,
3788
    string $recovery_public_key = '',
3789
    string $recovery_private_key = '',
3790
    bool $userHasToEncryptPersonalItemsAfter = false
3791
): string
3792
{
3793
    $session = SessionManager::getSession();
3794
    $lang = new Language($session->get('user-language') ?? 'english');
3795
3796
    // prepapre background tasks for item keys generation        
3797
    $userTP = DB::queryFirstRow(
3798
        'SELECT pw, public_key, private_key
3799
        FROM ' . prefixTable('users') . '
3800
        WHERE id = %i',
3801
        TP_USER_ID
3802
    );
3803
    if (DB::count() === 0) {
3804
        return prepareExchangedData(
3805
            array(
3806
                'error' => true,
3807
                'message' => 'User not exists',
3808
            ),
3809
            'encode'
3810
        );
3811
    }
3812
3813
    // Do we need to generate new user password
3814
    if ($generate_user_new_password === true) {
3815
        // Generate a new password
3816
        $passwordClear = GenerateCryptKey(20, false, true, true, false, true);
3817
    }
3818
3819
    // Create password hash
3820
    $passwordManager = new PasswordManager();
3821
    $hashedPassword = $passwordManager->hashPassword($passwordClear);
3822
    if ($passwordManager->verifyPassword($hashedPassword, $passwordClear) === false) {
3823
        return prepareExchangedData(
3824
            array(
3825
                'error' => true,
3826
                'message' => $lang->get('pw_hash_not_correct'),
3827
            ),
3828
            'encode'
3829
        );
3830
    }
3831
3832
    // Check if valid public/private keys
3833
    if ($recovery_public_key !== '' && $recovery_private_key !== '') {
3834
        try {
3835
            // Generate random string
3836
            $random_str = generateQuickPassword(12, false);
3837
            // Encrypt random string with user publick key
3838
            $encrypted = encryptUserObjectKey($random_str, $recovery_public_key);
3839
            // Decrypt $encrypted with private key
3840
            $decrypted = decryptUserObjectKey($encrypted, $recovery_private_key);
3841
            // Check if decryptUserObjectKey returns our random string
3842
            if ($decrypted !== $random_str) {
3843
                throw new Exception('Public/Private keypair invalid.');
3844
            }
3845
        } catch (Exception $e) {
3846
            // Show error message to user and log event
3847
            if (defined('LOG_TO_SERVER') && LOG_TO_SERVER === true) {
3848
                error_log('ERROR: User '.$userId.' - '.$e->getMessage());
3849
            }
3850
            return prepareExchangedData([
3851
                    'error' => true,
3852
                    'message' => $lang->get('pw_encryption_error'),
3853
                ],
3854
                'encode'
3855
            );
3856
        }
3857
    }
3858
3859
    // Generate new keys
3860
    if ($user_self_change === true && empty($recovery_public_key) === false && empty($recovery_private_key) === false){
3861
        $userKeys = [
3862
            'public_key' => $recovery_public_key,
3863
            'private_key_clear' => $recovery_private_key,
3864
            'private_key' => encryptPrivateKey($passwordClear, $recovery_private_key),
3865
        ];
3866
    } else {
3867
        $userKeys = generateUserKeys($passwordClear);
3868
    }
3869
    
3870
    // Handle private key
3871
    insertPrivateKeyWithCurrentFlag(
3872
        $userId,
3873
        $userKeys['private_key'],        
3874
    );
3875
3876
    // Save in DB
3877
    // TODO: remove private key field from Users table
3878
    DB::update(
3879
        prefixTable('users'),
3880
        array(
3881
            'pw' => $hashedPassword,
3882
            'public_key' => $userKeys['public_key'],
3883
            'private_key' => $userKeys['private_key'],
3884
            'keys_recovery_time' => NULL,
3885
        ),
3886
        'id=%i',
3887
        $userId
3888
    );
3889
3890
3891
    // update session too
3892
    if ($userId === $session->get('user-id')) {
3893
        $session->set('user-private_key', $userKeys['private_key_clear']);
3894
        $session->set('user-public_key', $userKeys['public_key']);
3895
        // Notify user that he must re download his keys:
3896
        $session->set('user-keys_recovery_time', NULL);
3897
    }
3898
3899
    // Manage empty encryption key
3900
    // Let's take the user's password if asked and if no encryption key provided
3901
    $encryptionKey = $encryptWithUserPassword === true && empty($encryptionKey) === true ? $passwordClear : $encryptionKey;
3902
3903
    // Create process
3904
    DB::insert(
3905
        prefixTable('background_tasks'),
3906
        array(
3907
            'created_at' => time(),
3908
            'process_type' => 'create_user_keys',
3909
            'arguments' => json_encode([
3910
                'new_user_id' => (int) $userId,
3911
                'new_user_pwd' => cryption($passwordClear, '','encrypt')['string'],
3912
                'new_user_code' => cryption(empty($encryptionKey) === true ? uniqidReal(20) : $encryptionKey, '','encrypt')['string'],
3913
                'owner_id' => (int) TP_USER_ID,
3914
                'creator_pwd' => $userTP['pw'],
3915
                'send_email' => $sendEmailToUser === true ? 1 : 0,
3916
                'otp_provided_new_value' => 1,
3917
                'email_body' => empty($emailBody) === true ? '' : $lang->get($emailBody),
3918
                'user_self_change' => $user_self_change === true ? 1 : 0,
3919
                'userHasToEncryptPersonalItemsAfter' => $userHasToEncryptPersonalItemsAfter === true ? 1 : 0,
3920
            ]),
3921
        )
3922
    );
3923
    $processId = DB::insertId();
3924
3925
    // Delete existing keys
3926
    if ($deleteExistingKeys === true) {
3927
        deleteUserObjetsKeys(
3928
            (int) $userId,
3929
        );
3930
    }
3931
3932
    // Create tasks
3933
    createUserTasks($processId, $nbItemsToTreat);
3934
3935
    // update user's new status
3936
    DB::update(
3937
        prefixTable('users'),
3938
        [
3939
            'is_ready_for_usage' => 0,
3940
            'otp_provided' => 1,
3941
            'ongoing_process_id' => $processId,
3942
            'special' => 'generate-keys',
3943
        ],
3944
        'id=%i',
3945
        $userId
3946
    );
3947
3948
    return prepareExchangedData(
3949
        array(
3950
            'error' => false,
3951
            'message' => '',
3952
            'user_password' => $generate_user_new_password === true ? $passwordClear : '',
3953
        ),
3954
        'encode'
3955
    );
3956
}
3957
3958
/**
3959
 * Permits to generate a new password for a user
3960
 *
3961
 * @param integer $processId
3962
 * @param integer $nbItemsToTreat
3963
 * @return void
3964
 
3965
 */
3966
function createUserTasks($processId, $nbItemsToTreat): void
3967
{
3968
    // Create subtask for step 0
3969
    DB::insert(
3970
        prefixTable('background_subtasks'),
3971
        array(
3972
            'task_id' => $processId,
3973
            'created_at' => time(),
3974
            'task' => json_encode([
3975
                'step' => 'step0',
3976
                'index' => 0,
3977
                'nb' => $nbItemsToTreat,
3978
            ]),
3979
        )
3980
    );
3981
3982
    // Prepare the subtask queries
3983
    $queries = [
3984
        'step20' => 'SELECT * FROM ' . prefixTable('items'),
3985
3986
        'step30' => 'SELECT * FROM ' . prefixTable('log_items') . 
3987
                    ' WHERE raison LIKE "at_pw :%" AND encryption_type = "teampass_aes"',
3988
3989
        'step40' => 'SELECT * FROM ' . prefixTable('categories_items') . 
3990
                    ' WHERE encryption_type = "teampass_aes"',
3991
3992
        'step50' => 'SELECT * FROM ' . prefixTable('suggestion'),
3993
3994
        'step60' => 'SELECT * FROM ' . prefixTable('files') . ' AS f
3995
                        INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
3996
                        WHERE f.status = "' . TP_ENCRYPTION_NAME . '"'
3997
    ];
3998
3999
    // Perform loop on $queries to create sub-tasks
4000
    foreach ($queries as $step => $query) {
4001
        DB::query($query);
4002
        createAllSubTasks($step, DB::count(), $nbItemsToTreat, $processId);
4003
    }
4004
4005
    // Create subtask for step 99
4006
    DB::insert(
4007
        prefixTable('background_subtasks'),
4008
        array(
4009
            'task_id' => $processId,
4010
            'created_at' => time(),
4011
            'task' => json_encode([
4012
                'step' => 'step99',
4013
            ]),
4014
        )
4015
    );
4016
}
4017
4018
/**
4019
 * Create all subtasks for a given action
4020
 * @param string $action The action to be performed
4021
 * @param int $totalElements Total number of elements to process
4022
 * @param int $elementsPerIteration Number of elements per iteration
4023
 * @param int $taskId The ID of the task
4024
 */
4025
function createAllSubTasks($action, $totalElements, $elementsPerIteration, $taskId) {
4026
    // Calculate the number of iterations
4027
    $iterations = ceil($totalElements / $elementsPerIteration);
4028
4029
    // Create the subtasks
4030
    for ($i = 0; $i < $iterations; $i++) {
4031
        DB::insert(prefixTable('background_subtasks'), [
4032
            'task_id' => $taskId,
4033
            'created_at' => time(),
4034
            'task' => json_encode([
4035
                "step" => $action,
4036
                "index" => $i * $elementsPerIteration,
4037
                "nb" => $elementsPerIteration,
4038
            ]),
4039
        ]);
4040
    }
4041
}
4042
4043
/**
4044
 * Permeits to check the consistency of date versus columns definition
4045
 *
4046
 * @param string $table
4047
 * @param array $dataFields
4048
 * @return array
4049
 */
4050
function validateDataFields(
4051
    string $table,
4052
    array $dataFields
4053
): array
4054
{
4055
    // Get table structure
4056
    $result = DB::query(
4057
        "SELECT `COLUMN_NAME`, `CHARACTER_MAXIMUM_LENGTH` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%l' AND TABLE_NAME = '%l';",
4058
        DB_NAME,
4059
        $table
4060
    );
4061
4062
    foreach ($result as $row) {
4063
        $field = $row['COLUMN_NAME'];
4064
        $maxLength = is_null($row['CHARACTER_MAXIMUM_LENGTH']) === false ? (int) $row['CHARACTER_MAXIMUM_LENGTH'] : '';
4065
4066
        if (isset($dataFields[$field]) === true && is_array($dataFields[$field]) === false && empty($maxLength) === false) {
4067
            if (strlen((string) $dataFields[$field]) > $maxLength) {
4068
                return [
4069
                    'state' => false,
4070
                    'field' => $field,
4071
                    'maxLength' => $maxLength,
4072
                    'currentLength' => strlen((string) $dataFields[$field]),
4073
                ];
4074
            }
4075
        }
4076
    }
4077
    
4078
    return [
4079
        'state' => true,
4080
        'message' => '',
4081
    ];
4082
}
4083
4084
/**
4085
 * Adapt special characters sanitized during filter_var with option FILTER_SANITIZE_SPECIAL_CHARS operation
4086
 *
4087
 * @param string $string
4088
 * @return string
4089
 */
4090
function filterVarBack(string $string): string
4091
{
4092
    $arr = [
4093
        '&#060;' => '<',
4094
        '&#062;' => '>',
4095
        '&#034;' => '"',
4096
        '&#039;' => "'",
4097
        '&#038;' => '&',
4098
    ];
4099
4100
    foreach ($arr as $key => $value) {
4101
        $string = str_replace($key, $value, $string);
4102
    }
4103
4104
    return $string;
4105
}
4106
4107
/**
4108
 * 
4109
 */
4110
function storeTask(
4111
    string $taskName,
4112
    int $user_id,
4113
    int $is_personal_folder,
4114
    int $folder_destination_id,
4115
    int $item_id,
4116
    string $object_keys,
4117
    array $fields_keys = [],
4118
    array $files_keys = []
4119
)
4120
{
4121
    if (in_array($taskName, ['item_copy', 'new_item', 'update_item'])) {
4122
        // Create process
4123
        DB::insert(
4124
            prefixTable('background_tasks'),
4125
            array(
4126
                'created_at' => time(),
4127
                'process_type' => $taskName,
4128
                'arguments' => json_encode([
4129
                    'item_id' => $item_id,
4130
                    'object_key' => $object_keys,
4131
                ]),
4132
                'item_id' => $item_id,
4133
            )
4134
        );
4135
        $processId = DB::insertId();
4136
4137
        // Create tasks
4138
        // 1- Create password 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_pwd_key',
4146
                    'index' => 0,
4147
                ]),
4148
            )
4149
        );
4150
4151
        // 2- Create fields sharekeys for users of this new ITEM
4152
        DB::insert(
4153
            prefixTable('background_subtasks'),
4154
            array(
4155
                'task_id' => $processId,
4156
                'created_at' => time(),
4157
                'task' => json_encode([
4158
                    'step' => 'create_users_fields_key',
4159
                    'index' => 0,
4160
                    'fields_keys' => $fields_keys,
4161
                ]),
4162
            )
4163
        );
4164
4165
        // 3- Create files sharekeys for users of this new ITEM
4166
        DB::insert(
4167
            prefixTable('background_subtasks'),
4168
            array(
4169
                'task_id' => $processId,
4170
                'created_at' => time(),
4171
                'task' => json_encode([
4172
                    'step' => 'create_users_files_key',
4173
                    'index' => 0,
4174
                    'files_keys' => $files_keys,
4175
                ]),
4176
            )
4177
        );
4178
    }
4179
}
4180
4181
/**
4182
 * 
4183
 */
4184
function createTaskForItem(
4185
    string $processType,
4186
    string|array $taskName,
4187
    int $itemId,
4188
    int $userId,
4189
    string $objectKey,
4190
    int $parentId = -1,
4191
    array $fields_keys = [],
4192
    array $files_keys = []
4193
)
4194
{
4195
    // 1- Create main process
4196
    // ---
4197
    
4198
    // Create process
4199
    DB::insert(
4200
        prefixTable('background_tasks'),
4201
        array(
4202
            'created_at' => time(),
4203
            'process_type' => $processType,
4204
            'arguments' => json_encode([
4205
                'all_users_except_id' => (int) $userId,
4206
                'item_id' => (int) $itemId,
4207
                'object_key' => $objectKey,
4208
                'author' => (int) $userId,
4209
            ]),
4210
            'item_id' => (int) $parentId !== -1 ?  $parentId : null,
4211
        )
4212
    );
4213
    $processId = DB::insertId();
4214
4215
    // 2- Create expected tasks
4216
    // ---
4217
    if (is_array($taskName) === false) {
4218
        $taskName = [$taskName];
4219
    }
4220
    foreach($taskName as $task) {
4221
        if (WIP === true) error_log('createTaskForItem - task: '.$task);
4222
        switch ($task) {
4223
            case 'item_password':
4224
                
4225
                DB::insert(
4226
                    prefixTable('background_subtasks'),
4227
                    array(
4228
                        'task_id' => $processId,
4229
                        'created_at' => time(),
4230
                        'task' => json_encode([
4231
                            'step' => 'create_users_pwd_key',
4232
                            'index' => 0,
4233
                        ]),
4234
                    )
4235
                );
4236
4237
                break;
4238
            case 'item_field':
4239
                
4240
                DB::insert(
4241
                    prefixTable('background_subtasks'),
4242
                    array(
4243
                        'task_id' => $processId,
4244
                        'created_at' => time(),
4245
                        'task' => json_encode([
4246
                            'step' => 'create_users_fields_key',
4247
                            'index' => 0,
4248
                            'fields_keys' => $fields_keys,
4249
                        ]),
4250
                    )
4251
                );
4252
4253
                break;
4254
            case 'item_file':
4255
4256
                DB::insert(
4257
                    prefixTable('background_subtasks'),
4258
                    array(
4259
                        'task_id' => $processId,
4260
                        'created_at' => time(),
4261
                        'task' => json_encode([
4262
                            'step' => 'create_users_files_key',
4263
                            'index' => 0,
4264
                            'fields_keys' => $files_keys,
4265
                        ]),
4266
                    )
4267
                );
4268
                break;
4269
            default:
4270
                # code...
4271
                break;
4272
        }
4273
    }
4274
}
4275
4276
4277
function deleteProcessAndRelatedTasks(int $processId)
4278
{
4279
    // Delete process
4280
    DB::delete(
4281
        prefixTable('background_tasks'),
4282
        'id=%i',
4283
        $processId
4284
    );
4285
4286
    // Delete tasks
4287
    DB::delete(
4288
        prefixTable('background_subtasks'),
4289
        'task_id=%i',
4290
        $processId
4291
    );
4292
4293
}
4294
4295
/**
4296
 * Return PHP binary path
4297
 *
4298
 * @return string
4299
 */
4300
function getPHPBinary(): string
4301
{
4302
    // Get PHP binary path
4303
    $phpBinaryFinder = new PhpExecutableFinder();
4304
    $phpBinaryPath = $phpBinaryFinder->find();
4305
    return $phpBinaryPath === false ? 'false' : $phpBinaryPath;
4306
}
4307
4308
4309
4310
/**
4311
 * Delete unnecessary keys for personal items
4312
 *
4313
 * @param boolean $allUsers
4314
 * @param integer $user_id
4315
 * @return void
4316
 */
4317
function purgeUnnecessaryKeys(bool $allUsers = true, int $user_id=0)
4318
{
4319
    if ($allUsers === true) {
4320
        // Load class DB
4321
        if (class_exists('DB') === false) {
4322
            loadClasses('DB');
4323
        }
4324
4325
        $users = DB::query(
4326
            'SELECT id
4327
            FROM ' . prefixTable('users') . '
4328
            WHERE id NOT IN ('.OTV_USER_ID.', '.TP_USER_ID.', '.SSH_USER_ID.', '.API_USER_ID.')
4329
            ORDER BY login ASC'
4330
        );
4331
        foreach ($users as $user) {
4332
            purgeUnnecessaryKeysForUser((int) $user['id']);
4333
        }
4334
    } else {
4335
        purgeUnnecessaryKeysForUser((int) $user_id);
4336
    }
4337
}
4338
4339
/**
4340
 * Delete unnecessary keys for personal items
4341
 *
4342
 * @param integer $user_id
4343
 * @return void
4344
 */
4345
function purgeUnnecessaryKeysForUser(int $user_id=0)
4346
{
4347
    if ($user_id === 0) {
4348
        return;
4349
    }
4350
4351
    // Load class DB
4352
    loadClasses('DB');
4353
4354
    $personalItems = DB::queryFirstColumn(
4355
        'SELECT id
4356
        FROM ' . prefixTable('items') . ' AS i
4357
        INNER JOIN ' . prefixTable('log_items') . ' AS li ON li.id_item = i.id
4358
        WHERE i.perso = 1 AND li.action = "at_creation" AND li.id_user IN (%i, '.TP_USER_ID.')',
4359
        $user_id
4360
    );
4361
    if (count($personalItems) > 0) {
4362
        // Item keys
4363
        DB::delete(
4364
            prefixTable('sharekeys_items'),
4365
            'object_id IN %li AND user_id NOT IN %ls',
4366
            $personalItems,
4367
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4368
        );
4369
        // Files keys
4370
        DB::delete(
4371
            prefixTable('sharekeys_files'),
4372
            'object_id IN %li AND user_id NOT IN %ls',
4373
            $personalItems,
4374
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4375
        );
4376
        // Fields keys
4377
        DB::delete(
4378
            prefixTable('sharekeys_fields'),
4379
            'object_id IN %li AND user_id NOT IN %ls',
4380
            $personalItems,
4381
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4382
        );
4383
        // Logs keys
4384
        DB::delete(
4385
            prefixTable('sharekeys_logs'),
4386
            'object_id IN %li AND user_id NOT IN %ls',
4387
            $personalItems,
4388
            [$user_id, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4389
        );
4390
    }
4391
}
4392
4393
/**
4394
 * Generate recovery keys file
4395
 *
4396
 * @param integer $userId
4397
 * @param array $SETTINGS
4398
 * @return string
4399
 */
4400
function handleUserRecoveryKeysDownload(int $userId, array $SETTINGS):string
4401
{
4402
    $session = SessionManager::getSession();
4403
    // Check if user exists
4404
    $userInfo = DB::queryFirstRow(
4405
        'SELECT login
4406
        FROM ' . prefixTable('users') . '
4407
        WHERE id = %i',
4408
        $userId
4409
    );
4410
4411
    if (DB::count() > 0) {
4412
        $now = (int) time();
4413
        // Prepare file content
4414
        $export_value = file_get_contents(__DIR__."/../includes/core/teampass_ascii.txt")."\n".
4415
            "Generation date: ".date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now)."\n\n".
4416
            "RECOVERY KEYS - Not to be shared - To be store safely\n\n".
4417
            "Public Key:\n".$session->get('user-public_key')."\n\n".
4418
            "Private Key:\n".$session->get('user-private_key')."\n\n";
4419
4420
        // Update user's keys_recovery_time
4421
        DB::update(
4422
            prefixTable('users'),
4423
            [
4424
                'keys_recovery_time' => $now,
4425
            ],
4426
            'id=%i',
4427
            $userId
4428
        );
4429
        $session->set('user-keys_recovery_time', $now);
4430
4431
        //Log into DB the user's disconnection
4432
        logEvents($SETTINGS, 'user_mngt', 'at_user_keys_download', (string) $userId, $userInfo['login']);
4433
        
4434
        // Return data
4435
        return prepareExchangedData(
4436
            array(
4437
                'error' => false,
4438
                'datetime' => date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now),
4439
                'timestamp' => $now,
4440
                'content' => base64_encode($export_value),
4441
                'login' => $userInfo['login'],
4442
            ),
4443
            'encode'
4444
        );
4445
    }
4446
4447
    return prepareExchangedData(
4448
        array(
4449
            'error' => true,
4450
            'datetime' => '',
4451
        ),
4452
        'encode'
4453
    );
4454
}
4455
4456
/**
4457
 * Permits to load expected classes
4458
 *
4459
 * @param string $className
4460
 * @return void
4461
 */
4462
function loadClasses(string $className = ''): void
4463
{
4464
    require_once __DIR__. '/../includes/config/include.php';
4465
    require_once __DIR__. '/../includes/config/settings.php';
4466
    require_once __DIR__.'/../vendor/autoload.php';
4467
4468
    if (defined('DB_PASSWD_CLEAR') === false) {
4469
        define('DB_PASSWD_CLEAR', defuseReturnDecrypted(DB_PASSWD));
4470
    }
4471
4472
    if (empty($className) === false) {
4473
        // Load class DB
4474
        if ((string) $className === 'DB') {
4475
            //Connect to DB
4476
            DB::$host = DB_HOST;
4477
            DB::$user = DB_USER;
4478
            DB::$password = DB_PASSWD_CLEAR;
4479
            DB::$dbName = DB_NAME;
4480
            DB::$port = DB_PORT;
4481
            DB::$encoding = DB_ENCODING;
4482
            DB::$ssl = DB_SSL;
4483
            DB::$connect_options = DB_CONNECT_OPTIONS;
4484
        }
4485
    }
4486
}
4487
4488
/**
4489
 * Returns the page the user is visiting.
4490
 *
4491
 * @return string The page name
4492
 */
4493
function getCurrectPage($SETTINGS)
4494
{
4495
    
4496
    $request = SymfonyRequest::createFromGlobals();
4497
4498
    // Parse the url
4499
    parse_str(
4500
        substr(
4501
            (string) $request->getRequestUri(),
4502
            strpos((string) $request->getRequestUri(), '?') + 1
4503
        ),
4504
        $result
4505
    );
4506
4507
    return $result['page'];
4508
}
4509
4510
/**
4511
 * Permits to return value if set
4512
 *
4513
 * @param string|int $value
4514
 * @param string|int|null $retFalse
4515
 * @param string|int $retTrue
4516
 * @return mixed
4517
 */
4518
function returnIfSet($value, $retFalse = '', $retTrue = null): mixed
4519
{
4520
    if (!empty($value)) {
4521
        return is_null($retTrue) ? $value : $retTrue;
4522
    }
4523
    return $retFalse;
4524
}
4525
4526
4527
/**
4528
 * SEnd email to user
4529
 *
4530
 * @param string $post_receipt
4531
 * @param string $post_body
4532
 * @param string $post_subject
4533
 * @param array $post_replace
4534
 * @param boolean $immediate_email
4535
 * @param string $encryptedUserPassword
4536
 * @return string
4537
 */
4538
function sendMailToUser(
4539
    string $post_receipt,
4540
    string $post_body,
4541
    string $post_subject,
4542
    array $post_replace,
4543
    bool $immediate_email = false,
4544
    $encryptedUserPassword = ''
4545
): ?string {
4546
    global $SETTINGS;
4547
    $emailSettings = new EmailSettings($SETTINGS);
4548
    $emailService = new EmailService();
4549
    $antiXss = new AntiXSS();
4550
4551
    // Sanitize inputs
4552
    $post_receipt = filter_var($post_receipt, FILTER_SANITIZE_EMAIL);
4553
    $post_subject = $antiXss->xss_clean($post_subject);
4554
    $post_body = $antiXss->xss_clean($post_body);
4555
4556
    if (count($post_replace) > 0) {
4557
        $post_body = str_replace(
4558
            array_keys($post_replace),
4559
            array_values($post_replace),
4560
            $post_body
4561
        );
4562
    }
4563
4564
    // Remove newlines to prevent header injection
4565
    $post_body = str_replace(array("\r", "\n"), '', $post_body);    
4566
4567
    if ($immediate_email === true) {
4568
        // Send email
4569
        $ret = $emailService->sendMail(
4570
            $post_subject,
4571
            $post_body,
4572
            $post_receipt,
4573
            $emailSettings,
4574
            '',
4575
            false
4576
        );
4577
    
4578
        $ret = json_decode($ret, true);
4579
    
4580
        return prepareExchangedData(
4581
            array(
4582
                'error' => empty($ret['error']) === true ? false : true,
4583
                'message' => $ret['message'],
4584
            ),
4585
            'encode'
4586
        );
4587
    } else {
4588
        // Send through task handler
4589
        prepareSendingEmail(
4590
            $post_subject,
4591
            $post_body,
4592
            $post_receipt,
4593
            "",
4594
            $encryptedUserPassword,
4595
        );
4596
    }
4597
4598
    return null;
4599
}
4600
4601
/**
4602
 * Converts a password strengh value to zxcvbn level
4603
 * 
4604
 * @param integer $passwordStrength
4605
 * 
4606
 * @return integer
4607
 */
4608
function convertPasswordStrength($passwordStrength): int
4609
{
4610
    if ($passwordStrength === 0) {
4611
        return TP_PW_STRENGTH_1;
4612
    } else if ($passwordStrength === 1) {
4613
        return TP_PW_STRENGTH_2;
4614
    } else if ($passwordStrength === 2) {
4615
        return TP_PW_STRENGTH_3;
4616
    } else if ($passwordStrength === 3) {
4617
        return TP_PW_STRENGTH_4;
4618
    } else {
4619
        return TP_PW_STRENGTH_5;
4620
    }
4621
}
4622
4623
/**
4624
 * Check that a password is strong. The password needs to have at least :
4625
 *   - length >= 10.
4626
 *   - Uppercase and lowercase chars.
4627
 *   - Number or special char.
4628
 *   - Not contain username, name or mail part.
4629
 *   - Different from previous password.
4630
 * 
4631
 * @param string $password - Password to ckeck.
4632
 * @return bool - true if the password is strong, false otherwise.
4633
 */
4634
function isPasswordStrong($password) {
4635
    $session = SessionManager::getSession();
4636
4637
    // Password can't contain login, name or lastname
4638
    $forbiddenWords = [
4639
        $session->get('user-login'),
4640
        $session->get('user-name'),
4641
        $session->get('user-lastname'),
4642
    ];
4643
4644
    // Cut out the email
4645
    if ($email = $session->get('user-email')) {
4646
        $emailParts = explode('@', $email);
4647
4648
        if (count($emailParts) === 2) {
4649
            // Mail username (removed @domain.tld)
4650
            $forbiddenWords[] = $emailParts[0];
4651
4652
            // Organisation name (removed username@ and .tld)
4653
            $domain = explode('.', $emailParts[1]);
4654
            if (count($domain) > 1)
4655
                $forbiddenWords[] = $domain[0];
4656
        }
4657
    }
4658
4659
    // Search forbidden words in password
4660
    foreach ($forbiddenWords as $word) {
4661
        if (empty($word))
4662
            continue;
4663
4664
        // Stop if forbidden word found in password
4665
        if (stripos($password, $word) !== false)
4666
            return false;
4667
    }
4668
4669
    // Get password complexity
4670
    $length = strlen($password);
4671
    $hasUppercase = preg_match('/[A-Z]/', $password);
4672
    $hasLowercase = preg_match('/[a-z]/', $password);
4673
    $hasNumber = preg_match('/[0-9]/', $password);
4674
    $hasSpecialChar = preg_match('/[\W_]/', $password);
4675
4676
    // Get current user hash
4677
    $userHash = DB::queryFirstRow(
4678
        "SELECT pw FROM " . prefixtable('users') . " WHERE id = %d;",
4679
        $session->get('user-id')
4680
    )['pw'];
4681
4682
    $passwordManager = new PasswordManager();
4683
    
4684
    return $length >= 8
4685
           && $hasUppercase
4686
           && $hasLowercase
4687
           && ($hasNumber || $hasSpecialChar)
4688
           && !$passwordManager->verifyPassword($userHash, $password);
4689
}
4690
4691
4692
/**
4693
 * Converts a value to a string, handling various types and cases.
4694
 *
4695
 * @param mixed $value La valeur à convertir
4696
 * @param string $default Valeur par défaut si la conversion n'est pas possible
4697
 * @return string
4698
 */
4699
function safeString($value, string $default = ''): string
4700
{
4701
    // Simple cases
4702
    if (is_string($value)) {
4703
        return $value;
4704
    }
4705
    
4706
    if (is_scalar($value)) {
4707
        return (string) $value;
4708
    }
4709
    
4710
    // Special cases
4711
    if (is_null($value)) {
4712
        return $default;
4713
    }
4714
    
4715
    if (is_array($value)) {
4716
        return empty($value) ? $default : json_encode($value, JSON_UNESCAPED_UNICODE);
4717
    }
4718
    
4719
    if (is_object($value)) {
4720
        // Vérifie si l'objet implémente __toString()
4721
        if (method_exists($value, '__toString')) {
4722
            return (string) $value;
4723
        }
4724
        
4725
        // Alternative: serialize ou json selon le contexte
4726
        return get_class($value) . (method_exists($value, 'getId') ? '#' . $value->getId() : '');
4727
    }
4728
    
4729
    if (is_resource($value)) {
4730
        return 'Resource#' . get_resource_id($value) . ' of type ' . get_resource_type($value);
4731
    }
4732
    
4733
    // Cas par défaut
4734
    return $default;
4735
}
4736
4737
/**
4738
 * Check if a user has access to a file
4739
 *
4740
 * @param integer $userId
4741
 * @param integer $fileId
4742
 * @return boolean
4743
 */
4744
function userHasAccessToFile(int $userId, int $fileId): bool
4745
{
4746
    // Check if user is admin
4747
    // Refuse access if user does not exist and/or is admin
4748
    $user = DB::queryFirstRow(
4749
        'SELECT admin
4750
        FROM ' . prefixTable('users') . '
4751
        WHERE id = %i',
4752
        $userId
4753
    );
4754
    if (DB::count() === 0 || (int) $user['admin'] === 1) {
4755
        return false;
4756
    }
4757
4758
    // Get file info
4759
    $file = DB::queryFirstRow(
4760
        'SELECT f.id_item, i.id_tree
4761
        FROM ' . prefixTable('files') . ' as f
4762
        INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
4763
        WHERE f.id = %i',
4764
        $fileId
4765
    );
4766
    if (DB::count() === 0) {
4767
        return false;
4768
    }
4769
4770
    // Check if user has access to the item
4771
    include_once __DIR__. '/items.queries.php';
4772
    $itemAccess = getCurrentAccessRights(
4773
        (int) filter_var($userId, FILTER_SANITIZE_NUMBER_INT),
4774
        (int) filter_var($file['id_item'], FILTER_SANITIZE_NUMBER_INT),
4775
        (int) filter_var($file['id_tree'], FILTER_SANITIZE_NUMBER_INT),
4776
        (string) filter_var('show', FILTER_SANITIZE_SPECIAL_CHARS),
4777
    );
4778
4779
    return $itemAccess['access'] === true;
4780
}
4781
4782
/**
4783
 * Check if a user has access to a backup file
4784
 * 
4785
 * @param integer $userId
4786
 * @param string $file
4787
 * @param string $key
4788
 * @param string $keyTmp
4789
 * @return boolean
4790
 */
4791
function userHasAccessToBackupFile(int $userId, string $file, string $key, string $keyTmp): bool
4792
{
4793
    $session = SessionManager::getSession();
4794
4795
    // Ensure session keys are ok
4796
    if ($session->get('key') !== $key || $session->get('user-key_tmp') !== $keyTmp) {
4797
        return false;
4798
    }
4799
    
4800
    // Check if user is admin
4801
    // Refuse access if user does not exist and/or is not admin
4802
    $user = DB::queryFirstRow(
4803
        'SELECT admin
4804
        FROM ' . prefixTable('users') . '
4805
        WHERE id = %i',
4806
        $userId
4807
    );
4808
    if (DB::count() === 0 || (int) $user['admin'] === 0) {
4809
        return false;
4810
    }
4811
    
4812
    // Ensure that user has performed the backup
4813
    DB::queryFirstRow(
4814
        'SELECT f.id
4815
        FROM ' . prefixTable('log_system') . ' as f
4816
        WHERE f.type = %s AND f.label = %s AND f.qui = %i AND f.field_1 = %s',
4817
        'admin_action',
4818
        'dataBase backup',
4819
        $userId,
4820
        $file
4821
    );
4822
    if (DB::count() === 0) {
4823
        return false;
4824
    }
4825
4826
    return true;
4827
}
4828
4829
/**
4830
 * Ensure that personal items have only keys for their owner
4831
 *
4832
 * @param integer $userId
4833
 * @param integer $itemId
4834
 * @return boolean
4835
 */
4836
function EnsurePersonalItemHasOnlyKeysForOwner(int $userId, int $itemId): bool
4837
{
4838
    // Check if user is admin
4839
    // Refuse access if user does not exist and/or is admin
4840
    $user = DB::queryFirstRow(
4841
        'SELECT admin
4842
        FROM ' . prefixTable('users') . '
4843
        WHERE id = %i',
4844
        $userId
4845
    );
4846
    if (DB::count() === 0 || (int) $user['admin'] === 1) {
4847
        return false;
4848
    }
4849
4850
    // Get item info
4851
    $item = DB::queryFirstRow(
4852
        'SELECT i.perso, i.id_tree
4853
        FROM ' . prefixTable('items') . ' as i
4854
        WHERE i.id = %i',
4855
        $itemId
4856
    );
4857
    if (DB::count() === 0 || (int) $item['perso'] === 0) {
4858
        return false;
4859
    }
4860
4861
    // Get item owner
4862
    $itemOwner = DB::queryFirstRow(
4863
        'SELECT li.id_user
4864
        FROM ' . prefixTable('log_items') . ' as li
4865
        WHERE li.id_item = %i AND li.action = %s',
4866
        $itemId,
4867
        'at_creation'
4868
    );
4869
    if (DB::count() === 0 || (int) $itemOwner['id_user'] !== $userId) {
4870
        return false;
4871
    }
4872
4873
    // Delete all keys for this item except for the owner and TeamPass system user
4874
    DB::delete(
4875
        prefixTable('sharekeys_items'),
4876
        'object_id = %i AND user_id NOT IN %ls',
4877
        $itemId,
4878
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4879
    );
4880
    DB::delete(
4881
        prefixTable('sharekeys_files'),
4882
        'object_id IN (SELECT id FROM '.prefixTable('files').' WHERE id_item = %i) AND user_id NOT IN %ls',
4883
        $itemId,
4884
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4885
    );
4886
    DB::delete(
4887
        prefixTable('sharekeys_fields'),
4888
        'object_id IN (SELECT id FROM '.prefixTable('fields').' WHERE id_item = %i) AND user_id NOT IN %ls',
4889
        $itemId,
4890
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4891
    );
4892
    DB::delete(
4893
        prefixTable('sharekeys_logs'),
4894
        'object_id IN (SELECT id FROM '.prefixTable('log_items').' WHERE id_item = %i) AND user_id NOT IN %ls',
4895
        $itemId,
4896
        [$userId, TP_USER_ID, API_USER_ID, OTV_USER_ID,SSH_USER_ID]
4897
    );
4898
4899
    return true;
4900
}
4901
4902
/**
4903
 * Insert a new record in a table with an "is_current" flag.
4904
 * This function ensures that only one record per user has the "is_current" flag set to true.
4905
 * 
4906
 * @param int $userId The ID of the user.
4907
 * @param string $privateKey The private key to be inserted.
4908
 * @return void
4909
 */
4910
function insertPrivateKeyWithCurrentFlag(int $userId, string $privateKey) {    
4911
    try {
4912
        DB::startTransaction();
4913
        
4914
        // Disable is_current for existing records of the user
4915
        DB::update(
4916
            prefixTable('user_private_keys'),
4917
            array('is_current' => false),
4918
            "user_id = %i AND is_current = %i",
4919
            $userId,
4920
            true
4921
        );
4922
        
4923
        // Insert the new record
4924
        DB::insert(
4925
            prefixTable('user_private_keys'),
4926
            array(
4927
                'user_id' => $userId,
4928
                'private_key' => $privateKey,
4929
                'is_current' => true,
4930
            )
4931
        );
4932
        
4933
        DB::commit();
4934
        
4935
    } catch (Exception $e) {
4936
        DB::rollback();
4937
        throw $e;
4938
    }
4939
}
4940
4941
/**
4942
 * Gets transparent recovery statistics for monitoring.
4943
 *
4944
 * @param array $SETTINGS Teampass settings
4945
 *
4946
 * @return array Statistics data
4947
 */
4948
function getTransparentRecoveryStats(array $SETTINGS): array
4949
{
4950
    // Count auto-recoveries in last 24h
4951
    $autoRecoveriesLast24h = DB::queryFirstField(
4952
        'SELECT COUNT(*) FROM ' . prefixTable('log_system') . '
4953
         WHERE label = %s
4954
         AND date > %i',
4955
        'auto_reencryption_success',
4956
        time() - 86400
4957
    );
4958
4959
    // Count failed recoveries (all time)
4960
    $failedRecoveries = DB::queryFirstField(
4961
        'SELECT COUNT(*) FROM ' . prefixTable('log_system') . '
4962
         WHERE label = %s',
4963
        'auto_reencryption_failed'
4964
    );
4965
4966
    // Count critical failures (all time)
4967
    $criticalFailures = DB::queryFirstField(
4968
        'SELECT COUNT(*) FROM ' . prefixTable('log_system') . '
4969
         WHERE label = %s',
4970
        'auto_reencryption_critical_failure'
4971
    );
4972
4973
    // Count users with transparent recovery enabled (have seed and backup)
4974
    $usersMigrated = DB::queryFirstField(
4975
        'SELECT COUNT(*) FROM ' . prefixTable('users') . '
4976
         WHERE user_derivation_seed IS NOT NULL
4977
         AND private_key_backup IS NOT NULL
4978
         AND disabled = 0'
4979
    );
4980
4981
    // Count total active users
4982
    $totalUsers = DB::queryFirstField(
4983
        'SELECT COUNT(*) FROM ' . prefixTable('users') . '
4984
         WHERE disabled = 0
4985
         AND private_key IS NOT NULL
4986
         AND private_key != "none"'
4987
    );
4988
4989
    // Get recent recovery events (last 10)
4990
    $recentEvents = DB::query(
4991
        'SELECT l.date, l.label, l.qui, u.login
4992
         FROM ' . prefixTable('log_system') . ' AS l
4993
         INNER JOIN ' . prefixTable('users') . ' AS u ON u.id = l.qui
4994
         WHERE l.label IN %ls
4995
         ORDER BY l.date DESC
4996
         LIMIT 10',
4997
        ['auto_reencryption_success', 'auto_reencryption_failed', 'auto_reencryption_critical_failure']
4998
    );
4999
5000
    // Calculate migration percentage
5001
    $migrationPercentage = $totalUsers > 0 ? round(($usersMigrated / $totalUsers) * 100, 2) : 0;
5002
5003
    // Calculate failure rate (last 30 days)
5004
    $totalAttempts30d = DB::queryFirstField(
5005
        'SELECT COUNT(*) FROM ' . prefixTable('log_system') . '
5006
         WHERE label IN %ls
5007
         AND date > %i',
5008
        ['auto_reencryption_success', 'auto_reencryption_failed'],
5009
        time() - (30 * 86400)
5010
    );
5011
5012
    $failures30d = DB::queryFirstField(
5013
        'SELECT COUNT(*) FROM ' . prefixTable('log_system') . '
5014
         WHERE label = %s
5015
         AND date > %i',
5016
        'auto_reencryption_failed',
5017
        time() - (30 * 86400)
5018
    );
5019
5020
    $failureRate = $totalAttempts30d > 0 ? round(($failures30d / $totalAttempts30d) * 100, 2) : 0;
5021
5022
    return [
5023
        'auto_recoveries_last_24h' => (int) $autoRecoveriesLast24h,
5024
        'failed_recoveries_total' => (int) $failedRecoveries,
5025
        'critical_failures_total' => (int) $criticalFailures,
5026
        'users_migrated' => (int) $usersMigrated,
5027
        'total_users' => (int) $totalUsers,
5028
        'migration_percentage' => $migrationPercentage,
5029
        'failure_rate_30d' => $failureRate,
5030
        'recent_events' => $recentEvents,
5031
    ];
5032
}