handleUserKeys()   F
last analyzed

Complexity

Conditions 22
Paths 147

Size

Total Lines 168
Code Lines 91

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 22
eloc 91
nc 147
nop 12
dl 0
loc 168
rs 3.775
c 0
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

Long Method

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

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

Commonly applied refactorings include:

Many Parameters

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

There are several approaches to avoid long parameter lists:

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

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

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