Passed
Push — master ( b727b7...6bf24f )
by Nils
06:30
created

storeUsersShareKey()   B

Complexity

Conditions 9
Paths 32

Size

Total Lines 63
Code Lines 33

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 9
eloc 33
c 3
b 1
f 0
nc 32
nop 9
dl 0
loc 63
rs 8.0555

How to fix   Long Method    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1843
            /** @scrutinizer ignore-type */ $password
Loading history...
1844
        );
1845
    } elseif ($type === 'encrypt') {
1846
        // Encrypt file
1847
        $err = defuseFileEncrypt(
1848
            $source_file,
1849
            $target_file,
1850
            $password
0 ignored issues
show
Bug introduced by
It seems like $password can also be of type Defuse\Crypto\Key; however, parameter $password of defuseFileEncrypt() does only seem to accept null|string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

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