Passed
Push — master ( a8a99f...7da16c )
by Nils
05:59
created

isBase64Encoded()   A

Complexity

Conditions 4
Paths 4

Size

Total Lines 14
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 4
eloc 5
c 1
b 0
f 0
nc 4
nop 1
dl 0
loc 14
rs 10
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This library is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 * ---
12
 *
13
 * @project   Teampass
14
 * @file      main.functions.php
15
 * ---
16
 *
17
 * @author    Nils Laumaillé ([email protected])
18
 *
19
 * @copyright 2009-2023 Teampass.net
20
 *
21
 * @license   https://spdx.org/licenses/GPL-3.0-only.html#licenseText GPL-3.0
22
 * ---
23
 *
24
 * @see       https://www.teampass.net
25
 */
26
27
use LdapRecord\Connection;
28
use ForceUTF8\Encoding;
29
use Elegant\Sanitizer\Sanitizer;
30
use voku\helper\AntiXSS;
31
use Hackzilla\PasswordGenerator\Generator\ComputerPasswordGenerator;
32
use Hackzilla\PasswordGenerator\RandomGenerator\Php7RandomGenerator;
33
use TeampassClasses\SessionManager\SessionManager;
34
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
35
use TeampassClasses\Language\Language;
36
use TeampassClasses\NestedTree\NestedTree;
37
use Defuse\Crypto\Key;
38
use Defuse\Crypto\Crypto;
39
use Defuse\Crypto\KeyProtectedByPassword;
40
use Defuse\Crypto\File as CryptoFile;
41
use Defuse\Crypto\Exception as CryptoException;
42
use Elegant\Sanitizer\Filters\Uppercase;
43
use PHPMailer\PHPMailer\PHPMailer;
44
use PasswordLib\PasswordLib;
45
use Symfony\Component\Process\Exception\ProcessFailedException;
46
use Symfony\Component\Process\Process;
47
use Symfony\Component\Process\PhpExecutableFinder;
48
use TeampassClasses\Encryption\Encryption;
49
50
51
// Load config if $SETTINGS not defined
52
if (isset($SETTINGS['cpassman_dir']) === false || empty($SETTINGS['cpassman_dir']) === true) {
53
    include_once __DIR__ . '/../includes/config/tp.config.php';
54
}
55
56
header('Content-type: text/html; charset=utf-8');
57
header('Cache-Control: no-cache, must-revalidate');
58
59
loadClasses('DB');
60
$session = SessionManager::getSession();
61
62
63
/**
64
 * genHash().
65
 *
66
 * Generate a hash for user login
67
 *
68
 * @param string $password What password
69
 * @param string $cost     What cost
70
 *
71
 * @return string|void
72
 */
73
function bCrypt(
74
    string $password,
75
    string $cost
76
): ?string
77
{
78
    $salt = sprintf('$2y$%02d$', $cost);
79
    if (function_exists('openssl_random_pseudo_bytes')) {
80
        $salt .= bin2hex(openssl_random_pseudo_bytes(11));
81
    } else {
82
        $chars = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
83
        for ($i = 0; $i < 22; ++$i) {
84
            $salt .= $chars[mt_rand(0, 63)];
85
        }
86
    }
87
88
    return crypt($password, $salt);
89
}
90
91
/**
92
 * Checks if a string is hex encoded
93
 *
94
 * @param string $str
95
 * @return boolean
96
 */
97
function isHex(string $str): bool
98
{
99
    if (str_starts_with(strtolower($str), '0x')) {
100
        $str = substr($str, 2);
101
    }
102
103
    return ctype_xdigit($str);
104
}
105
106
/**
107
 * Defuse cryption function.
108
 *
109
 * @param string $message   what to de/crypt
110
 * @param string $ascii_key key to use
111
 * @param string $type      operation to perform
112
 * @param array  $SETTINGS  Teampass settings
113
 *
114
 * @return array
115
 */
116
function cryption(string $message, string $ascii_key, string $type, ?array $SETTINGS = []): array
117
{
118
    $ascii_key = empty($ascii_key) === true ? file_get_contents(SECUREPATH.'/'.SECUREFILE) : $ascii_key;
119
    $err = false;
120
    
121
    // convert KEY
122
    $key = Key::loadFromAsciiSafeString($ascii_key);
123
    try {
124
        if ($type === 'encrypt') {
125
            $text = Crypto::encrypt($message, $key);
126
        } elseif ($type === 'decrypt') {
127
            $text = Crypto::decrypt($message, $key);
128
        }
129
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
130
        $err = 'an attack! either the wrong key was loaded, or the ciphertext has changed since it was created either corrupted in the database or intentionally modified by someone trying to carry out an attack.';
131
    } catch (CryptoException\BadFormatException $ex) {
132
        $err = $ex;
133
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
134
        $err = $ex;
135
    } catch (CryptoException\CryptoException $ex) {
136
        $err = $ex;
137
    } catch (CryptoException\IOException $ex) {
138
        $err = $ex;
139
    }
140
141
    return [
142
        'string' => $text ?? '',
143
        'error' => $err,
144
    ];
145
}
146
147
/**
148
 * Generating a defuse key.
149
 *
150
 * @return string
151
 */
152
function defuse_generate_key()
153
{
154
    $key = Key::createNewRandomKey();
155
    $key = $key->saveToAsciiSafeString();
156
    return $key;
157
}
158
159
/**
160
 * Generate a Defuse personal key.
161
 *
162
 * @param string $psk psk used
163
 *
164
 * @return string
165
 */
166
function defuse_generate_personal_key(string $psk): string
167
{
168
    $protected_key = KeyProtectedByPassword::createRandomPasswordProtectedKey($psk);
169
    return $protected_key->saveToAsciiSafeString(); // save this in user table
170
}
171
172
/**
173
 * Validate persoanl key with defuse.
174
 *
175
 * @param string $psk                   the user's psk
176
 * @param string $protected_key_encoded special key
177
 *
178
 * @return string
179
 */
180
function defuse_validate_personal_key(string $psk, string $protected_key_encoded): string
181
{
182
    try {
183
        $protected_key_encoded = KeyProtectedByPassword::loadFromAsciiSafeString($protected_key_encoded);
184
        $user_key = $protected_key_encoded->unlockKey($psk);
185
        $user_key_encoded = $user_key->saveToAsciiSafeString();
186
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
187
        return 'Error - Major issue as the encryption is broken.';
188
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
189
        return 'Error - The saltkey is not the correct one.';
190
    }
191
192
    return $user_key_encoded;
193
    // store it in session once user has entered his psk
194
}
195
196
/**
197
 * Decrypt a defuse string if encrypted.
198
 *
199
 * @param string $value Encrypted string
200
 *
201
 * @return string Decrypted string
202
 */
203
function defuseReturnDecrypted(string $value, $SETTINGS): string
204
{
205
    if (substr($value, 0, 3) === 'def') {
206
        $value = cryption($value, '', 'decrypt', $SETTINGS)['string'];
207
    }
208
209
    return $value;
210
}
211
212
/**
213
 * Trims a string depending on a specific string.
214
 *
215
 * @param string|array $chaine  what to trim
216
 * @param string       $element trim on what
217
 *
218
 * @return string
219
 */
220
function trimElement($chaine, string $element): string
221
{
222
    if (! empty($chaine)) {
223
        if (is_array($chaine) === true) {
224
            $chaine = implode(';', $chaine);
225
        }
226
        $chaine = trim($chaine);
227
        if (substr($chaine, 0, 1) === $element) {
228
            $chaine = substr($chaine, 1);
229
        }
230
        if (substr($chaine, strlen($chaine) - 1, 1) === $element) {
231
            $chaine = substr($chaine, 0, strlen($chaine) - 1);
232
        }
233
    }
234
235
    return $chaine;
236
}
237
238
/**
239
 * Permits to suppress all "special" characters from string.
240
 *
241
 * @param string $string  what to clean
242
 * @param bool   $special use of special chars?
243
 *
244
 * @return string
245
 */
246
function cleanString(string $string, bool $special = false): string
247
{
248
    // Create temporary table for special characters escape
249
    $tabSpecialChar = [];
250
    for ($i = 0; $i <= 31; ++$i) {
251
        $tabSpecialChar[] = chr($i);
252
    }
253
    array_push($tabSpecialChar, '<br />');
254
    if ((int) $special === 1) {
255
        $tabSpecialChar = array_merge($tabSpecialChar, ['</li>', '<ul>', '<ol>']);
256
    }
257
258
    return str_replace($tabSpecialChar, "\n", $string);
259
}
260
261
/**
262
 * Erro manager for DB.
263
 *
264
 * @param array $params output from query
265
 *
266
 * @return void
267
 */
268
function db_error_handler(array $params): void
269
{
270
    echo 'Error: ' . $params['error'] . "<br>\n";
271
    echo 'Query: ' . $params['query'] . "<br>\n";
272
    throw new Exception('Error - Query', 1);
273
}
274
275
/**
276
 * Identify user's rights
277
 *
278
 * @param string|array $groupesVisiblesUser  [description]
279
 * @param string|array $groupesInterditsUser [description]
280
 * @param string       $isAdmin              [description]
281
 * @param string       $idFonctions          [description]
282
 *
283
 * @return bool
284
 */
285
function identifyUserRights(
286
    $groupesVisiblesUser,
287
    $groupesInterditsUser,
288
    $isAdmin,
289
    $idFonctions,
290
    $SETTINGS
291
) {
292
    $session = SessionManager::getSession();
293
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
294
295
    // Check if user is ADMINISTRATOR    
296
    (int) $isAdmin === 1 ?
297
        identAdmin(
298
            $idFonctions,
299
            $SETTINGS, /** @scrutinizer ignore-type */
300
            $tree
301
        )
302
        :
303
        identUser(
304
            $groupesVisiblesUser,
305
            $groupesInterditsUser,
306
            $idFonctions,
307
            $SETTINGS, /** @scrutinizer ignore-type */
308
            $tree
309
        );
310
311
    // update user's timestamp
312
    DB::update(
313
        prefixTable('users'),
314
        [
315
            'timestamp' => time(),
316
        ],
317
        'id=%i',
318
        $session->get('user-id')
319
    );
320
321
    return true;
322
}
323
324
/**
325
 * Identify administrator.
326
 *
327
 * @param string $idFonctions Roles of user
328
 * @param array  $SETTINGS    Teampass settings
329
 * @param object $tree        Tree of folders
330
 *
331
 * @return bool
332
 */
333
function identAdmin($idFonctions, $SETTINGS, $tree)
334
{
335
    
336
    $session = SessionManager::getSession();
337
    $groupesVisibles = [];
338
    $session->set('user-personal_folders', []);
339
    $session->set('user-accessible_folders', []);
340
    $session->set('user-no_access_folders', []);
341
    $session->set('user-personal_visible_folders', []);
342
    $session->set('user-read_only_folders', []);
343
    $session->set('system-list_restricted_folders_for_items', []);
344
    $session->set('system-list_folders_editable_by_role', []);
345
    $session->set('user-list_folders_limited', []);
346
    $session->set('user-forbiden_personal_folders', []);
347
    $globalsUserId = $session->get('user-id');
348
    $globalsVisibleFolders = $session->get('user-accessible_folders');
349
    $globalsPersonalVisibleFolders = $session->get('user-personal_visible_folders');
350
    // Get list of Folders
351
    $rows = DB::query('SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i', 0);
352
    foreach ($rows as $record) {
353
        array_push($groupesVisibles, $record['id']);
354
    }
355
    $session->set('user-accessible_folders', $groupesVisibles);
356
    $session->set('user-all_non_personal_folders', $groupesVisibles);
357
    // Exclude all PF
358
    $where = new WhereClause('and');
359
    // create a WHERE statement of pieces joined by ANDs
360
    $where->add('personal_folder=%i', 1);
361
    if (
362
        isset($SETTINGS['enable_pf_feature']) === true
363
        && (int) $SETTINGS['enable_pf_feature'] === 1
364
    ) {
365
        $where->add('title=%s', $globalsUserId);
366
        $where->negateLast();
367
    }
368
    // Get ID of personal folder
369
    $persfld = DB::queryfirstrow(
370
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE title = %s',
371
        $globalsUserId
372
    );
373
    if (empty($persfld['id']) === false) {
374
        if (in_array($persfld['id'], $globalsVisibleFolders) === false) {
375
            array_push($globalsVisibleFolders, $persfld['id']);
376
            array_push($globalsPersonalVisibleFolders, $persfld['id']);
377
            // get all descendants
378
            $tree->rebuild();
379
            $tst = $tree->getDescendants($persfld['id']);
380
            foreach ($tst as $t) {
381
                array_push($globalsVisibleFolders, $t->id);
382
                array_push($globalsPersonalVisibleFolders, $t->id);
383
            }
384
        }
385
    }
386
387
    // get complete list of ROLES
388
    $tmp = explode(';', $idFonctions);
389
    $rows = DB::query(
390
        'SELECT * FROM ' . prefixTable('roles_title') . '
391
        ORDER BY title ASC'
392
    );
393
    foreach ($rows as $record) {
394
        if (! empty($record['id']) && ! in_array($record['id'], $tmp)) {
395
            array_push($tmp, $record['id']);
396
        }
397
    }
398
    $session->set('user-roles', implode(';', $tmp));
399
    $session->set('user-admin', 1);
400
    // Check if admin has created Folders and Roles
401
    DB::query('SELECT * FROM ' . prefixTable('nested_tree') . '');
402
    $session->set('user-nb_folders', DB::count());
403
    DB::query('SELECT * FROM ' . prefixTable('roles_title'));
404
    $session->set('user-nb_roles', DB::count());
405
406
    return true;
407
}
408
409
/**
410
 * Permits to convert an element to array.
411
 *
412
 * @param string|array $element Any value to be returned as array
413
 *
414
 * @return array
415
 */
416
function convertToArray($element): array
417
{
418
    if (is_string($element) === true) {
419
        if (empty($element) === true) {
420
            return [];
421
        }
422
        return explode(
423
            ';',
424
            trimElement($element, ';')
425
        );
426
    }
427
    return $element;
428
}
429
430
/**
431
 * Defines the rights the user has.
432
 *
433
 * @param string|array $allowedFolders  Allowed folders
434
 * @param string|array $noAccessFolders Not allowed folders
435
 * @param string|array $userRoles       Roles of user
436
 * @param array        $SETTINGS        Teampass settings
437
 * @param object       $tree            Tree of folders
438
 * 
439
 * @return bool
440
 */
441
function identUser(
442
    $allowedFolders,
443
    $noAccessFolders,
444
    $userRoles,
445
    array $SETTINGS,
446
    object $tree
447
) {
448
    
449
    $session = SessionManager::getSession();
450
    // Init
451
    $session->set('user-accessible_folders', []);
452
    $session->set('user-personal_folders', []);
453
    $session->set('user-no_access_folders', []);
454
    $session->set('user-personal_visible_folders', []);
455
    $session->set('user-read_only_folders', []);
456
    $session->set('user-user-roles', $userRoles);
457
    $session->set('user-admin', 0);
458
    // init
459
    $personalFolders = [];
460
    $readOnlyFolders = [];
461
    $noAccessPersonalFolders = [];
462
    $restrictedFoldersForItems = [];
463
    $foldersLimited = [];
464
    $foldersLimitedFull = [];
465
    $allowedFoldersByRoles = [];
466
    $globalsUserId = $session->get('user-id');
467
    $globalsPersonalFolders = $session->get('user-personal_folder_enabled');
468
    // Ensure consistency in array format
469
    $noAccessFolders = convertToArray($noAccessFolders);
470
    $userRoles = convertToArray($userRoles);
471
    $allowedFolders = convertToArray($allowedFolders);
472
    
473
    // Get list of folders depending on Roles
474
    $arrays = identUserGetFoldersFromRoles(
475
        $userRoles,
476
        $allowedFoldersByRoles,
477
        $readOnlyFolders,
478
        $allowedFolders
479
    );
480
    $allowedFoldersByRoles = $arrays['allowedFoldersByRoles'];
481
    $readOnlyFolders = $arrays['readOnlyFolders'];
482
483
    // Does this user is allowed to see other items
484
    $inc = 0;
485
    $rows = DB::query(
486
        'SELECT id, id_tree FROM ' . prefixTable('items') . '
487
            WHERE restricted_to LIKE %ss AND inactif = %s'.
488
            (count($allowedFolders) > 0 ? ' AND id_tree NOT IN ('.implode(',', $allowedFolders).')' : ''),
489
        $globalsUserId,
490
        '0'
491
    );
492
    foreach ($rows as $record) {
493
        // Exclude restriction on item if folder is fully accessible
494
        //if (in_array($record['id_tree'], $allowedFolders) === false) {
495
            $restrictedFoldersForItems[$record['id_tree']][$inc] = $record['id'];
496
            ++$inc;
497
        //}
498
    }
499
500
    // Check for the users roles if some specific rights exist on items
501
    $rows = DB::query(
502
        'SELECT i.id_tree, r.item_id
503
        FROM ' . prefixTable('items') . ' as i
504
        INNER JOIN ' . prefixTable('restriction_to_roles') . ' as r ON (r.item_id=i.id)
505
        WHERE i.id_tree <> "" '.
506
        (count($userRoles) > 0 ? 'AND r.role_id IN %li ' : '').
507
        'ORDER BY i.id_tree ASC',
508
        $userRoles
509
    );
510
    $inc = 0;
511
    foreach ($rows as $record) {
512
        //if (isset($record['id_tree'])) {
513
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
514
            array_push($foldersLimitedFull, $record['id_tree']);
515
            ++$inc;
516
        //}
517
    }
518
519
    // Get list of Personal Folders
520
    $arrays = identUserGetPFList(
521
        $globalsPersonalFolders,
522
        $allowedFolders,
523
        $globalsUserId,
524
        $personalFolders,
525
        $noAccessPersonalFolders,
526
        $foldersLimitedFull,
527
        $allowedFoldersByRoles,
528
        array_keys($restrictedFoldersForItems),
529
        $readOnlyFolders,
530
        $noAccessFolders,
531
        isset($SETTINGS['enable_pf_feature']) === true ? $SETTINGS['enable_pf_feature'] : 0,
532
        $tree
533
    );
534
    $allowedFolders = $arrays['allowedFolders'];
535
    $personalFolders = $arrays['personalFolders'];
536
    $noAccessPersonalFolders = $arrays['noAccessPersonalFolders'];
537
538
    // Return data
539
    $session->set('user-all_non_personal_folders', $allowedFolders);
540
    $session->set('user-accessible_folders', array_unique(array_merge($allowedFolders, $personalFolders), SORT_NUMERIC));
541
    $session->set('user-read_only_folders', $readOnlyFolders);
542
    $session->set('user-no_access_folders', $noAccessFolders);
543
    $session->set('user-personal_folders', $personalFolders);
544
    $session->set('user-list_folders_limited', $foldersLimited);
545
    $session->set('system-list_folders_editable_by_role', $allowedFoldersByRoles, 'SESSION');
546
    $session->set('system-list_restricted_folders_for_items', $restrictedFoldersForItems);
547
    $session->set('user-forbiden_personal_folders', $noAccessPersonalFolders);
548
    $session->set(
549
        'all_folders_including_no_access',
550
        array_unique(array_merge(
551
            $allowedFolders,
552
            $personalFolders,
553
            $noAccessFolders,
554
            $readOnlyFolders
555
        ), SORT_NUMERIC)
556
    );
557
    // Folders and Roles numbers
558
    DB::queryfirstrow('SELECT id FROM ' . prefixTable('nested_tree') . '');
559
    $session->set('user-nb_folders', DB::count());
560
    DB::queryfirstrow('SELECT id FROM ' . prefixTable('roles_title'));
561
    $session->set('user-nb_roles', DB::count());
562
    // check if change proposals on User's items
563
    if (isset($SETTINGS['enable_suggestion']) === true && (int) $SETTINGS['enable_suggestion'] === 1) {
564
        $countNewItems = DB::query(
565
            'SELECT COUNT(*)
566
            FROM ' . prefixTable('items_change') . ' AS c
567
            LEFT JOIN ' . prefixTable('log_items') . ' AS i ON (c.item_id = i.id_item)
568
            WHERE i.action = %s AND i.id_user = %i',
569
            'at_creation',
570
            $globalsUserId
571
        );
572
        $session->set('user-nb_item_change_proposals', $countNewItems);
573
    } else {
574
        $session->set('user-nb_item_change_proposals', 0);
575
    }
576
577
    return true;
578
}
579
580
/**
581
 * Get list of folders depending on Roles
582
 * 
583
 * @param array $userRoles
584
 * @param array $allowedFoldersByRoles
585
 * @param array $readOnlyFolders
586
 * @param array $allowedFolders
587
 * 
588
 * @return array
589
 */
590
function identUserGetFoldersFromRoles($userRoles, $allowedFoldersByRoles, $readOnlyFolders, $allowedFolders) : array
591
{
592
    $rows = DB::query(
593
        'SELECT *
594
        FROM ' . prefixTable('roles_values') . '
595
        WHERE type IN %ls'.(count($userRoles) > 0 ? ' AND role_id IN %li' : ''),
596
        ['W', 'ND', 'NE', 'NDNE', 'R'],
597
        $userRoles,
598
    );
599
    foreach ($rows as $record) {
600
        if ($record['type'] === 'R') {
601
            array_push($readOnlyFolders, $record['folder_id']);
602
        } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
603
            array_push($allowedFoldersByRoles, $record['folder_id']);
604
        }
605
    }
606
    $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
607
    $readOnlyFolders = array_unique($readOnlyFolders);
608
    
609
    // Clean arrays
610
    foreach ($allowedFoldersByRoles as $value) {
611
        $key = array_search($value, $readOnlyFolders);
612
        if ($key !== false) {
613
            unset($readOnlyFolders[$key]);
614
        }
615
    }
616
    return [
617
        'readOnlyFolders' => $readOnlyFolders,
618
        'allowedFoldersByRoles' => $allowedFoldersByRoles
619
    ];
620
}
621
622
/**
623
 * Get list of Personal Folders
624
 * 
625
 * @param int $globalsPersonalFolders
626
 * @param array $allowedFolders
627
 * @param int $globalsUserId
628
 * @param array $personalFolders
629
 * @param array $noAccessPersonalFolders
630
 * @param array $foldersLimitedFull
631
 * @param array $allowedFoldersByRoles
632
 * @param array $restrictedFoldersForItems
633
 * @param array $readOnlyFolders
634
 * @param array $noAccessFolders
635
 * @param int $enablePfFeature
636
 * @param object $tree
637
 * 
638
 * @return array
639
 */
640
function identUserGetPFList(
641
    $globalsPersonalFolders,
642
    $allowedFolders,
643
    $globalsUserId,
644
    $personalFolders,
645
    $noAccessPersonalFolders,
646
    $foldersLimitedFull,
647
    $allowedFoldersByRoles,
648
    $restrictedFoldersForItems,
649
    $readOnlyFolders,
650
    $noAccessFolders,
651
    $enablePfFeature,
652
    $tree
653
)
654
{
655
    if (
656
        (int) $enablePfFeature === 1
657
        && (int) $globalsPersonalFolders === 1
658
    ) {
659
        $persoFld = DB::queryfirstrow(
660
            'SELECT id
661
            FROM ' . prefixTable('nested_tree') . '
662
            WHERE title = %s AND personal_folder = %i'.
663
            (count($allowedFolders) > 0 ? ' AND id NOT IN ('.implode(',', $allowedFolders).')' : ''),
664
            $globalsUserId,
665
            1
666
        );
667
        if (empty($persoFld['id']) === false) {
668
            array_push($personalFolders, $persoFld['id']);
669
            array_push($allowedFolders, $persoFld['id']);
670
            // get all descendants
671
            $ids = $tree->getDescendants($persoFld['id'], false, false, true);
672
            foreach ($ids as $id) {
673
                //array_push($allowedFolders, $id);
674
                array_push($personalFolders, $id);
675
            }
676
        }
677
    }
678
    
679
    // Exclude all other PF
680
    $where = new WhereClause('and');
681
    $where->add('personal_folder=%i', 1);
682
    if (count($personalFolders) > 0) {
683
        $where->add('id NOT IN ('.implode(',', $personalFolders).')');
684
    }
685
    if (
686
        (int) $enablePfFeature === 1
687
        && (int) $globalsPersonalFolders === 1
688
    ) {
689
        $where->add('title=%s', $globalsUserId);
690
        $where->negateLast();
691
    }
692
    $persoFlds = DB::query(
693
        'SELECT id
694
        FROM ' . prefixTable('nested_tree') . '
695
        WHERE %l',
696
        $where
697
    );
698
    foreach ($persoFlds as $persoFldId) {
699
        array_push($noAccessPersonalFolders, $persoFldId['id']);
700
    }
701
702
    // All folders visibles
703
    $allowedFolders = array_unique(array_merge(
704
        $allowedFolders,
705
        $foldersLimitedFull,
706
        $allowedFoldersByRoles,
707
        $restrictedFoldersForItems,
708
        $readOnlyFolders
709
    ), SORT_NUMERIC);
710
    // Exclude from allowed folders all the specific user forbidden folders
711
    if (count($noAccessFolders) > 0) {
712
        $allowedFolders = array_diff($allowedFolders, $noAccessFolders);
713
    }
714
715
    return [
716
        'allowedFolders' => array_diff(array_diff($allowedFolders, $noAccessPersonalFolders), $personalFolders),
717
        'personalFolders' => $personalFolders,
718
        'noAccessPersonalFolders' => $noAccessPersonalFolders
719
    ];
720
}
721
722
723
/**
724
 * Update the CACHE table.
725
 *
726
 * @param string $action   What to do
727
 * @param array  $SETTINGS Teampass settings
728
 * @param int    $ident    Ident format
729
 * 
730
 * @return void
731
 */
732
function updateCacheTable(string $action, ?int $ident = null): void
733
{
734
    if ($action === 'reload') {
735
        // Rebuild full cache table
736
        cacheTableRefresh();
737
    } elseif ($action === 'update_value' && is_null($ident) === false) {
738
        // UPDATE an item
739
        cacheTableUpdate($ident);
740
    } elseif ($action === 'add_value' && is_null($ident) === false) {
741
        // ADD an item
742
        cacheTableAdd($ident);
743
    } elseif ($action === 'delete_value' && is_null($ident) === false) {
744
        // DELETE an item
745
        DB::delete(prefixTable('cache'), 'id = %i', $ident);
746
    }
747
}
748
749
/**
750
 * Cache table - refresh.
751
 *
752
 * @return void
753
 */
754
function cacheTableRefresh(): void
755
{
756
    // Load class DB
757
    loadClasses('DB');
758
759
    //Load Tree
760
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
761
    // truncate table
762
    DB::query('TRUNCATE TABLE ' . prefixTable('cache'));
763
    // reload date
764
    $rows = DB::query(
765
        'SELECT *
766
        FROM ' . prefixTable('items') . ' as i
767
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
768
        AND l.action = %s
769
        AND i.inactif = %i',
770
        'at_creation',
771
        0
772
    );
773
    foreach ($rows as $record) {
774
        if (empty($record['id_tree']) === false) {
775
            // Get all TAGS
776
            $tags = '';
777
            $itemTags = DB::query(
778
                'SELECT tag
779
                FROM ' . prefixTable('tags') . '
780
                WHERE item_id = %i AND tag != ""',
781
                $record['id']
782
            );
783
            foreach ($itemTags as $itemTag) {
784
                $tags .= $itemTag['tag'] . ' ';
785
            }
786
787
            // Get renewal period
788
            $resNT = DB::queryfirstrow(
789
                'SELECT renewal_period
790
                FROM ' . prefixTable('nested_tree') . '
791
                WHERE id = %i',
792
                $record['id_tree']
793
            );
794
            // form id_tree to full foldername
795
            $folder = [];
796
            $arbo = $tree->getPath($record['id_tree'], true);
797
            foreach ($arbo as $elem) {
798
                // Check if title is the ID of a user
799
                if (is_numeric($elem->title) === true) {
800
                    // Is this a User id?
801
                    $user = DB::queryfirstrow(
802
                        'SELECT id, login
803
                        FROM ' . prefixTable('users') . '
804
                        WHERE id = %i',
805
                        $elem->title
806
                    );
807
                    if (count($user) > 0) {
808
                        $elem->title = $user['login'];
809
                    }
810
                }
811
                // Build path
812
                array_push($folder, stripslashes($elem->title));
813
            }
814
            // store data
815
            DB::insert(
816
                prefixTable('cache'),
817
                [
818
                    'id' => $record['id'],
819
                    'label' => $record['label'],
820
                    'description' => $record['description'] ?? '',
821
                    'url' => isset($record['url']) && ! empty($record['url']) ? $record['url'] : '0',
822
                    'tags' => $tags,
823
                    'id_tree' => $record['id_tree'],
824
                    'perso' => $record['perso'],
825
                    'restricted_to' => isset($record['restricted_to']) && ! empty($record['restricted_to']) ? $record['restricted_to'] : '0',
826
                    'login' => $record['login'] ?? '',
827
                    'folder' => implode(' > ', $folder),
828
                    'author' => $record['id_user'],
829
                    'renewal_period' => $resNT['renewal_period'] ?? '0',
830
                    'timestamp' => $record['date'],
831
                ]
832
            );
833
        }
834
    }
835
}
836
837
/**
838
 * Cache table - update existing value.
839
 *
840
 * @param int    $ident    Ident format
841
 * 
842
 * @return void
843
 */
844
function cacheTableUpdate(?int $ident = null): void
845
{
846
    $session = SessionManager::getSession();
847
    loadClasses('DB');
848
849
    //Load Tree
850
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
851
    // get new value from db
852
    $data = DB::queryfirstrow(
853
        'SELECT label, description, id_tree, perso, restricted_to, login, url
854
        FROM ' . prefixTable('items') . '
855
        WHERE id=%i',
856
        $ident
857
    );
858
    // Get all TAGS
859
    $tags = '';
860
    $itemTags = DB::query(
861
        'SELECT tag
862
            FROM ' . prefixTable('tags') . '
863
            WHERE item_id = %i AND tag != ""',
864
        $ident
865
    );
866
    foreach ($itemTags as $itemTag) {
867
        $tags .= $itemTag['tag'] . ' ';
868
    }
869
    // form id_tree to full foldername
870
    $folder = [];
871
    $arbo = $tree->getPath($data['id_tree'], true);
872
    foreach ($arbo as $elem) {
873
        // Check if title is the ID of a user
874
        if (is_numeric($elem->title) === true) {
875
            // Is this a User id?
876
            $user = DB::queryfirstrow(
877
                'SELECT id, login
878
                FROM ' . prefixTable('users') . '
879
                WHERE id = %i',
880
                $elem->title
881
            );
882
            if (count($user) > 0) {
883
                $elem->title = $user['login'];
884
            }
885
        }
886
        // Build path
887
        array_push($folder, stripslashes($elem->title));
888
    }
889
    // finaly update
890
    DB::update(
891
        prefixTable('cache'),
892
        [
893
            'label' => $data['label'],
894
            'description' => $data['description'],
895
            'tags' => $tags,
896
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
897
            'id_tree' => $data['id_tree'],
898
            'perso' => $data['perso'],
899
            'restricted_to' => isset($data['restricted_to']) && ! empty($data['restricted_to']) ? $data['restricted_to'] : '0',
900
            'login' => $data['login'] ?? '',
901
            'folder' => implode(' » ', $folder),
902
            'author' => $session->get('user-id'),
903
        ],
904
        'id = %i',
905
        $ident
906
    );
907
}
908
909
/**
910
 * Cache table - add new value.
911
 *
912
 * @param int    $ident    Ident format
913
 * 
914
 * @return void
915
 */
916
function cacheTableAdd(?int $ident = null): void
917
{
918
    $session = SessionManager::getSession();
919
    $globalsUserId = $session->get('user-id');
920
921
    // Load class DB
922
    loadClasses('DB');
923
924
    //Load Tree
925
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
926
    // get new value from db
927
    $data = DB::queryFirstRow(
928
        'SELECT i.label, i.description, i.id_tree as id_tree, i.perso, i.restricted_to, i.id, i.login, i.url, l.date
929
        FROM ' . prefixTable('items') . ' as i
930
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
931
        WHERE i.id = %i
932
        AND l.action = %s',
933
        $ident,
934
        'at_creation'
935
    );
936
    // Get all TAGS
937
    $tags = '';
938
    $itemTags = DB::query(
939
        'SELECT tag
940
            FROM ' . prefixTable('tags') . '
941
            WHERE item_id = %i AND tag != ""',
942
        $ident
943
    );
944
    foreach ($itemTags as $itemTag) {
945
        $tags .= $itemTag['tag'] . ' ';
946
    }
947
    // form id_tree to full foldername
948
    $folder = [];
949
    $arbo = $tree->getPath($data['id_tree'], true);
950
    foreach ($arbo as $elem) {
951
        // Check if title is the ID of a user
952
        if (is_numeric($elem->title) === true) {
953
            // Is this a User id?
954
            $user = DB::queryfirstrow(
955
                'SELECT id, login
956
                FROM ' . prefixTable('users') . '
957
                WHERE id = %i',
958
                $elem->title
959
            );
960
            if (count($user) > 0) {
961
                $elem->title = $user['login'];
962
            }
963
        }
964
        // Build path
965
        array_push($folder, stripslashes($elem->title));
966
    }
967
    // finaly update
968
    DB::insert(
969
        prefixTable('cache'),
970
        [
971
            'id' => $data['id'],
972
            'label' => $data['label'],
973
            'description' => $data['description'],
974
            'tags' => isset($tags) && empty($tags) === false ? $tags : 'None',
975
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
976
            'id_tree' => $data['id_tree'],
977
            'perso' => isset($data['perso']) && empty($data['perso']) === false && $data['perso'] !== 'None' ? $data['perso'] : '0',
978
            'restricted_to' => isset($data['restricted_to']) && empty($data['restricted_to']) === false ? $data['restricted_to'] : '0',
979
            'login' => $data['login'] ?? '',
980
            'folder' => implode(' » ', $folder),
981
            'author' => $globalsUserId,
982
            'timestamp' => $data['date'],
983
        ]
984
    );
985
}
986
987
/**
988
 * Do statistics.
989
 *
990
 * @param array $SETTINGS Teampass settings
991
 *
992
 * @return array
993
 */
994
function getStatisticsData(array $SETTINGS): array
995
{
996
    DB::query(
997
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
998
        0
999
    );
1000
    $counter_folders = DB::count();
1001
    DB::query(
1002
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
1003
        1
1004
    );
1005
    $counter_folders_perso = DB::count();
1006
    DB::query(
1007
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
1008
        0
1009
    );
1010
    $counter_items = DB::count();
1011
        DB::query(
1012
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
1013
        1
1014
    );
1015
    $counter_items_perso = DB::count();
1016
        DB::query(
1017
        'SELECT id FROM ' . prefixTable('users') . ''
1018
    );
1019
    $counter_users = DB::count();
1020
        DB::query(
1021
        'SELECT id FROM ' . prefixTable('users') . ' WHERE admin = %i',
1022
        1
1023
    );
1024
    $admins = DB::count();
1025
    DB::query(
1026
        'SELECT id FROM ' . prefixTable('users') . ' WHERE gestionnaire = %i',
1027
        1
1028
    );
1029
    $managers = DB::count();
1030
    DB::query(
1031
        'SELECT id FROM ' . prefixTable('users') . ' WHERE read_only = %i',
1032
        1
1033
    );
1034
    $readOnly = DB::count();
1035
    // list the languages
1036
    $usedLang = [];
1037
    $tp_languages = DB::query(
1038
        'SELECT name FROM ' . prefixTable('languages')
1039
    );
1040
    foreach ($tp_languages as $tp_language) {
1041
        DB::query(
1042
            'SELECT * FROM ' . prefixTable('users') . ' WHERE user_language = %s',
1043
            $tp_language['name']
1044
        );
1045
        $usedLang[$tp_language['name']] = round((DB::count() * 100 / $counter_users), 0);
1046
    }
1047
1048
    // get list of ips
1049
    $usedIp = [];
1050
    $tp_ips = DB::query(
1051
        'SELECT user_ip FROM ' . prefixTable('users')
1052
    );
1053
    foreach ($tp_ips as $ip) {
1054
        if (array_key_exists($ip['user_ip'], $usedIp)) {
1055
            $usedIp[$ip['user_ip']] += $usedIp[$ip['user_ip']];
1056
        } elseif (! empty($ip['user_ip']) && $ip['user_ip'] !== 'none') {
1057
            $usedIp[$ip['user_ip']] = 1;
1058
        }
1059
    }
1060
1061
    return [
1062
        'error' => '',
1063
        'stat_phpversion' => phpversion(),
1064
        'stat_folders' => $counter_folders,
1065
        'stat_folders_shared' => intval($counter_folders) - intval($counter_folders_perso),
1066
        'stat_items' => $counter_items,
1067
        'stat_items_shared' => intval($counter_items) - intval($counter_items_perso),
1068
        'stat_users' => $counter_users,
1069
        'stat_admins' => $admins,
1070
        'stat_managers' => $managers,
1071
        'stat_ro' => $readOnly,
1072
        'stat_kb' => $SETTINGS['enable_kb'],
1073
        'stat_pf' => $SETTINGS['enable_pf_feature'],
1074
        'stat_fav' => $SETTINGS['enable_favourites'],
1075
        'stat_teampassversion' => TP_VERSION,
1076
        'stat_ldap' => $SETTINGS['ldap_mode'],
1077
        'stat_agses' => $SETTINGS['agses_authentication_enabled'],
1078
        'stat_duo' => $SETTINGS['duo'],
1079
        'stat_suggestion' => $SETTINGS['enable_suggestion'],
1080
        'stat_api' => $SETTINGS['api'],
1081
        'stat_customfields' => $SETTINGS['item_extra_fields'],
1082
        'stat_syslog' => $SETTINGS['syslog_enable'],
1083
        'stat_2fa' => $SETTINGS['google_authentication'],
1084
        'stat_stricthttps' => $SETTINGS['enable_sts'],
1085
        'stat_mysqlversion' => DB::serverVersion(),
1086
        'stat_languages' => $usedLang,
1087
        'stat_country' => $usedIp,
1088
    ];
1089
}
1090
1091
/**
1092
 * Permits to prepare the way to send the email
1093
 * 
1094
 * @param string $subject       email subject
1095
 * @param string $body          email message
1096
 * @param string $email         email
1097
 * @param string $receiverName  Receiver name
1098
 * @param array  $SETTINGS      settings
1099
 *
1100
 * @return void
1101
 */
1102
function prepareSendingEmail(
1103
    $subject,
1104
    $body,
1105
    $email,
1106
    $receiverName = ''
1107
): void 
1108
{
1109
    DB::insert(
1110
        prefixTable('processes'),
1111
        array(
1112
            'created_at' => time(),
1113
            'process_type' => 'send_email',
1114
            'arguments' => json_encode([
1115
                'subject' => $subject,
1116
                'receivers' => $email,
1117
                'body' => $body,
1118
                'receiver_name' => $receiverName,
1119
            ], JSON_HEX_QUOT | JSON_HEX_TAG),
1120
            'updated_at' => '',
1121
            'finished_at' => '',
1122
            'output' => '',
1123
        )
1124
    );
1125
}
1126
1127
/**
1128
 * Permits to send an email.
1129
 *
1130
 * @param string $subject     email subject
1131
 * @param string $textMail    email message
1132
 * @param string $email       email
1133
 * @param array  $SETTINGS    settings
1134
 * @param string $textMailAlt email message alt
1135
 * @param bool   $silent      no errors
1136
 *
1137
 * @return string some json info
1138
 */
1139
function sendEmail(
1140
    $subject,
1141
    $textMail,
1142
    $email,
1143
    $SETTINGS,
1144
    $textMailAlt = null,
1145
    $silent = true,
1146
    $cron = false
1147
) {
1148
    $lang = new Language(); 
1149
1150
    // CAse where email not defined
1151
    if ($email === 'none' || empty($email) === true) {
1152
        return json_encode(
1153
            [
1154
                'error' => true,
1155
                'message' => $lang->get('forgot_my_pw_email_sent'),
1156
            ]
1157
        );
1158
    }
1159
1160
    // Build and send email
1161
    $email = buildEmail(
1162
        $subject,
1163
        $textMail,
1164
        $email,
1165
        $SETTINGS,
1166
        $textMailAlt = null,
1167
        $silent = true,
1168
        $cron
1169
    );
1170
1171
    if ($silent === false) {
0 ignored issues
show
introduced by
The condition $silent === false is always false.
Loading history...
1172
        return json_encode(
1173
            [
1174
                'error' => false,
1175
                'message' => $lang->get('forgot_my_pw_email_sent'),
1176
            ]
1177
        );
1178
    }
1179
    // Debug purpose
1180
    if ((int) $SETTINGS['email_debug_level'] !== 0 && $cron === false) {
1181
        return json_encode(
1182
            [
1183
                'error' => true,
1184
                'message' => isset($email['ErrorInfo']) === true ? $email['ErrorInfo'] : '',
1185
            ]
1186
        );
1187
    }
1188
    return json_encode(
1189
        [
1190
            'error' => false,
1191
            'message' => $lang->get('share_sent_ok'),
1192
        ]
1193
    );
1194
}
1195
1196
1197
function buildEmail(
1198
    $subject,
1199
    $textMail,
1200
    $email,
1201
    $SETTINGS,
1202
    $textMailAlt = null,
1203
    $silent = true,
1204
    $cron = false
1205
)
1206
{
1207
    // Load PHPMailer
1208
    $mail = new PHPMailer(true);
1209
    $languageDir = $SETTINGS['cpassman_dir'] . '/vendor/phpmailer/phpmailer/language/';
1210
1211
    try {
1212
        // Set language and SMTPDebug
1213
        $mail->setLanguage('en', $languageDir);
1214
        $mail->SMTPDebug = ($cron || $silent) ? 0 : $SETTINGS['email_debug_level'];
1215
        
1216
        /*
1217
        // Define custom Debug output function
1218
        $mail->Debugoutput = function($str, $level) {
1219
            // Path to your log file
1220
            $logFilePath = '/var/log/phpmailer.log';
1221
            file_put_contents($logFilePath, gmdate('Y-m-d H:i:s'). "\t$level\t$str\n", FILE_APPEND | LOCK_EX);
1222
        };
1223
        */
1224
1225
        // Configure SMTP
1226
        $mail->isSMTP();
1227
        $mail->Host = $SETTINGS['email_smtp_server'];
1228
        $mail->SMTPAuth = (int) $SETTINGS['email_smtp_auth'] === 1;
1229
        $mail->Username = $SETTINGS['email_auth_username'];
1230
        $mail->Password = $SETTINGS['email_auth_pwd'];
1231
        $mail->Port = (int) $SETTINGS['email_port'];
1232
        $mail->SMTPSecure = $SETTINGS['email_security'] !== 'none' ? $SETTINGS['email_security'] : '';
1233
        $mail->SMTPAutoTLS = $SETTINGS['email_security'] !== 'none';
1234
        $mail->SMTPOptions = [
1235
            'ssl' => [
1236
                'verify_peer' => false,
1237
                'verify_peer_name' => false,
1238
                'allow_self_signed' => true,
1239
            ],
1240
        ];
1241
1242
        // Set From and FromName
1243
        $mail->From = $SETTINGS['email_from'];
1244
        $mail->FromName = $SETTINGS['email_from_name'];
1245
1246
        // Prepare recipients
1247
        foreach (array_filter(explode(',', $email)) as $dest) {
1248
            $mail->addAddress($dest);
1249
        }
1250
        
1251
        // Prepare HTML and AltBody
1252
        $text_html = emailBody($textMail);
1253
        $mail->WordWrap = 80;
1254
        $mail->isHtml(true);
1255
        $mail->Subject = $subject;
1256
        $mail->Body = $text_html;
1257
        $mail->AltBody = is_null($textMailAlt) ? '' : $textMailAlt;
1258
        
1259
        // Send email
1260
        $mail->send();
1261
        $mail->smtpClose();
1262
        
1263
        return '';
1264
    } catch (Exception $e) {
1265
        if (!$silent || (int) $SETTINGS['email_debug_level'] !== 0) {
1266
            return json_encode([
1267
                'error' => true,
1268
                'errorInfo' => str_replace(["\n", "\t", "\r"], '', $mail->ErrorInfo),
1269
            ]);
1270
        }
1271
        return '';
1272
    }
1273
}
1274
1275
/**
1276
 * Returns the email body.
1277
 *
1278
 * @param string $textMail Text for the email
1279
 */
1280
function emailBody(string $textMail): string
1281
{
1282
    return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
1283
    w3.org/TR/html4/loose.dtd"><html>
1284
    <head><title>Email Template</title>
1285
    <style type="text/css">
1286
    body { background-color: #f0f0f0; padding: 10px 0; margin:0 0 10px =0; }
1287
    </style></head>
1288
    <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">
1289
    <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#f0f0f0" style="border-spacing: 0;">
1290
    <tr><td style="border-collapse: collapse;"><br>
1291
        <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#17357c" style="border-spacing: 0; margin-bottom: 25px;">
1292
        <tr><td style="border-collapse: collapse; padding: 11px 20px;">
1293
            <div style="max-width:150px; max-height:34px; color:#f0f0f0; font-weight:bold;">Teampass</div>
1294
        </td></tr></table></td>
1295
    </tr>
1296
    <tr><td align="center" valign="top" bgcolor="#f0f0f0" style="border-collapse: collapse; background-color: #f0f0f0;">
1297
        <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;">
1298
        <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;">
1299
        <br><div style="float:right;">' .
1300
        $textMail .
1301
        '<br><br></td></tr></table>
1302
    </td></tr></table>
1303
    <br></body></html>';
1304
}
1305
1306
/**
1307
 * Generate a Key.
1308
 * 
1309
 * @return string
1310
 */
1311
function generateKey(): string
1312
{
1313
    return substr(md5(rand() . rand()), 0, 15);
1314
}
1315
1316
/**
1317
 * Convert date to timestamp.
1318
 *
1319
 * @param string $date        The date
1320
 * @param string $date_format Date format
1321
 *
1322
 * @return int
1323
 */
1324
function dateToStamp(string $date, string $date_format): int
1325
{
1326
    $date = date_parse_from_format($date_format, $date);
1327
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1328
        return mktime(
1329
            empty($date['hour']) === false ? $date['hour'] : 23,
1330
            empty($date['minute']) === false ? $date['minute'] : 59,
1331
            empty($date['second']) === false ? $date['second'] : 59,
1332
            $date['month'],
1333
            $date['day'],
1334
            $date['year']
1335
        );
1336
    }
1337
    return 0;
1338
}
1339
1340
/**
1341
 * Is this a date.
1342
 *
1343
 * @param string $date Date
1344
 *
1345
 * @return bool
1346
 */
1347
function isDate(string $date): bool
1348
{
1349
    return strtotime($date) !== false;
1350
}
1351
1352
/**
1353
 * Check if isUTF8().
1354
 *
1355
 * @param string|array $string Is the string
1356
 *
1357
 * @return int is the string in UTF8 format
1358
 */
1359
function isUTF8($string): int
1360
{
1361
    if (is_array($string) === true) {
1362
        $string = $string['string'];
1363
    }
1364
1365
    return preg_match(
1366
        '%^(?:
1367
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1368
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1369
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1370
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1371
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1372
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1373
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1374
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1375
        )*$%xs',
1376
        $string
1377
    );
1378
}
1379
1380
/**
1381
 * Prepare an array to UTF8 format before JSON_encode.
1382
 *
1383
 * @param array $array Array of values
1384
 *
1385
 * @return array
1386
 */
1387
function utf8Converter(array $array): array
1388
{
1389
    array_walk_recursive(
1390
        $array,
1391
        static function (&$item): void {
1392
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1393
                $item = mb_convert_encoding($item, 'ISO-8859-1', 'UTF-8');
1394
            }
1395
        }
1396
    );
1397
    return $array;
1398
}
1399
1400
/**
1401
 * Permits to prepare data to be exchanged.
1402
 *
1403
 * @param array|string $data Text
1404
 * @param string       $type Parameter
1405
 * @param string       $key  Optional key
1406
 *
1407
 * @return string|array
1408
 */
1409
function prepareExchangedData($data, string $type, ?string $key = null)
1410
{
1411
    $session = SessionManager::getSession();
1412
    
1413
    // Perform
1414
    if ($type === 'encode' && is_array($data) === true) {
1415
        // Now encode
1416
        return Encryption::encrypt(
1417
            json_encode(
1418
                $data,
1419
                JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1420
            ),
1421
            $session->get('key')
1422
        );
1423
    }
1424
    if ($type === 'decode' && is_array($data) === false) {
1425
        // check if key exists
1426
        return json_decode(
1427
            (string) Encryption::decrypt(
1428
                (string) $data,
1429
                $session->get('key')
1430
            ),
1431
            true
1432
        );
1433
    }
1434
    return '';
1435
}
1436
1437
1438
/**
1439
 * Create a thumbnail.
1440
 *
1441
 * @param string  $src           Source
1442
 * @param string  $dest          Destination
1443
 * @param int $desired_width Size of width
1444
 * 
1445
 * @return void|string|bool
1446
 */
1447
function makeThumbnail(string $src, string $dest, int $desired_width)
1448
{
1449
    /* read the source image */
1450
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1451
        $source_image = imagecreatefrompng($src);
1452
        if ($source_image === false) {
1453
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1454
        }
1455
    } else {
1456
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1457
    }
1458
1459
    // Get height and width
1460
    $width = imagesx($source_image);
1461
    $height = imagesy($source_image);
1462
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1463
    $desired_height = (int) floor($height * $desired_width / $width);
1464
    /* create a new, "virtual" image */
1465
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1466
    if ($virtual_image === false) {
1467
        return false;
1468
    }
1469
    /* copy source image at a resized size */
1470
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1471
    /* create the physical thumbnail image to its destination */
1472
    imagejpeg($virtual_image, $dest);
1473
}
1474
1475
/**
1476
 * Check table prefix in SQL query.
1477
 *
1478
 * @param string $table Table name
1479
 * 
1480
 * @return string
1481
 */
1482
function prefixTable(string $table): string
1483
{
1484
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1485
    if (empty($safeTable) === false) {
1486
        // sanitize string
1487
        return $safeTable;
1488
    }
1489
    // stop error no table
1490
    return 'table_not_exists';
1491
}
1492
1493
/**
1494
 * GenerateCryptKey
1495
 *
1496
 * @param int     $size      Length
1497
 * @param bool $secure Secure
1498
 * @param bool $numerals Numerics
1499
 * @param bool $uppercase Uppercase letters
1500
 * @param bool $symbols Symbols
1501
 * @param bool $lowercase Lowercase
1502
 * @param array   $SETTINGS  SETTINGS
1503
 * 
1504
 * @return string
1505
 */
1506
function GenerateCryptKey(
1507
    int $size = 20,
1508
    bool $secure = false,
1509
    bool $numerals = false,
1510
    bool $uppercase = false,
1511
    bool $symbols = false,
1512
    bool $lowercase = false,
1513
    array $SETTINGS = []
1514
): string {
1515
    $generator = new ComputerPasswordGenerator();
1516
    $generator->setRandomGenerator(new Php7RandomGenerator());
1517
    
1518
    // Manage size
1519
    $generator->setLength((int) $size);
1520
    if ($secure === true) {
1521
        $generator->setSymbols(true);
1522
        $generator->setLowercase(true);
1523
        $generator->setUppercase(true);
1524
        $generator->setNumbers(true);
1525
    } else {
1526
        $generator->setLowercase($lowercase);
1527
        $generator->setUppercase($uppercase);
1528
        $generator->setNumbers($numerals);
1529
        $generator->setSymbols($symbols);
1530
    }
1531
1532
    return $generator->generatePasswords()[0];
1533
}
1534
1535
/**
1536
 * Send sysLOG message
1537
 *
1538
 * @param string    $message
1539
 * @param string    $host
1540
 * @param int       $port
1541
 * @param string    $component
1542
 * 
1543
 * @return void
1544
*/
1545
function send_syslog($message, $host, $port, $component = 'teampass'): void
1546
{
1547
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1548
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1549
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1550
    socket_close($sock);
1551
}
1552
1553
/**
1554
 * Permits to log events into DB
1555
 *
1556
 * @param array  $SETTINGS Teampass settings
1557
 * @param string $type     Type
1558
 * @param string $label    Label
1559
 * @param string $who      Who
1560
 * @param string $login    Login
1561
 * @param string|int $field_1  Field
1562
 * 
1563
 * @return void
1564
 */
1565
function logEvents(
1566
    array $SETTINGS, 
1567
    string $type, 
1568
    string $label, 
1569
    string $who, 
1570
    ?string $login = null, 
1571
    $field_1 = null
1572
): void
1573
{
1574
    if (empty($who)) {
1575
        $who = getClientIpServer();
1576
    }
1577
1578
    // Load class DB
1579
    loadClasses('DB');
1580
1581
    DB::insert(
1582
        prefixTable('log_system'),
1583
        [
1584
            'type' => $type,
1585
            'date' => time(),
1586
            'label' => $label,
1587
            'qui' => $who,
1588
            'field_1' => $field_1 === null ? '' : $field_1,
1589
        ]
1590
    );
1591
    // If SYSLOG
1592
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1593
        if ($type === 'user_mngt') {
1594
            send_syslog(
1595
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1596
                $SETTINGS['syslog_host'],
1597
                $SETTINGS['syslog_port'],
1598
                'teampass'
1599
            );
1600
        } else {
1601
            send_syslog(
1602
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1603
                $SETTINGS['syslog_host'],
1604
                $SETTINGS['syslog_port'],
1605
                'teampass'
1606
            );
1607
        }
1608
    }
1609
}
1610
1611
/**
1612
 * Log events.
1613
 *
1614
 * @param array  $SETTINGS        Teampass settings
1615
 * @param int    $item_id         Item id
1616
 * @param string $item_label      Item label
1617
 * @param int    $id_user         User id
1618
 * @param string $action          Code for reason
1619
 * @param string $login           User login
1620
 * @param string $raison          Code for reason
1621
 * @param string $encryption_type Encryption on
1622
 * @param string $time Encryption Time
1623
 * @param string $old_value       Old value
1624
 * 
1625
 * @return void
1626
 */
1627
function logItems(
1628
    array $SETTINGS,
1629
    int $item_id,
1630
    string $item_label,
1631
    int $id_user,
1632
    string $action,
1633
    ?string $login = null,
1634
    ?string $raison = null,
1635
    ?string $encryption_type = null,
1636
    ?string $time = null,
1637
    ?string $old_value = null
1638
): void {
1639
    // Load class DB
1640
    loadClasses('DB');
1641
1642
    // Insert log in DB
1643
    DB::insert(
1644
        prefixTable('log_items'),
1645
        [
1646
            'id_item' => $item_id,
1647
            'date' => is_null($time) === true ? time() : $time,
1648
            'id_user' => $id_user,
1649
            'action' => $action,
1650
            'raison' => $raison,
1651
            'old_value' => $old_value,
1652
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1653
        ]
1654
    );
1655
    // Timestamp the last change
1656
    if ($action === 'at_creation' || $action === 'at_modifiation' || $action === 'at_delete' || $action === 'at_import') {
1657
        DB::update(
1658
            prefixTable('misc'),
1659
            [
1660
                'valeur' => time(),
1661
            ],
1662
            'type = %s AND intitule = %s',
1663
            'timestamp',
1664
            'last_item_change'
1665
        );
1666
    }
1667
1668
    // SYSLOG
1669
    if (isset($SETTINGS['syslog_enable']) === true && $SETTINGS['syslog_enable'] === '1') {
1670
        // Extract reason
1671
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1672
        // Get item info if not known
1673
        if (empty($item_label) === true) {
1674
            $dataItem = DB::queryfirstrow(
1675
                'SELECT id, id_tree, label
1676
                FROM ' . prefixTable('items') . '
1677
                WHERE id = %i',
1678
                $item_id
1679
            );
1680
            $item_label = $dataItem['label'];
1681
        }
1682
1683
        send_syslog(
1684
            'action=' . str_replace('at_', '', $action) .
1685
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1686
                ' itemno=' . $item_id .
1687
                ' user=' . is_null($login) === true ? '' : addslashes((string) $login) .
1688
                ' itemname="' . addslashes($item_label) . '"',
1689
            $SETTINGS['syslog_host'],
1690
            $SETTINGS['syslog_port'],
1691
            'teampass'
1692
        );
1693
    }
1694
1695
    // send notification if enabled
1696
    //notifyOnChange($item_id, $action, $SETTINGS);
1697
}
1698
1699
/**
1700
 * Prepare notification email to subscribers.
1701
 *
1702
 * @param int    $item_id  Item id
1703
 * @param string $label    Item label
1704
 * @param array  $changes  List of changes
1705
 * @param array  $SETTINGS Teampass settings
1706
 * 
1707
 * @return void
1708
 */
1709
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1710
{
1711
    $session = SessionManager::getSession();
1712
    $lang = new Language(); 
1713
    $globalsUserId = $session->get('user-id');
1714
    $globalsLastname = $session->get('user-lastname');
1715
    $globalsName = $session->get('user-name');
1716
    // send email to user that what to be notified
1717
    $notification = DB::queryOneColumn(
1718
        'email',
1719
        'SELECT *
1720
        FROM ' . prefixTable('notification') . ' AS n
1721
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1722
        WHERE n.item_id = %i AND n.user_id != %i',
1723
        $item_id,
1724
        $globalsUserId
1725
    );
1726
    if (DB::count() > 0) {
1727
        // Prepare path
1728
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1729
        // Get list of changes
1730
        $htmlChanges = '<ul>';
1731
        foreach ($changes as $change) {
1732
            $htmlChanges .= '<li>' . $change . '</li>';
1733
        }
1734
        $htmlChanges .= '</ul>';
1735
        // send email
1736
        DB::insert(
1737
            prefixTable('emails'),
1738
            [
1739
                'timestamp' => time(),
1740
                'subject' => $lang->get('email_subject_item_updated'),
1741
                'body' => str_replace(
1742
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1743
                    [$label, $path, $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1744
                    $lang->get('email_body_item_updated')
1745
                ),
1746
                'receivers' => implode(',', $notification),
1747
                'status' => '',
1748
            ]
1749
        );
1750
    }
1751
}
1752
1753
/**
1754
 * Returns the Item + path.
1755
 *
1756
 * @param int    $id_tree  Node id
1757
 * @param string $label    Label
1758
 * @param array  $SETTINGS TP settings
1759
 * 
1760
 * @return string
1761
 */
1762
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1763
{
1764
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1765
    $arbo = $tree->getPath($id_tree, true);
1766
    $path = '';
1767
    foreach ($arbo as $elem) {
1768
        if (empty($path) === true) {
1769
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1770
        } else {
1771
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1772
        }
1773
    }
1774
1775
    // Build text to show user
1776
    if (empty($label) === false) {
1777
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1778
    }
1779
    return empty($path) === true ? '' : $path;
1780
}
1781
1782
/**
1783
 * Get the client ip address.
1784
 *
1785
 * @return string IP address
1786
 */
1787
function getClientIpServer(): string
1788
{
1789
    if (getenv('HTTP_CLIENT_IP')) {
1790
        $ipaddress = getenv('HTTP_CLIENT_IP');
1791
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1792
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1793
    } elseif (getenv('HTTP_X_FORWARDED')) {
1794
        $ipaddress = getenv('HTTP_X_FORWARDED');
1795
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1796
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1797
    } elseif (getenv('HTTP_FORWARDED')) {
1798
        $ipaddress = getenv('HTTP_FORWARDED');
1799
    } elseif (getenv('REMOTE_ADDR')) {
1800
        $ipaddress = getenv('REMOTE_ADDR');
1801
    } else {
1802
        $ipaddress = 'UNKNOWN';
1803
    }
1804
1805
    return $ipaddress;
1806
}
1807
1808
/**
1809
 * Escape all HTML, JavaScript, and CSS.
1810
 *
1811
 * @param string $input    The input string
1812
 * @param string $encoding Which character encoding are we using?
1813
 * 
1814
 * @return string
1815
 */
1816
function noHTML(string $input, string $encoding = 'UTF-8'): string
1817
{
1818
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1819
}
1820
1821
/**
1822
 * Permits to handle the Teampass config file
1823
 * $action accepts "rebuild" and "update"
1824
 *
1825
 * @param string $action   Action to perform
1826
 * @param array  $SETTINGS Teampass settings
1827
 * @param string $field    Field to refresh
1828
 * @param string $value    Value to set
1829
 *
1830
 * @return string|bool
1831
 */
1832
function handleConfigFile($action, $SETTINGS, $field = null, $value = null)
1833
{
1834
    $tp_config_file = $SETTINGS['cpassman_dir'] . '/includes/config/tp.config.php';
1835
1836
    // Load class DB
1837
    loadClasses('DB');
1838
1839
    if (file_exists($tp_config_file) === false || $action === 'rebuild') {
1840
        // perform a copy
1841
        if (file_exists($tp_config_file)) {
1842
            if (! copy($tp_config_file, $tp_config_file . '.' . date('Y_m_d_His', time()))) {
1843
                return "ERROR: Could not copy file '" . $tp_config_file . "'";
1844
            }
1845
        }
1846
1847
        // regenerate
1848
        $data = [];
1849
        $data[0] = "<?php\n";
1850
        $data[1] = "global \$SETTINGS;\n";
1851
        $data[2] = "\$SETTINGS = array (\n";
1852
        $rows = DB::query(
1853
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s',
1854
            'admin'
1855
        );
1856
        foreach ($rows as $record) {
1857
            array_push($data, "    '" . $record['intitule'] . "' => '" . htmlspecialchars_decode($record['valeur'], ENT_COMPAT) . "',\n");
1858
        }
1859
        array_push($data, ");\n");
1860
        $data = array_unique($data);
1861
    // ---
1862
    } elseif ($action === 'update' && empty($field) === false) {
1863
        $data = file($tp_config_file);
1864
        $inc = 0;
1865
        $bFound = false;
1866
        foreach ($data as $line) {
1867
            if (stristr($line, ');')) {
1868
                break;
1869
            }
1870
1871
            if (stristr($line, "'" . $field . "' => '")) {
1872
                $data[$inc] = "    '" . $field . "' => '" . htmlspecialchars_decode($value ?? '', ENT_COMPAT) . "',\n";
1873
                $bFound = true;
1874
                break;
1875
            }
1876
            ++$inc;
1877
        }
1878
        if ($bFound === false) {
1879
            $data[$inc] = "    '" . $field . "' => '" . htmlspecialchars_decode($value ?? '', ENT_COMPAT). "',\n);\n";
1880
        }
1881
    }
1882
1883
    // update file
1884
    file_put_contents($tp_config_file, implode('', $data ?? []));
1885
    return true;
1886
}
1887
1888
/**
1889
 * Permits to replace &#92; to permit correct display
1890
 *
1891
 * @param string $input Some text
1892
 * 
1893
 * @return string
1894
 */
1895
function handleBackslash(string $input): string
1896
{
1897
    return str_replace('&amp;#92;', '&#92;', $input);
1898
}
1899
1900
/**
1901
 * Permits to load settings
1902
 * 
1903
 * @return void
1904
*/
1905
function loadSettings(): void
1906
{
1907
    global $SETTINGS;
1908
    /* LOAD CPASSMAN SETTINGS */
1909
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1910
        $SETTINGS = [];
1911
        $SETTINGS['duplicate_folder'] = 0;
1912
        //by default, this is set to 0;
1913
        $SETTINGS['duplicate_item'] = 0;
1914
        //by default, this is set to 0;
1915
        $SETTINGS['number_of_used_pw'] = 5;
1916
        //by default, this value is set to 5;
1917
        $settings = [];
1918
        $rows = DB::query(
1919
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1920
            [
1921
                'type' => 'admin',
1922
                'type2' => 'settings',
1923
            ]
1924
        );
1925
        foreach ($rows as $record) {
1926
            if ($record['type'] === 'admin') {
1927
                $SETTINGS[$record['intitule']] = $record['valeur'];
1928
            } else {
1929
                $settings[$record['intitule']] = $record['valeur'];
1930
            }
1931
        }
1932
        $SETTINGS['loaded'] = 1;
1933
        $SETTINGS['default_session_expiration_time'] = 5;
1934
    }
1935
}
1936
1937
/**
1938
 * check if folder has custom fields.
1939
 * Ensure that target one also has same custom fields
1940
 * 
1941
 * @param int $source_id
1942
 * @param int $target_id 
1943
 * 
1944
 * @return bool
1945
*/
1946
function checkCFconsistency(int $source_id, int $target_id): bool
1947
{
1948
    $source_cf = [];
1949
    $rows = DB::QUERY(
1950
        'SELECT id_category
1951
            FROM ' . prefixTable('categories_folders') . '
1952
            WHERE id_folder = %i',
1953
        $source_id
1954
    );
1955
    foreach ($rows as $record) {
1956
        array_push($source_cf, $record['id_category']);
1957
    }
1958
1959
    $target_cf = [];
1960
    $rows = DB::QUERY(
1961
        'SELECT id_category
1962
            FROM ' . prefixTable('categories_folders') . '
1963
            WHERE id_folder = %i',
1964
        $target_id
1965
    );
1966
    foreach ($rows as $record) {
1967
        array_push($target_cf, $record['id_category']);
1968
    }
1969
1970
    $cf_diff = array_diff($source_cf, $target_cf);
1971
    if (count($cf_diff) > 0) {
1972
        return false;
1973
    }
1974
1975
    return true;
1976
}
1977
1978
/**
1979
 * Will encrypte/decrypt a fil eusing Defuse.
1980
 *
1981
 * @param string $type        can be either encrypt or decrypt
1982
 * @param string $source_file path to source file
1983
 * @param string $target_file path to target file
1984
 * @param array  $SETTINGS    Settings
1985
 * @param string $password    A password
1986
 *
1987
 * @return string|bool
1988
 */
1989
function prepareFileWithDefuse(
1990
    string $type,
1991
    string $source_file,
1992
    string $target_file,
1993
    array $SETTINGS,
1994
    string $password = null
1995
) {
1996
    // Load AntiXSS
1997
    $antiXss = new AntiXSS();
1998
    // Protect against bad inputs
1999
    if (is_array($source_file) === true || is_array($target_file) === true) {
2000
        return 'error_cannot_be_array';
2001
    }
2002
2003
    // Sanitize
2004
    $source_file = $antiXss->xss_clean($source_file);
2005
    $target_file = $antiXss->xss_clean($target_file);
2006
    if (empty($password) === true || is_null($password) === true) {
2007
        // get KEY to define password
2008
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
2009
        $password = Key::loadFromAsciiSafeString($ascii_key);
2010
    }
2011
2012
    $err = '';
2013
    if ($type === 'decrypt') {
2014
        // Decrypt file
2015
        $err = defuseFileDecrypt(
2016
            $source_file,
2017
            $target_file,
2018
            $SETTINGS, /** @scrutinizer ignore-type */
2019
            $password
2020
        );
2021
    } elseif ($type === 'encrypt') {
2022
        // Encrypt file
2023
        $err = defuseFileEncrypt(
2024
            $source_file,
2025
            $target_file,
2026
            $SETTINGS, /** @scrutinizer ignore-type */
2027
            $password
2028
        );
2029
    }
2030
2031
    // return error
2032
    return $err === true ? '' : $err;
2033
}
2034
2035
/**
2036
 * Encrypt a file with Defuse.
2037
 *
2038
 * @param string $source_file path to source file
2039
 * @param string $target_file path to target file
2040
 * @param array  $SETTINGS    Settings
2041
 * @param string $password    A password
2042
 *
2043
 * @return string|bool
2044
 */
2045
function defuseFileEncrypt(
2046
    string $source_file,
2047
    string $target_file,
2048
    array $SETTINGS,
2049
    string $password = null
2050
) {
2051
    try {
2052
        CryptoFile::encryptFileWithPassword(
2053
            $source_file,
2054
            $target_file,
2055
            $password
2056
        );
2057
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
2058
        $err = 'wrong_key';
2059
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
2060
        $err = $ex;
2061
    } catch (CryptoException\IOException $ex) {
2062
        $err = $ex;
2063
    }
2064
2065
    // return error
2066
    return empty($err) === false ? $err : true;
2067
}
2068
2069
/**
2070
 * Decrypt a file with Defuse.
2071
 *
2072
 * @param string $source_file path to source file
2073
 * @param string $target_file path to target file
2074
 * @param array  $SETTINGS    Settings
2075
 * @param string $password    A password
2076
 *
2077
 * @return string|bool
2078
 */
2079
function defuseFileDecrypt(
2080
    string $source_file,
2081
    string $target_file,
2082
    array $SETTINGS,
2083
    string $password = null
2084
) {
2085
    try {
2086
        CryptoFile::decryptFileWithPassword(
2087
            $source_file,
2088
            $target_file,
2089
            $password
2090
        );
2091
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
2092
        $err = 'wrong_key';
2093
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
2094
        $err = $ex;
2095
    } catch (CryptoException\IOException $ex) {
2096
        $err = $ex;
2097
    }
2098
2099
    // return error
2100
    return empty($err) === false ? $err : true;
2101
}
2102
2103
/*
2104
* NOT TO BE USED
2105
*/
2106
/**
2107
 * Undocumented function.
2108
 *
2109
 * @param string $text Text to debug
2110
 */
2111
function debugTeampass(string $text): void
2112
{
2113
    $debugFile = fopen('D:/wamp64/www/TeamPass/debug.txt', 'r+');
2114
    if ($debugFile !== false) {
2115
        fputs($debugFile, $text);
2116
        fclose($debugFile);
2117
    }
2118
}
2119
2120
/**
2121
 * DELETE the file with expected command depending on server type.
2122
 *
2123
 * @param string $file     Path to file
2124
 * @param array  $SETTINGS Teampass settings
2125
 *
2126
 * @return void
2127
 */
2128
function fileDelete(string $file, array $SETTINGS): void
2129
{
2130
    // Load AntiXSS
2131
    $antiXss = new AntiXSS();
2132
    $file = $antiXss->xss_clean($file);
2133
    if (is_file($file)) {
2134
        unlink($file);
2135
    }
2136
}
2137
2138
/**
2139
 * Permits to extract the file extension.
2140
 *
2141
 * @param string $file File name
2142
 *
2143
 * @return string
2144
 */
2145
function getFileExtension(string $file): string
2146
{
2147
    if (strpos($file, '.') === false) {
2148
        return $file;
2149
    }
2150
2151
    return substr($file, strrpos($file, '.') + 1);
2152
}
2153
2154
/**
2155
 * Chmods files and folders with different permissions.
2156
 *
2157
 * This is an all-PHP alternative to using: \n
2158
 * <tt>exec("find ".$path." -type f -exec chmod 644 {} \;");</tt> \n
2159
 * <tt>exec("find ".$path." -type d -exec chmod 755 {} \;");</tt>
2160
 *
2161
 * @author Jeppe Toustrup (tenzer at tenzer dot dk)
2162
  *
2163
 * @param string $path      An either relative or absolute path to a file or directory which should be processed.
2164
 * @param int    $filePerm The permissions any found files should get.
2165
 * @param int    $dirPerm  The permissions any found folder should get.
2166
 *
2167
 * @return bool Returns TRUE if the path if found and FALSE if not.
2168
 *
2169
 * @warning The permission levels has to be entered in octal format, which
2170
 * normally means adding a zero ("0") in front of the permission level. \n
2171
 * More info at: http://php.net/chmod.
2172
*/
2173
2174
function recursiveChmod(
2175
    string $path,
2176
    int $filePerm = 0644,
2177
    int  $dirPerm = 0755
2178
) {
2179
    // Check if the path exists
2180
    if (! file_exists($path)) {
2181
        return false;
2182
    }
2183
2184
    // See whether this is a file
2185
    if (is_file($path)) {
2186
        // Chmod the file with our given filepermissions
2187
        try {
2188
            chmod($path, $filePerm);
2189
        } catch (Exception $e) {
2190
            return false;
2191
        }
2192
    // If this is a directory...
2193
    } elseif (is_dir($path)) {
2194
        // Then get an array of the contents
2195
        $foldersAndFiles = scandir($path);
2196
        // Remove "." and ".." from the list
2197
        $entries = array_slice($foldersAndFiles, 2);
2198
        // Parse every result...
2199
        foreach ($entries as $entry) {
2200
            // And call this function again recursively, with the same permissions
2201
            recursiveChmod($path.'/'.$entry, $filePerm, $dirPerm);
2202
        }
2203
2204
        // When we are done with the contents of the directory, we chmod the directory itself
2205
        try {
2206
            chmod($path, $filePerm);
2207
        } catch (Exception $e) {
2208
            return false;
2209
        }
2210
    }
2211
2212
    // Everything seemed to work out well, return true
2213
    return true;
2214
}
2215
2216
/**
2217
 * Check if user can access to this item.
2218
 *
2219
 * @param int   $item_id ID of item
2220
 * @param array $SETTINGS
2221
 *
2222
 * @return bool|string
2223
 */
2224
function accessToItemIsGranted(int $item_id, array $SETTINGS)
2225
{
2226
    
2227
    $session = SessionManager::getSession();
2228
    $session_groupes_visibles = $session->get('user-accessible_folders');
2229
    $session_list_restricted_folders_for_items = $session->get('system-list_restricted_folders_for_items');
2230
    // Load item data
2231
    $data = DB::queryFirstRow(
2232
        'SELECT id_tree
2233
        FROM ' . prefixTable('items') . '
2234
        WHERE id = %i',
2235
        $item_id
2236
    );
2237
    // Check if user can access this folder
2238
    if (in_array($data['id_tree'], $session_groupes_visibles) === false) {
2239
        // Now check if this folder is restricted to user
2240
        if (isset($session_list_restricted_folders_for_items[$data['id_tree']]) === true
2241
            && in_array($item_id, $session_list_restricted_folders_for_items[$data['id_tree']]) === false
2242
        ) {
2243
            return 'ERR_FOLDER_NOT_ALLOWED';
2244
        }
2245
    }
2246
2247
    return true;
2248
}
2249
2250
/**
2251
 * Creates a unique key.
2252
 *
2253
 * @param int $lenght Key lenght
2254
 *
2255
 * @return string
2256
 */
2257
function uniqidReal(int $lenght = 13): string
2258
{
2259
    if (function_exists('random_bytes')) {
2260
        $bytes = random_bytes(intval(ceil($lenght / 2)));
2261
    } elseif (function_exists('openssl_random_pseudo_bytes')) {
2262
        $bytes = openssl_random_pseudo_bytes(intval(ceil($lenght / 2)));
2263
    } else {
2264
        throw new Exception('no cryptographically secure random function available');
2265
    }
2266
2267
    return substr(bin2hex($bytes), 0, $lenght);
2268
}
2269
2270
/**
2271
 * Obfuscate an email.
2272
 *
2273
 * @param string $email Email address
2274
 *
2275
 * @return string
2276
 */
2277
function obfuscateEmail(string $email): string
2278
{
2279
    $email = explode("@", $email);
2280
    $name = $email[0];
2281
    if (strlen($name) > 3) {
2282
        $name = substr($name, 0, 2);
2283
        for ($i = 0; $i < strlen($email[0]) - 3; $i++) {
2284
            $name .= "*";
2285
        }
2286
        $name .= substr($email[0], -1, 1);
2287
    }
2288
    $host = explode(".", $email[1])[0];
2289
    if (strlen($host) > 3) {
2290
        $host = substr($host, 0, 1);
2291
        for ($i = 0; $i < strlen(explode(".", $email[1])[0]) - 2; $i++) {
2292
            $host .= "*";
2293
        }
2294
        $host .= substr(explode(".", $email[1])[0], -1, 1);
2295
    }
2296
    $email = $name . "@" . $host . "." . explode(".", $email[1])[1];
2297
    return $email;
2298
}
2299
2300
/**
2301
 * Perform a Query.
2302
 *
2303
 * @param array  $SETTINGS Teamapss settings
2304
 * @param string $fields   Fields to use
2305
 * @param string $table    Table to use
2306
 *
2307
 * @return array
2308
 */
2309
function performDBQuery(array $SETTINGS, string $fields, string $table): array
2310
{
2311
    // include librairies & connect to DB
2312
    //include_once $SETTINGS['cpassman_dir'] . '/includes/config/settings.php';
2313
2314
    // Load class DB
2315
    loadClasses('DB');
2316
    
2317
    // Insert log in DB
2318
    return DB::query(
2319
        'SELECT ' . $fields . '
2320
        FROM ' . prefixTable($table)
2321
    );
2322
}
2323
2324
/**
2325
 * Undocumented function.
2326
 *
2327
 * @param int $bytes Size of file
2328
 *
2329
 * @return string
2330
 */
2331
function formatSizeUnits(int $bytes): string
2332
{
2333
    if ($bytes >= 1073741824) {
2334
        $bytes = number_format($bytes / 1073741824, 2) . ' GB';
2335
    } elseif ($bytes >= 1048576) {
2336
        $bytes = number_format($bytes / 1048576, 2) . ' MB';
2337
    } elseif ($bytes >= 1024) {
2338
        $bytes = number_format($bytes / 1024, 2) . ' KB';
2339
    } elseif ($bytes > 1) {
2340
        $bytes .= ' bytes';
2341
    } elseif ($bytes === 1) {
2342
        $bytes .= ' byte';
2343
    } else {
2344
        $bytes = '0 bytes';
2345
    }
2346
2347
    return $bytes;
2348
}
2349
2350
/**
2351
 * Generate user pair of keys.
2352
 *
2353
 * @param string $userPwd User password
2354
 *
2355
 * @return array
2356
 */
2357
function generateUserKeys(string $userPwd): array
2358
{
2359
    // Sanitize
2360
    $antiXss = new AntiXSS();
2361
    $userPwd = $antiXss->xss_clean($userPwd);
2362
    // Load classes
2363
    $rsa = new Crypt_RSA();
2364
    $cipher = new Crypt_AES();
2365
    // Create the private and public key
2366
    $res = $rsa->createKey(4096);
2367
    // Encrypt the privatekey
2368
    $cipher->setPassword($userPwd);
2369
    $privatekey = $cipher->encrypt($res['privatekey']);
2370
    return [
2371
        'private_key' => base64_encode($privatekey),
2372
        'public_key' => base64_encode($res['publickey']),
2373
        'private_key_clear' => base64_encode($res['privatekey']),
2374
    ];
2375
}
2376
2377
/**
2378
 * Permits to decrypt the user's privatekey.
2379
 *
2380
 * @param string $userPwd        User password
2381
 * @param string $userPrivateKey User private key
2382
 *
2383
 * @return string|object
2384
 */
2385
function decryptPrivateKey(string $userPwd, string $userPrivateKey)
2386
{
2387
    // Sanitize
2388
    $antiXss = new AntiXSS();
2389
    $userPwd = $antiXss->xss_clean($userPwd);
2390
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2391
2392
    if (empty($userPwd) === false) {
2393
        // Load classes
2394
        $cipher = new Crypt_AES();
2395
        // Encrypt the privatekey
2396
        $cipher->setPassword($userPwd);
2397
        try {
2398
            return base64_encode((string) $cipher->decrypt(base64_decode($userPrivateKey)));
2399
        } catch (Exception $e) {
2400
            return $e;
2401
        }
2402
    }
2403
    return '';
2404
}
2405
2406
/**
2407
 * Permits to encrypt the user's privatekey.
2408
 *
2409
 * @param string $userPwd        User password
2410
 * @param string $userPrivateKey User private key
2411
 *
2412
 * @return string
2413
 */
2414
function encryptPrivateKey(string $userPwd, string $userPrivateKey): string
2415
{
2416
    // Sanitize
2417
    $antiXss = new AntiXSS();
2418
    $userPwd = $antiXss->xss_clean($userPwd);
2419
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2420
2421
    if (empty($userPwd) === false) {
2422
        // Load classes
2423
        $cipher = new Crypt_AES();
2424
        // Encrypt the privatekey
2425
        $cipher->setPassword($userPwd);        
2426
        try {
2427
            return base64_encode($cipher->encrypt(base64_decode($userPrivateKey)));
2428
        } catch (Exception $e) {
2429
            return $e;
2430
        }
2431
    }
2432
    return '';
2433
}
2434
2435
/**
2436
 * Encrypts a string using AES.
2437
 *
2438
 * @param string $data String to encrypt
2439
 * @param string $key
2440
 *
2441
 * @return array
2442
 */
2443
function doDataEncryption(string $data, string $key = NULL): array
2444
{
2445
    // Sanitize
2446
    $antiXss = new AntiXSS();
2447
    $data = $antiXss->xss_clean($data);
2448
    
2449
    // Load classes
2450
    $cipher = new Crypt_AES(CRYPT_AES_MODE_CBC);
2451
    // Generate an object key
2452
    $objectKey = is_null($key) === true ? uniqidReal(KEY_LENGTH) : $antiXss->xss_clean($key);
2453
    // Set it as password
2454
    $cipher->setPassword($objectKey);
2455
    return [
2456
        'encrypted' => base64_encode($cipher->encrypt($data)),
2457
        'objectKey' => base64_encode($objectKey),
2458
    ];
2459
}
2460
2461
/**
2462
 * Decrypts a string using AES.
2463
 *
2464
 * @param string $data Encrypted data
2465
 * @param string $key  Key to uncrypt
2466
 *
2467
 * @return string
2468
 */
2469
function doDataDecryption(string $data, string $key): string
2470
{
2471
    // Sanitize
2472
    $antiXss = new AntiXSS();
2473
    $data = $antiXss->xss_clean($data);
2474
    $key = $antiXss->xss_clean($key);
2475
2476
    // Load classes
2477
    $cipher = new Crypt_AES();
2478
    // Set the object key
2479
    $cipher->setPassword(base64_decode($key));
2480
    return base64_encode((string) $cipher->decrypt(base64_decode($data)));
2481
}
2482
2483
/**
2484
 * Encrypts using RSA a string using a public key.
2485
 *
2486
 * @param string $key       Key to be encrypted
2487
 * @param string $publicKey User public key
2488
 *
2489
 * @return string
2490
 */
2491
function encryptUserObjectKey(string $key, string $publicKey): string
2492
{
2493
    // Sanitize
2494
    $antiXss = new AntiXSS();
2495
    $publicKey = $antiXss->xss_clean($publicKey);
2496
    // Load classes
2497
    $rsa = new Crypt_RSA();
2498
    // Load the public key
2499
    $decodedPublicKey = base64_decode($publicKey, true);
2500
    if ($decodedPublicKey === false) {
2501
        throw new InvalidArgumentException("Error while decoding key.");
2502
    }
2503
    $rsa->loadKey($decodedPublicKey);
2504
    // Encrypt
2505
    $encrypted = $rsa->encrypt(base64_decode($key));
2506
    if ($encrypted === false) {
0 ignored issues
show
introduced by
The condition $encrypted === false is always false.
Loading history...
2507
        throw new RuntimeException("Error while encrypting key.");
2508
    }
2509
    // Return
2510
    return base64_encode($encrypted);
2511
}
2512
2513
/**
2514
 * Decrypts using RSA an encrypted string using a private key.
2515
 *
2516
 * @param string $key        Encrypted key
2517
 * @param string $privateKey User private key
2518
 *
2519
 * @return string
2520
 */
2521
function decryptUserObjectKey(string $key, string $privateKey): string
2522
{
2523
    // Sanitize
2524
    $antiXss = new AntiXSS();
2525
    $privateKey = $antiXss->xss_clean($privateKey);
2526
2527
    // Load classes
2528
    $rsa = new Crypt_RSA();
2529
    // Load the private key
2530
    $decodedPrivateKey = base64_decode($privateKey, true);
2531
    if ($decodedPrivateKey === false) {
2532
        throw new InvalidArgumentException("Error while decoding private key.");
2533
    }
2534
2535
    $rsa->loadKey($decodedPrivateKey);
2536
2537
    // Decrypt
2538
    try {
2539
        $decodedKey = base64_decode($key, true);
2540
        if ($decodedKey === false) {
2541
            throw new InvalidArgumentException("Error while decoding key.");
2542
        }
2543
2544
        $tmpValue = $rsa->decrypt($decodedKey);
2545
        if ($tmpValue !== false) {
0 ignored issues
show
introduced by
The condition $tmpValue !== false is always true.
Loading history...
2546
            return base64_encode($tmpValue);
2547
        } else {
2548
            return '';
2549
        }
2550
    } catch (Exception $e) {
2551
        error_log('TEAMPASS Error - ldap - '.$e->getMessage());
2552
        return 'Exception: could not decrypt object';
2553
    }
2554
}
2555
2556
/**
2557
 * Encrypts a file.
2558
 *
2559
 * @param string $fileInName File name
2560
 * @param string $fileInPath Path to file
2561
 *
2562
 * @return array
2563
 */
2564
function encryptFile(string $fileInName, string $fileInPath): array
2565
{
2566
    if (defined('FILE_BUFFER_SIZE') === false) {
2567
        define('FILE_BUFFER_SIZE', 128 * 1024);
2568
    }
2569
2570
    // Load classes
2571
    $cipher = new Crypt_AES();
2572
2573
    // Generate an object key
2574
    $objectKey = uniqidReal(32);
2575
    // Set it as password
2576
    $cipher->setPassword($objectKey);
2577
    // Prevent against out of memory
2578
    $cipher->enableContinuousBuffer();
2579
2580
    // Encrypt the file content
2581
    $filePath = filter_var($fileInPath . '/' . $fileInName, FILTER_SANITIZE_URL);
2582
    $fileContent = file_get_contents($filePath);
2583
    $plaintext = $fileContent;
2584
    $ciphertext = $cipher->encrypt($plaintext);
2585
2586
    // Save new file
2587
    $hash = md5($plaintext);
2588
    $fileOut = $fileInPath . '/' . TP_FILE_PREFIX . $hash;
2589
    file_put_contents($fileOut, $ciphertext);
2590
    unlink($fileInPath . '/' . $fileInName);
2591
    return [
2592
        'fileHash' => base64_encode($hash),
2593
        'objectKey' => base64_encode($objectKey),
2594
    ];
2595
}
2596
2597
/**
2598
 * Decrypt a file.
2599
 *
2600
 * @param string $fileName File name
2601
 * @param string $filePath Path to file
2602
 * @param string $key      Key to use
2603
 *
2604
 * @return string
2605
 */
2606
function decryptFile(string $fileName, string $filePath, string $key): string
2607
{
2608
    if (! defined('FILE_BUFFER_SIZE')) {
2609
        define('FILE_BUFFER_SIZE', 128 * 1024);
2610
    }
2611
    
2612
    // Load classes
2613
    $cipher = new Crypt_AES();
2614
    $antiXSS = new AntiXSS();
2615
    
2616
    // Get file name
2617
    $safeFileName = $antiXSS->xss_clean(base64_decode($fileName));
2618
2619
    // Set the object key
2620
    $cipher->setPassword(base64_decode($key));
2621
    // Prevent against out of memory
2622
    $cipher->enableContinuousBuffer();
2623
    $cipher->disablePadding();
2624
    // Get file content
2625
    $safeFilePath = $filePath . '/' . TP_FILE_PREFIX . $safeFileName;
2626
    $ciphertext = file_get_contents(filter_var($safeFilePath, FILTER_SANITIZE_URL));
2627
2628
    if (WIP) error_log('DEBUG: File image url -> '.filter_var($safeFilePath, FILTER_SANITIZE_URL));
2629
2630
    // Decrypt file content and return
2631
    return base64_encode($cipher->decrypt($ciphertext));
2632
}
2633
2634
/**
2635
 * Generate a simple password
2636
 *
2637
 * @param int $length Length of string
2638
 * @param bool $symbolsincluded Allow symbols
2639
 *
2640
 * @return string
2641
 */
2642
function generateQuickPassword(int $length = 16, bool $symbolsincluded = true): string
2643
{
2644
    // Generate new user password
2645
    $small_letters = range('a', 'z');
2646
    $big_letters = range('A', 'Z');
2647
    $digits = range(0, 9);
2648
    $symbols = $symbolsincluded === true ?
2649
        ['#', '_', '-', '@', '$', '+', '!'] : [];
2650
    $res = array_merge($small_letters, $big_letters, $digits, $symbols);
2651
    $count = count($res);
2652
    // first variant
2653
2654
    $random_string = '';
2655
    for ($i = 0; $i < $length; ++$i) {
2656
        $random_string .= $res[random_int(0, $count - 1)];
2657
    }
2658
2659
    return $random_string;
2660
}
2661
2662
/**
2663
 * Permit to store the sharekey of an object for users.
2664
 *
2665
 * @param string $object_name             Type for table selection
2666
 * @param int    $post_folder_is_personal Personal
2667
 * @param int    $post_folder_id          Folder
2668
 * @param int    $post_object_id          Object
2669
 * @param string $objectKey               Object key
2670
 * @param array  $SETTINGS                Teampass settings
2671
 * @param int    $user_id                 User ID if needed
2672
 * @param bool   $onlyForUser                 User ID if needed
2673
 * @param bool   $deleteAll                 User ID if needed
2674
 * @param array  $objectKeyArray                 User ID if needed
2675
 *
2676
 * @return void
2677
 */
2678
function storeUsersShareKey(
2679
    string $object_name,
2680
    int $post_folder_is_personal,
2681
    int $post_folder_id,
2682
    int $post_object_id,
2683
    string $objectKey,
2684
    array $SETTINGS,
2685
    bool $onlyForUser = false,
2686
    bool $deleteAll = true,
2687
    array $objectKeyArray = []
2688
): void {
2689
    
2690
    $session = SessionManager::getSession();
2691
    loadClasses('DB');
2692
2693
    // Delete existing entries for this object
2694
    if ($deleteAll === true) {
2695
        DB::delete(
2696
            $object_name,
2697
            'object_id = %i',
2698
            $post_object_id
2699
        );
2700
    }
2701
    
2702
    if (
2703
        $onlyForUser === true || (int) $post_folder_is_personal === 1
2704
    ) {
2705
        // Only create the sharekey for a user
2706
        $user = DB::queryFirstRow(
2707
            'SELECT public_key
2708
            FROM ' . prefixTable('users') . '
2709
            WHERE id = ' . (int) $session->get('user-id') . '
2710
            AND public_key != ""'
2711
        );
2712
2713
        if (empty($objectKey) === false) {
2714
            DB::insert(
2715
                $object_name,
2716
                [
2717
                    'object_id' => (int) $post_object_id,
2718
                    'user_id' => (int) $session->get('user-id'),
2719
                    'share_key' => encryptUserObjectKey(
2720
                        $objectKey,
2721
                        $user['public_key']
2722
                    ),
2723
                ]
2724
            );
2725
        } else if (count($objectKeyArray) > 0) {
2726
            foreach ($objectKeyArray as $object) {
2727
                DB::insert(
2728
                    $object_name,
2729
                    [
2730
                        'object_id' => (int) $object['objectId'],
2731
                        'user_id' => (int) $session->get('user-id'),
2732
                        'share_key' => encryptUserObjectKey(
2733
                            $object['objectKey'],
2734
                            $user['public_key']
2735
                        ),
2736
                    ]
2737
                );
2738
            }
2739
        }
2740
    } else {
2741
        // Create sharekey for each user
2742
        //DB::debugmode(true);
2743
        $users = DB::query(
2744
            'SELECT id, public_key
2745
            FROM ' . prefixTable('users') . '
2746
            WHERE ' . ($onlyForUser === true ? 
0 ignored issues
show
introduced by
The condition $onlyForUser === true is always false.
Loading history...
2747
                'id IN ("' . TP_USER_ID . '","' . $session->get('user-id') . '") ' : 
2748
                'id NOT IN ("' . OTV_USER_ID . '","' . SSH_USER_ID . '","' . API_USER_ID . '") ') . '
2749
            AND public_key != ""'
2750
        );
2751
        //DB::debugmode(false);
2752
        foreach ($users as $user) {
2753
            // Insert in DB the new object key for this item by user
2754
            if (count($objectKeyArray) === 0) {
2755
                DB::insert(
2756
                    $object_name,
2757
                    [
2758
                        'object_id' => $post_object_id,
2759
                        'user_id' => (int) $user['id'],
2760
                        'share_key' => encryptUserObjectKey(
2761
                            $objectKey,
2762
                            $user['public_key']
2763
                        ),
2764
                    ]
2765
                );
2766
            } else {
2767
                foreach ($objectKeyArray as $object) {
2768
                    DB::insert(
2769
                        $object_name,
2770
                        [
2771
                            'object_id' => (int) $object['objectId'],
2772
                            'user_id' => (int) $user['id'],
2773
                            'share_key' => encryptUserObjectKey(
2774
                                $object['objectKey'],
2775
                                $user['public_key']
2776
                            ),
2777
                        ]
2778
                    );
2779
                }
2780
            }
2781
        }
2782
    }
2783
}
2784
2785
/**
2786
 * Is this string base64 encoded?
2787
 *
2788
 * @param string $str Encoded string?
2789
 *
2790
 * @return bool
2791
 */
2792
function isBase64(string $str): bool
2793
{
2794
    $str = (string) trim($str);
2795
    if (! isset($str[0])) {
2796
        return false;
2797
    }
2798
2799
    $base64String = (string) base64_decode($str, true);
2800
    if ($base64String && base64_encode($base64String) === $str) {
2801
        return true;
2802
    }
2803
2804
    return false;
2805
}
2806
2807
/**
2808
 * Undocumented function
2809
 *
2810
 * @param string $field Parameter
2811
 *
2812
 * @return array|bool|resource|string
2813
 */
2814
function filterString(string $field)
2815
{
2816
    // Sanitize string
2817
    $field = filter_var(trim($field), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
2818
    if (empty($field) === false) {
2819
        // Load AntiXSS
2820
        $antiXss = new AntiXSS();
2821
        // Return
2822
        return $antiXss->xss_clean($field);
2823
    }
2824
2825
    return false;
2826
}
2827
2828
/**
2829
 * CHeck if provided credentials are allowed on server
2830
 *
2831
 * @param string $login    User Login
2832
 * @param string $password User Pwd
2833
 * @param array  $SETTINGS Teampass settings
2834
 *
2835
 * @return bool
2836
 */
2837
function ldapCheckUserPassword(string $login, string $password, array $SETTINGS): bool
2838
{
2839
    // Build ldap configuration array
2840
    $config = [
2841
        // Mandatory Configuration Options
2842
        'hosts' => [$SETTINGS['ldap_hosts']],
2843
        'base_dn' => $SETTINGS['ldap_bdn'],
2844
        'username' => $SETTINGS['ldap_username'],
2845
        'password' => $SETTINGS['ldap_password'],
2846
2847
        // Optional Configuration Options
2848
        'port' => $SETTINGS['ldap_port'],
2849
        'use_ssl' => (int) $SETTINGS['ldap_ssl'] === 1 ? true : false,
2850
        'use_tls' => (int) $SETTINGS['ldap_tls'] === 1 ? true : false,
2851
        'version' => 3,
2852
        'timeout' => 5,
2853
        'follow_referrals' => false,
2854
2855
        // Custom LDAP Options
2856
        'options' => [
2857
            // See: http://php.net/ldap_set_option
2858
            LDAP_OPT_X_TLS_REQUIRE_CERT => (isset($SETTINGS['ldap_tls_certiface_check']) ? $SETTINGS['ldap_tls_certiface_check'] : LDAP_OPT_X_TLS_HARD),
2859
        ],
2860
    ];
2861
    
2862
    $connection = new Connection($config);
2863
    // Connect to LDAP
2864
    try {
2865
        $connection->connect();
2866
    } catch (\LdapRecord\Auth\BindException $e) {
2867
        $error = $e->getDetailedError();
2868
        echo 'Error : '.$error->getErrorCode().' - '.$error->getErrorMessage(). '<br>'.$error->getDiagnosticMessage();
2869
        return false;
2870
    }
2871
2872
    // Authenticate user
2873
    try {
2874
        if ($SETTINGS['ldap_type'] === 'ActiveDirectory') {
2875
            $connection->auth()->attempt($login, $password, $stayAuthenticated = true);
2876
        } else {
2877
            $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);
2878
        }
2879
    } catch (\LdapRecord\Auth\BindException $e) {
2880
        $error = $e->getDetailedError();
2881
        echo 'Error : '.$error->getErrorCode().' - '.$error->getErrorMessage(). '<br>'.$error->getDiagnosticMessage();
2882
        return false;
2883
    }
2884
2885
    return true;
2886
}
2887
2888
/**
2889
 * Removes from DB all sharekeys of this user
2890
 *
2891
 * @param int $userId User's id
2892
 * @param array   $SETTINGS Teampass settings
2893
 *
2894
 * @return bool
2895
 */
2896
function deleteUserObjetsKeys(int $userId, array $SETTINGS = []): bool
2897
{
2898
    // Load class DB
2899
    loadClasses('DB');
2900
2901
    // Remove all item sharekeys items
2902
    // expect if personal item
2903
    DB::delete(
2904
        prefixTable('sharekeys_items'),
2905
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2906
        $userId
2907
    );
2908
    // Remove all item sharekeys files
2909
    DB::delete(
2910
        prefixTable('sharekeys_files'),
2911
        'user_id = %i AND object_id NOT IN (
2912
            SELECT f.id 
2913
            FROM ' . prefixTable('items') . ' AS i 
2914
            INNER JOIN ' . prefixTable('files') . ' AS f ON f.id_item = i.id
2915
            WHERE i.perso = 1
2916
        )',
2917
        $userId
2918
    );
2919
    // Remove all item sharekeys fields
2920
    DB::delete(
2921
        prefixTable('sharekeys_fields'),
2922
        'user_id = %i AND object_id NOT IN (
2923
            SELECT c.id 
2924
            FROM ' . prefixTable('items') . ' AS i 
2925
            INNER JOIN ' . prefixTable('categories_items') . ' AS c ON c.item_id = i.id
2926
            WHERE i.perso = 1
2927
        )',
2928
        $userId
2929
    );
2930
    // Remove all item sharekeys logs
2931
    DB::delete(
2932
        prefixTable('sharekeys_logs'),
2933
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2934
        $userId
2935
    );
2936
    // Remove all item sharekeys suggestions
2937
    DB::delete(
2938
        prefixTable('sharekeys_suggestions'),
2939
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2940
        $userId
2941
    );
2942
    return false;
2943
}
2944
2945
/**
2946
 * Manage list of timezones   $SETTINGS Teampass settings
2947
 *
2948
 * @return array
2949
 */
2950
function timezone_list()
2951
{
2952
    static $timezones = null;
2953
    if ($timezones === null) {
2954
        $timezones = [];
2955
        $offsets = [];
2956
        $now = new DateTime('now', new DateTimeZone('UTC'));
2957
        foreach (DateTimeZone::listIdentifiers() as $timezone) {
2958
            $now->setTimezone(new DateTimeZone($timezone));
2959
            $offsets[] = $offset = $now->getOffset();
2960
            $timezones[$timezone] = '(' . format_GMT_offset($offset) . ') ' . format_timezone_name($timezone);
2961
        }
2962
2963
        array_multisort($offsets, $timezones);
2964
    }
2965
2966
    return $timezones;
2967
}
2968
2969
/**
2970
 * Provide timezone offset
2971
 *
2972
 * @param int $offset Timezone offset
2973
 *
2974
 * @return string
2975
 */
2976
function format_GMT_offset($offset): string
2977
{
2978
    $hours = intval($offset / 3600);
2979
    $minutes = abs(intval($offset % 3600 / 60));
2980
    return 'GMT' . ($offset ? sprintf('%+03d:%02d', $hours, $minutes) : '');
2981
}
2982
2983
/**
2984
 * Provides timezone name
2985
 *
2986
 * @param string $name Timezone name
2987
 *
2988
 * @return string
2989
 */
2990
function format_timezone_name($name): string
2991
{
2992
    $name = str_replace('/', ', ', $name);
2993
    $name = str_replace('_', ' ', $name);
2994
2995
    return str_replace('St ', 'St. ', $name);
2996
}
2997
2998
/**
2999
 * Provides info if user should use MFA based on roles
3000
 *
3001
 * @param string $userRolesIds  User roles ids
3002
 * @param string $mfaRoles      Roles for which MFA is requested
3003
 *
3004
 * @return bool
3005
 */
3006
function mfa_auth_requested_roles(string $userRolesIds, string $mfaRoles): bool
3007
{
3008
    if (empty($mfaRoles) === true) {
3009
        return true;
3010
    }
3011
3012
    $mfaRoles = array_values(json_decode($mfaRoles, true));
3013
    $userRolesIds = array_filter(explode(';', $userRolesIds));
3014
    if (count($mfaRoles) === 0 || count(array_intersect($mfaRoles, $userRolesIds)) > 0) {
3015
        return true;
3016
    }
3017
3018
    return false;
3019
}
3020
3021
/**
3022
 * Permits to clean a string for export purpose
3023
 *
3024
 * @param string $text
3025
 * @param bool $emptyCheckOnly
3026
 * 
3027
 * @return string
3028
 */
3029
function cleanStringForExport(string $text, bool $emptyCheckOnly = false): string
3030
{
3031
    if (is_null($text) === true || empty($text) === true) {
3032
        return '';
3033
    }
3034
    // only expected to check if $text was empty
3035
    elseif ($emptyCheckOnly === true) {
3036
        return $text;
3037
    }
3038
3039
    return strip_tags(
3040
        cleanString(
3041
            html_entity_decode($text, ENT_QUOTES | ENT_XHTML, 'UTF-8'),
3042
            true)
3043
        );
3044
}
3045
3046
/**
3047
 * Permits to check if user ID is valid
3048
 *
3049
 * @param integer $post_user_id
3050
 * @return bool
3051
 */
3052
function isUserIdValid($userId): bool
3053
{
3054
    if (is_null($userId) === false
3055
        && isset($userId) === true
3056
        && empty($userId) === false
3057
    ) {
3058
        return true;
3059
    }
3060
    return false;
3061
}
3062
3063
/**
3064
 * Check if a key exists and if its value equal the one expected
3065
 *
3066
 * @param string $key
3067
 * @param integer|string $value
3068
 * @param array $array
3069
 * 
3070
 * @return boolean
3071
 */
3072
function isKeyExistingAndEqual(
3073
    string $key,
3074
    /*PHP8 - integer|string*/$value,
3075
    array $array
3076
): bool
3077
{
3078
    if (isset($array[$key]) === true
3079
        && (is_int($value) === true ?
3080
            (int) $array[$key] === $value :
3081
            (string) $array[$key] === $value)
3082
    ) {
3083
        return true;
3084
    }
3085
    return false;
3086
}
3087
3088
/**
3089
 * Check if a variable is not set or equal to a value
3090
 *
3091
 * @param string|null $var
3092
 * @param integer|string $value
3093
 * 
3094
 * @return boolean
3095
 */
3096
function isKeyNotSetOrEqual(
3097
    /*PHP8 - string|null*/$var,
3098
    /*PHP8 - integer|string*/$value
3099
): bool
3100
{
3101
    if (isset($var) === false
3102
        || (is_int($value) === true ?
3103
            (int) $var === $value :
3104
            (string) $var === $value)
3105
    ) {
3106
        return true;
3107
    }
3108
    return false;
3109
}
3110
3111
/**
3112
 * Check if a key exists and if its value < to the one expected
3113
 *
3114
 * @param string $key
3115
 * @param integer $value
3116
 * @param array $array
3117
 * 
3118
 * @return boolean
3119
 */
3120
function isKeyExistingAndInferior(string $key, int $value, array $array): bool
3121
{
3122
    if (isset($array[$key]) === true && (int) $array[$key] < $value) {
3123
        return true;
3124
    }
3125
    return false;
3126
}
3127
3128
/**
3129
 * Check if a key exists and if its value > to the one expected
3130
 *
3131
 * @param string $key
3132
 * @param integer $value
3133
 * @param array $array
3134
 * 
3135
 * @return boolean
3136
 */
3137
function isKeyExistingAndSuperior(string $key, int $value, array $array): bool
3138
{
3139
    if (isset($array[$key]) === true && (int) $array[$key] > $value) {
3140
        return true;
3141
    }
3142
    return false;
3143
}
3144
3145
/**
3146
 * Check if values in array are set
3147
 * Return true if all set
3148
 * Return false if one of them is not set
3149
 *
3150
 * @param array $arrayOfValues
3151
 * @return boolean
3152
 */
3153
function isSetArrayOfValues(array $arrayOfValues): bool
3154
{
3155
    foreach($arrayOfValues as $value) {
3156
        if (isset($value) === false) {
3157
            return false;
3158
        }
3159
    }
3160
    return true;
3161
}
3162
3163
/**
3164
 * Check if values in array are set
3165
 * Return true if all set
3166
 * Return false if one of them is not set
3167
 *
3168
 * @param array $arrayOfValues
3169
 * @param integer|string $value
3170
 * @return boolean
3171
 */
3172
function isArrayOfVarsEqualToValue(
3173
    array $arrayOfVars,
3174
    /*PHP8 - integer|string*/$value
3175
) : bool
3176
{
3177
    foreach($arrayOfVars as $variable) {
3178
        if ($variable !== $value) {
3179
            return false;
3180
        }
3181
    }
3182
    return true;
3183
}
3184
3185
/**
3186
 * Checks if at least one variable in array is equal to value
3187
 *
3188
 * @param array $arrayOfValues
3189
 * @param integer|string $value
3190
 * @return boolean
3191
 */
3192
function isOneVarOfArrayEqualToValue(
3193
    array $arrayOfVars,
3194
    /*PHP8 - integer|string*/$value
3195
) : bool
3196
{
3197
    foreach($arrayOfVars as $variable) {
3198
        if ($variable === $value) {
3199
            return true;
3200
        }
3201
    }
3202
    return false;
3203
}
3204
3205
/**
3206
 * Checks is value is null, not set OR empty
3207
 *
3208
 * @param string|int|null $value
3209
 * @return boolean
3210
 */
3211
function isValueSetNullEmpty(/*PHP8 - string|int|null*/ $value) : bool
3212
{
3213
    if (is_null($value) === true || isset($value) === false || empty($value) === true) {
3214
        return true;
3215
    }
3216
    return false;
3217
}
3218
3219
/**
3220
 * Checks if value is set and if empty is equal to passed boolean
3221
 *
3222
 * @param string|int $value
3223
 * @param boolean $boolean
3224
 * @return boolean
3225
 */
3226
function isValueSetEmpty($value, $boolean = true) : bool
3227
{
3228
    if (isset($value) === true && empty($value) === $boolean) {
3229
        return true;
3230
    }
3231
    return false;
3232
}
3233
3234
/**
3235
 * Ensure Complexity is translated
3236
 *
3237
 * @return void
3238
 */
3239
function defineComplexity() : void
3240
{
3241
    // Load user's language
3242
    $lang = new Language(); 
3243
    
3244
    if (defined('TP_PW_COMPLEXITY') === false) {
3245
        define(
3246
            'TP_PW_COMPLEXITY',
3247
            [
3248
                TP_PW_STRENGTH_1 => array(TP_PW_STRENGTH_1, $lang->get('complex_level1'), 'fas fa-thermometer-empty text-danger'),
3249
                TP_PW_STRENGTH_2 => array(TP_PW_STRENGTH_2, $lang->get('complex_level2'), 'fas fa-thermometer-quarter text-warning'),
3250
                TP_PW_STRENGTH_3 => array(TP_PW_STRENGTH_3, $lang->get('complex_level3'), 'fas fa-thermometer-half text-warning'),
3251
                TP_PW_STRENGTH_4 => array(TP_PW_STRENGTH_4, $lang->get('complex_level4'), 'fas fa-thermometer-three-quarters text-success'),
3252
                TP_PW_STRENGTH_5 => array(TP_PW_STRENGTH_5, $lang->get('complex_level5'), 'fas fa-thermometer-full text-success'),
3253
            ]
3254
        );
3255
    }
3256
}
3257
3258
/**
3259
 * Uses Sanitizer to perform data sanitization
3260
 *
3261
 * @param array     $data
3262
 * @param array     $filters
3263
 * @return array|string
3264
 */
3265
function dataSanitizer(array $data, array $filters): array|string
3266
{
3267
    // Load Sanitizer library
3268
    $sanitizer = new Sanitizer($data, $filters);
3269
3270
    // Load AntiXSS
3271
    $antiXss = new AntiXSS();
3272
3273
    // Sanitize post and get variables
3274
    return $antiXss->xss_clean($sanitizer->sanitize());
3275
}
3276
3277
/**
3278
 * Permits to manage the cache tree for a user
3279
 *
3280
 * @param integer $user_id
3281
 * @param string $data
3282
 * @param array $SETTINGS
3283
 * @param string $field_update
3284
 * @return void
3285
 */
3286
function cacheTreeUserHandler(int $user_id, string $data, array $SETTINGS, string $field_update = '')
3287
{
3288
    // Load class DB
3289
    loadClasses('DB');
3290
3291
    // Exists ?
3292
    $userCacheId = DB::queryfirstrow(
3293
        'SELECT increment_id
3294
        FROM ' . prefixTable('cache_tree') . '
3295
        WHERE user_id = %i',
3296
        $user_id
3297
    );
3298
    
3299
    if (is_null($userCacheId) === true || count($userCacheId) === 0) {
3300
        // insert in table
3301
        DB::insert(
3302
            prefixTable('cache_tree'),
3303
            array(
3304
                'data' => $data,
3305
                'timestamp' => time(),
3306
                'user_id' => $user_id,
3307
                'visible_folders' => '',
3308
            )
3309
        );
3310
    } else {
3311
        if (empty($field_update) === true) {
3312
            DB::update(
3313
                prefixTable('cache_tree'),
3314
                [
3315
                    'timestamp' => time(),
3316
                    'data' => $data,
3317
                ],
3318
                'increment_id = %i',
3319
                $userCacheId['increment_id']
3320
            );
3321
        /* USELESS
3322
        } else {
3323
            DB::update(
3324
                prefixTable('cache_tree'),
3325
                [
3326
                    $field_update => $data,
3327
                ],
3328
                'increment_id = %i',
3329
                $userCacheId['increment_id']
3330
            );*/
3331
        }
3332
    }
3333
}
3334
3335
/**
3336
 * Permits to calculate a %
3337
 *
3338
 * @param float $nombre
3339
 * @param float $total
3340
 * @param float $pourcentage
3341
 * @return float
3342
 */
3343
function pourcentage(float $nombre, float $total, float $pourcentage): float
3344
{ 
3345
    $resultat = ($nombre/$total) * $pourcentage;
3346
    return round($resultat);
3347
}
3348
3349
/**
3350
 * Load the folders list from the cache
3351
 *
3352
 * @param string $fieldName
3353
 * @param string $sessionName
3354
 * @param boolean $forceRefresh
3355
 * @return array
3356
 */
3357
function loadFoldersListByCache(
3358
    string $fieldName,
3359
    string $sessionName,
3360
    bool $forceRefresh = false
3361
): array
3362
{
3363
    // Case when refresh is EXPECTED / MANDATORY
3364
    if ($forceRefresh === true) {
3365
        return [
3366
            'state' => false,
3367
            'data' => [],
3368
        ];
3369
    }
3370
    
3371
    $session = SessionManager::getSession();
3372
3373
    // Get last folder update
3374
    $lastFolderChange = DB::queryfirstrow(
3375
        'SELECT valeur FROM ' . prefixTable('misc') . '
3376
        WHERE type = %s AND intitule = %s',
3377
        'timestamp',
3378
        'last_folder_change'
3379
    );
3380
    if (DB::count() === 0) {
3381
        $lastFolderChange['valeur'] = 0;
3382
    }
3383
3384
    // Case when an update in the tree has been done
3385
    // Refresh is then mandatory
3386
    if ((int) $lastFolderChange['valeur'] > (int) (null !== $session->get('user-tree_last_refresh_timestamp') ? $session->get('user-tree_last_refresh_timestamp') : 0)) {
3387
        return [
3388
            'state' => false,
3389
            'data' => [],
3390
        ];
3391
    }
3392
3393
    // Does this user has the tree structure in session?
3394
    // If yes then use it
3395
    if (count(null !== $session->get('user-folders_list') ? $session->get('user-folders_list') : []) > 0) {
3396
        return [
3397
            'state' => true,
3398
            'data' => json_encode($session->get('user-folders_list')),
3399
        ];
3400
    }
3401
3402
    // Does this user has a tree cache
3403
    $userCacheTree = DB::queryfirstrow(
3404
        'SELECT '.$fieldName.'
3405
        FROM ' . prefixTable('cache_tree') . '
3406
        WHERE user_id = %i',
3407
        $session->get('user-id')
3408
    );
3409
    if (empty($userCacheTree[$fieldName]) === false && $userCacheTree[$fieldName] !== '[]') {
3410
        SessionManager::addRemoveFromSessionAssociativeArray(
3411
            'user-folders_list',
3412
            [$userCacheTree[$fieldName]],
3413
            'add'
3414
        );
3415
        return [
3416
            'state' => true,
3417
            'data' => $userCacheTree[$fieldName],
3418
        ];
3419
    }
3420
3421
    return [
3422
        'state' => false,
3423
        'data' => [],
3424
    ];
3425
}
3426
3427
3428
/**
3429
 * Permits to refresh the categories of folders
3430
 *
3431
 * @param array $folderIds
3432
 * @return void
3433
 */
3434
function handleFoldersCategories(
3435
    array $folderIds
3436
)
3437
{
3438
    // Load class DB
3439
    loadClasses('DB');
3440
3441
    $arr_data = array();
3442
3443
    // force full list of folders
3444
    if (count($folderIds) === 0) {
3445
        $folderIds = DB::queryFirstColumn(
3446
            'SELECT id
3447
            FROM ' . prefixTable('nested_tree') . '
3448
            WHERE personal_folder=%i',
3449
            0
3450
        );
3451
    }
3452
3453
    // Get complexity
3454
    defineComplexity();
3455
3456
    // update
3457
    foreach ($folderIds as $folder) {
3458
        // Do we have Categories
3459
        // get list of associated Categories
3460
        $arrCatList = array();
3461
        $rows_tmp = DB::query(
3462
            'SELECT c.id, c.title, c.level, c.type, c.masked, c.order, c.encrypted_data, c.role_visibility, c.is_mandatory,
3463
            f.id_category AS category_id
3464
            FROM ' . prefixTable('categories_folders') . ' AS f
3465
            INNER JOIN ' . prefixTable('categories') . ' AS c ON (f.id_category = c.parent_id)
3466
            WHERE id_folder=%i',
3467
            $folder
3468
        );
3469
        if (DB::count() > 0) {
3470
            foreach ($rows_tmp as $row) {
3471
                $arrCatList[$row['id']] = array(
3472
                    'id' => $row['id'],
3473
                    'title' => $row['title'],
3474
                    'level' => $row['level'],
3475
                    'type' => $row['type'],
3476
                    'masked' => $row['masked'],
3477
                    'order' => $row['order'],
3478
                    'encrypted_data' => $row['encrypted_data'],
3479
                    'role_visibility' => $row['role_visibility'],
3480
                    'is_mandatory' => $row['is_mandatory'],
3481
                    'category_id' => $row['category_id'],
3482
                );
3483
            }
3484
        }
3485
        $arr_data['categories'] = $arrCatList;
3486
3487
        // Now get complexity
3488
        $valTemp = '';
3489
        $data = DB::queryFirstRow(
3490
            'SELECT valeur
3491
            FROM ' . prefixTable('misc') . '
3492
            WHERE type = %s AND intitule=%i',
3493
            'complex',
3494
            $folder
3495
        );
3496
        if (DB::count() > 0 && empty($data['valeur']) === false) {
3497
            $valTemp = array(
3498
                'value' => $data['valeur'],
3499
                'text' => TP_PW_COMPLEXITY[$data['valeur']][1],
3500
            );
3501
        }
3502
        $arr_data['complexity'] = $valTemp;
3503
3504
        // Now get Roles
3505
        $valTemp = '';
3506
        $rows_tmp = DB::query(
3507
            'SELECT t.title
3508
            FROM ' . prefixTable('roles_values') . ' as v
3509
            INNER JOIN ' . prefixTable('roles_title') . ' as t ON (v.role_id = t.id)
3510
            WHERE v.folder_id = %i
3511
            GROUP BY title',
3512
            $folder
3513
        );
3514
        foreach ($rows_tmp as $record) {
3515
            $valTemp .= (empty($valTemp) === true ? '' : ' - ') . $record['title'];
3516
        }
3517
        $arr_data['visibilityRoles'] = $valTemp;
3518
3519
        // now save in DB
3520
        DB::update(
3521
            prefixTable('nested_tree'),
3522
            array(
3523
                'categories' => json_encode($arr_data),
3524
            ),
3525
            'id = %i',
3526
            $folder
3527
        );
3528
    }
3529
}
3530
3531
/**
3532
 * List all users that have specific roles
3533
 *
3534
 * @param array $roles
3535
 * @return array
3536
 */
3537
function getUsersWithRoles(
3538
    array $roles
3539
): array
3540
{
3541
    $session = SessionManager::getSession();
3542
    $arrUsers = array();
3543
3544
    foreach ($roles as $role) {
3545
        // loop on users and check if user has this role
3546
        $rows = DB::query(
3547
            'SELECT id, fonction_id
3548
            FROM ' . prefixTable('users') . '
3549
            WHERE id != %i AND admin = 0 AND fonction_id IS NOT NULL AND fonction_id != ""',
3550
            $session->get('user-id')
3551
        );
3552
        foreach ($rows as $user) {
3553
            $userRoles = is_null($user['fonction_id']) === false && empty($user['fonction_id']) === false ? explode(';', $user['fonction_id']) : [];
3554
            if (in_array($role, $userRoles, true) === true) {
3555
                array_push($arrUsers, $user['id']);
3556
            }
3557
        }
3558
    }
3559
3560
    return $arrUsers;
3561
}
3562
3563
3564
/**
3565
 * Get all users informations
3566
 *
3567
 * @param integer $userId
3568
 * @return array
3569
 */
3570
function getFullUserInfos(
3571
    int $userId
3572
): array
3573
{
3574
    if (empty($userId) === true) {
3575
        return array();
3576
    }
3577
3578
    $val = DB::queryfirstrow(
3579
        'SELECT *
3580
        FROM ' . prefixTable('users') . '
3581
        WHERE id = %i',
3582
        $userId
3583
    );
3584
3585
    return $val;
3586
}
3587
3588
/**
3589
 * Is required an upgrade
3590
 *
3591
 * @return boolean
3592
 */
3593
function upgradeRequired(): bool
3594
{
3595
    // Get settings.php
3596
    include_once __DIR__. '/../includes/config/settings.php';
3597
3598
    // Get timestamp in DB
3599
    $val = DB::queryfirstrow(
3600
        'SELECT valeur
3601
        FROM ' . prefixTable('misc') . '
3602
        WHERE type = %s AND intitule = %s',
3603
        'admin',
3604
        'upgrade_timestamp'
3605
    );
3606
    
3607
    // if not exists then error
3608
    if (is_null($val) === true || count($val) === 0 || defined('UPGRADE_MIN_DATE') === false) return true;
3609
3610
    // if empty or too old then error
3611
    if (empty($val['valeur']) === true || (int) $val['valeur'] < (int) UPGRADE_MIN_DATE) {
3612
        return true;
3613
    }
3614
3615
    return false;
3616
}
3617
3618
/**
3619
 * Permits to change the user keys on his demand
3620
 *
3621
 * @param integer $userId
3622
 * @param string $passwordClear
3623
 * @param integer $nbItemsToTreat
3624
 * @param string $encryptionKey
3625
 * @param boolean $deleteExistingKeys
3626
 * @param boolean $sendEmailToUser
3627
 * @param boolean $encryptWithUserPassword
3628
 * @param boolean $generate_user_new_password
3629
 * @param string $emailBody
3630
 * @param boolean $user_self_change
3631
 * @param string $recovery_public_key
3632
 * @param string $recovery_private_key
3633
 * @return string
3634
 */
3635
function handleUserKeys(
3636
    int $userId,
3637
    string $passwordClear,
3638
    int $nbItemsToTreat,
3639
    string $encryptionKey = '',
3640
    bool $deleteExistingKeys = false,
3641
    bool $sendEmailToUser = true,
3642
    bool $encryptWithUserPassword = false,
3643
    bool $generate_user_new_password = false,
3644
    string $emailBody = '',
3645
    bool $user_self_change = false,
3646
    string $recovery_public_key = '',
3647
    string $recovery_private_key = ''
3648
): string
3649
{
3650
    $session = SessionManager::getSession();
3651
    $lang = new Language(); 
3652
3653
    // prepapre background tasks for item keys generation        
3654
    $userTP = DB::queryFirstRow(
3655
        'SELECT pw, public_key, private_key
3656
        FROM ' . prefixTable('users') . '
3657
        WHERE id = %i',
3658
        TP_USER_ID
3659
    );
3660
    if (DB::count() === 0) {
3661
        return prepareExchangedData(
3662
            array(
3663
                'error' => true,
3664
                'message' => 'User not exists',
3665
            ),
3666
            'encode'
3667
        );
3668
    }
3669
3670
    // Do we need to generate new user password
3671
    if ($generate_user_new_password === true) {
3672
        // Generate a new password
3673
        $passwordClear = GenerateCryptKey(20, false, true, true, false, true);
3674
    }
3675
3676
    // Hash the password
3677
    $pwdlib = new PasswordLib();
3678
    $hashedPassword = $pwdlib->createPasswordHash($passwordClear);
3679
    if ($pwdlib->verifyPasswordHash($passwordClear, $hashedPassword) === false) {
3680
        return prepareExchangedData(
3681
            array(
3682
                'error' => true,
3683
                'message' => $lang->get('pw_hash_not_correct'),
3684
            ),
3685
            'encode'
3686
        );
3687
    }
3688
3689
    // Generate new keys
3690
    if ($user_self_change === true && empty($recovery_public_key) === false && empty($recovery_private_key) === false){
3691
        $userKeys = [
3692
            'public_key' => $recovery_public_key,
3693
            'private_key_clear' => $recovery_private_key,
3694
            'private_key' => encryptPrivateKey($passwordClear, $recovery_private_key),
3695
        ];
3696
    } else {
3697
        $userKeys = generateUserKeys($passwordClear);
3698
    }
3699
3700
    // Save in DB
3701
    DB::update(
3702
        prefixTable('users'),
3703
        array(
3704
            'pw' => $hashedPassword,
3705
            'public_key' => $userKeys['public_key'],
3706
            'private_key' => $userKeys['private_key'],
3707
        ),
3708
        'id=%i',
3709
        $userId
3710
    );
3711
3712
    // update session too
3713
    if ($userId === $session->get('user-id')) {
3714
        $session->set('user-private_key', $userKeys['private_key_clear']);
3715
        $session->set('user-public_key', $userKeys['public_key']);
3716
    }
3717
3718
    // Manage empty encryption key
3719
    // Let's take the user's password if asked and if no encryption key provided
3720
    $encryptionKey = $encryptWithUserPassword === true && empty($encryptionKey) === true ? $passwordClear : $encryptionKey;
3721
3722
    // Create process
3723
    DB::insert(
3724
        prefixTable('processes'),
3725
        array(
3726
            'created_at' => time(),
3727
            'process_type' => 'create_user_keys',
3728
            'arguments' => json_encode([
3729
                'new_user_id' => (int) $userId,
3730
                'new_user_pwd' => cryption($passwordClear, '','encrypt')['string'],
3731
                'new_user_code' => cryption(empty($encryptionKey) === true ? uniqidReal(20) : $encryptionKey, '','encrypt')['string'],
3732
                'owner_id' => (int) TP_USER_ID,
3733
                'creator_pwd' => $userTP['pw'],
3734
                'send_email' => $sendEmailToUser === true ? 1 : 0,
3735
                'otp_provided_new_value' => 1,
3736
                'email_body' => empty($emailBody) === true ? '' : $lang->get($emailBody),
3737
                'user_self_change' => $user_self_change === true ? 1 : 0,
3738
            ]),
3739
            'updated_at' => '',
3740
            'finished_at' => '',
3741
            'output' => '',
3742
        )
3743
    );
3744
    $processId = DB::insertId();
3745
3746
    // Delete existing keys
3747
    if ($deleteExistingKeys === true) {
3748
        deleteUserObjetsKeys(
3749
            (int) $userId,
3750
        );
3751
    }
3752
3753
    // Create tasks
3754
    createUserTasks($processId, $nbItemsToTreat);
3755
3756
    // update user's new status
3757
    DB::update(
3758
        prefixTable('users'),
3759
        [
3760
            'is_ready_for_usage' => 0,
3761
            'otp_provided' => 1,
3762
            'ongoing_process_id' => $processId,
3763
            'special' => 'generate-keys',
3764
        ],
3765
        'id=%i',
3766
        $userId
3767
    );
3768
3769
    return prepareExchangedData(
3770
        array(
3771
            'error' => false,
3772
            'message' => '',
3773
        ),
3774
        'encode'
3775
    );
3776
}
3777
3778
/**
3779
 * Permits to generate a new password for a user
3780
 *
3781
 * @param integer $processId
3782
 * @param integer $nbItemsToTreat
3783
 * @return void
3784
 
3785
 */
3786
function createUserTasks($processId, $nbItemsToTreat): void
3787
{
3788
    DB::insert(
3789
        prefixTable('processes_tasks'),
3790
        array(
3791
            'process_id' => $processId,
3792
            'created_at' => time(),
3793
            'task' => json_encode([
3794
                'step' => 'step0',
3795
                'index' => 0,
3796
                'nb' => $nbItemsToTreat,
3797
            ]),
3798
        )
3799
    );
3800
3801
    DB::insert(
3802
        prefixTable('processes_tasks'),
3803
        array(
3804
            'process_id' => $processId,
3805
            'created_at' => time(),
3806
            'task' => json_encode([
3807
                'step' => 'step10',
3808
                'index' => 0,
3809
                'nb' => $nbItemsToTreat,
3810
            ]),
3811
        )
3812
    );
3813
3814
    DB::insert(
3815
        prefixTable('processes_tasks'),
3816
        array(
3817
            'process_id' => $processId,
3818
            'created_at' => time(),
3819
            'task' => json_encode([
3820
                'step' => 'step20',
3821
                'index' => 0,
3822
                'nb' => $nbItemsToTreat,
3823
            ]),
3824
        )
3825
    );
3826
3827
    DB::insert(
3828
        prefixTable('processes_tasks'),
3829
        array(
3830
            'process_id' => $processId,
3831
            'created_at' => time(),
3832
            'task' => json_encode([
3833
                'step' => 'step30',
3834
                'index' => 0,
3835
                'nb' => $nbItemsToTreat,
3836
            ]),
3837
        )
3838
    );
3839
3840
    DB::insert(
3841
        prefixTable('processes_tasks'),
3842
        array(
3843
            'process_id' => $processId,
3844
            'created_at' => time(),
3845
            'task' => json_encode([
3846
                'step' => 'step40',
3847
                'index' => 0,
3848
                'nb' => $nbItemsToTreat,
3849
            ]),
3850
        )
3851
    );
3852
3853
    DB::insert(
3854
        prefixTable('processes_tasks'),
3855
        array(
3856
            'process_id' => $processId,
3857
            'created_at' => time(),
3858
            'task' => json_encode([
3859
                'step' => 'step50',
3860
                'index' => 0,
3861
                'nb' => $nbItemsToTreat,
3862
            ]),
3863
        )
3864
    );
3865
3866
    DB::insert(
3867
        prefixTable('processes_tasks'),
3868
        array(
3869
            'process_id' => $processId,
3870
            'created_at' => time(),
3871
            'task' => json_encode([
3872
                'step' => 'step60',
3873
                'index' => 0,
3874
                'nb' => $nbItemsToTreat,
3875
            ]),
3876
        )
3877
    );
3878
}
3879
3880
/**
3881
 * Permeits to check the consistency of date versus columns definition
3882
 *
3883
 * @param string $table
3884
 * @param array $dataFields
3885
 * @return array
3886
 */
3887
function validateDataFields(
3888
    string $table,
3889
    array $dataFields
3890
): array
3891
{
3892
    // Get table structure
3893
    $result = DB::query(
3894
        "SELECT `COLUMN_NAME`, `CHARACTER_MAXIMUM_LENGTH` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%l' AND TABLE_NAME = '%l';",
3895
        DB_NAME,
3896
        $table
3897
    );
3898
3899
    foreach ($result as $row) {
3900
        $field = $row['COLUMN_NAME'];
3901
        $maxLength = is_null($row['CHARACTER_MAXIMUM_LENGTH']) === false ? (int) $row['CHARACTER_MAXIMUM_LENGTH'] : '';
3902
3903
        if (isset($dataFields[$field]) === true && is_array($dataFields[$field]) === false && empty($maxLength) === false) {
3904
            if (strlen((string) $dataFields[$field]) > $maxLength) {
3905
                return [
3906
                    'state' => false,
3907
                    'field' => $field,
3908
                    'maxLength' => $maxLength,
3909
                    'currentLength' => strlen((string) $dataFields[$field]),
3910
                ];
3911
            }
3912
        }
3913
    }
3914
    
3915
    return [
3916
        'state' => true,
3917
        'message' => '',
3918
    ];
3919
}
3920
3921
/**
3922
 * Adapt special characters sanitized during filter_var with option FILTER_SANITIZE_SPECIAL_CHARS operation
3923
 *
3924
 * @param string $string
3925
 * @return string
3926
 */
3927
function filterVarBack(string $string): string
3928
{
3929
    $arr = [
3930
        '&#060;' => '<',
3931
        '&#062;' => '>',
3932
        '&#034;' => '"',
3933
        '&#039;' => "'",
3934
        '&#038;' => '&',
3935
    ];
3936
3937
    foreach ($arr as $key => $value) {
3938
        $string = str_replace($key, $value, $string);
3939
    }
3940
3941
    return $string;
3942
}
3943
3944
/**
3945
 * 
3946
 */
3947
function storeTask(
3948
    string $taskName,
3949
    int $user_id,
3950
    int $is_personal_folder,
3951
    int $folder_destination_id,
3952
    int $item_id,
3953
    string $object_keys
3954
)
3955
{
3956
    if (in_array($taskName, ['item_copy', 'new_item', 'update_item'])) {
3957
        // Create process
3958
        DB::insert(
3959
            prefixTable('processes'),
3960
            array(
3961
                'created_at' => time(),
3962
                'process_type' => $taskName,
3963
                'arguments' => json_encode([
3964
                    'item_id' => $item_id,
3965
                    'object_key' => $object_keys,
3966
                ]),
3967
                'updated_at' => '',
3968
                'finished_at' => '',
3969
                'output' => '',
3970
                'item_id' => $item_id,
3971
            )
3972
        );
3973
        $processId = DB::insertId();
3974
3975
        // Create tasks
3976
        // 1- Create password sharekeys for users of this new ITEM
3977
        DB::insert(
3978
            prefixTable('processes_tasks'),
3979
            array(
3980
                'process_id' => $processId,
3981
                'created_at' => time(),
3982
                'task' => json_encode([
3983
                    'step' => 'create_users_pwd_key',
3984
                    'index' => 0,
3985
                ]),
3986
            )
3987
        );
3988
3989
        // 2- Create fields sharekeys for users of this new ITEM
3990
        DB::insert(
3991
            prefixTable('processes_tasks'),
3992
            array(
3993
                'process_id' => $processId,
3994
                'created_at' => time(),
3995
                'task' => json_encode([
3996
                    'step' => 'create_users_fields_key',
3997
                    'index' => 0,
3998
                ]),
3999
            )
4000
        );
4001
4002
        // 3- Create files sharekeys for users of this new ITEM
4003
        DB::insert(
4004
            prefixTable('processes_tasks'),
4005
            array(
4006
                'process_id' => $processId,
4007
                'created_at' => time(),
4008
                'task' => json_encode([
4009
                    'step' => 'create_users_files_key',
4010
                    'index' => 0,
4011
                ]),
4012
            )
4013
        );
4014
    }
4015
}
4016
4017
/**
4018
 * Return PHP binary path
4019
 *
4020
 * @return string
4021
 */
4022
function getPHPBinary(): string
4023
{
4024
    // Get PHP binary path
4025
    $phpBinaryFinder = new PhpExecutableFinder();
4026
    $phpBinaryPath = $phpBinaryFinder->find();
4027
    return $phpBinaryPath === false ? 'false' : $phpBinaryPath;
4028
}
4029
4030
4031
4032
/**
4033
 * Delete unnecessary keys for personal items
4034
 *
4035
 * @param boolean $allUsers
4036
 * @param integer $user_id
4037
 * @return void
4038
 */
4039
function purgeUnnecessaryKeys(bool $allUsers = true, int $user_id=0)
4040
{
4041
    if ($allUsers === true) {
4042
        // Load class DB
4043
        if (class_exists('DB') === false) {
4044
            loadClasses('DB');
4045
        }
4046
4047
        $users = DB::query(
4048
            'SELECT id
4049
            FROM ' . prefixTable('users') . '
4050
            WHERE id NOT IN ('.OTV_USER_ID.', '.TP_USER_ID.', '.SSH_USER_ID.', '.API_USER_ID.')
4051
            ORDER BY login ASC'
4052
        );
4053
        foreach ($users as $user) {
4054
            purgeUnnecessaryKeysForUser((int) $user['id']);
4055
        }
4056
    } else {
4057
        purgeUnnecessaryKeysForUser((int) $user_id);
4058
    }
4059
}
4060
4061
/**
4062
 * Delete unnecessary keys for personal items
4063
 *
4064
 * @param integer $user_id
4065
 * @return void
4066
 */
4067
function purgeUnnecessaryKeysForUser(int $user_id=0)
4068
{
4069
    if ($user_id === 0) {
4070
        return;
4071
    }
4072
4073
    // Load class DB
4074
    loadClasses('DB');
4075
4076
    $personalItems = DB::queryFirstColumn(
4077
        'SELECT id
4078
        FROM ' . prefixTable('items') . ' AS i
4079
        INNER JOIN ' . prefixTable('log_items') . ' AS li ON li.id_item = i.id
4080
        WHERE i.perso = 1 AND li.action = "at_creation" AND li.id_user IN (%i, '.TP_USER_ID.')',
4081
        $user_id
4082
    );
4083
    if (count($personalItems) > 0) {
4084
        // Item keys
4085
        DB::delete(
4086
            prefixTable('sharekeys_items'),
4087
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4088
            $personalItems,
4089
            $user_id
4090
        );
4091
        // Files keys
4092
        DB::delete(
4093
            prefixTable('sharekeys_files'),
4094
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4095
            $personalItems,
4096
            $user_id
4097
        );
4098
        // Fields keys
4099
        DB::delete(
4100
            prefixTable('sharekeys_fields'),
4101
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4102
            $personalItems,
4103
            $user_id
4104
        );
4105
        // Logs keys
4106
        DB::delete(
4107
            prefixTable('sharekeys_logs'),
4108
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4109
            $personalItems,
4110
            $user_id
4111
        );
4112
    }
4113
}
4114
4115
/**
4116
 * Generate recovery keys file
4117
 *
4118
 * @param integer $userId
4119
 * @param array $SETTINGS
4120
 * @return string
4121
 */
4122
function handleUserRecoveryKeysDownload(int $userId, array $SETTINGS):string
4123
{
4124
    $session = SessionManager::getSession();
4125
    // Check if user exists
4126
    $userInfo = DB::queryFirstRow(
4127
        'SELECT pw, public_key, private_key, login, name
4128
        FROM ' . prefixTable('users') . '
4129
        WHERE id = %i',
4130
        $userId
4131
    );
4132
4133
    if (DB::count() > 0) {
4134
        $now = (int) time();
4135
4136
        // Prepare file content
4137
        $export_value = file_get_contents(__DIR__."/../includes/core/teampass_ascii.txt")."\n".
4138
            "Generation date: ".date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now)."\n\n".
4139
            "RECOVERY KEYS - Not to be shared - To be store safely\n\n".
4140
            "Public Key:\n".$userInfo['public_key']."\n\n".
4141
            "Private Key:\n".decryptPrivateKey($session->get('user-password'), $userInfo['private_key'])."\n\n";
4142
4143
        // Update user's keys_recovery_time
4144
        DB::update(
4145
            prefixTable('users'),
4146
            [
4147
                'keys_recovery_time' => $now,
4148
            ],
4149
            'id=%i',
4150
            $userId
4151
        );
4152
        $session->set('user-keys_recovery_time', $now);
4153
4154
        //Log into DB the user's disconnection
4155
        logEvents($SETTINGS, 'user_mngt', 'at_user_keys_download', (string) $userId, $userInfo['login']);
4156
        
4157
        // Return data
4158
        return prepareExchangedData(
4159
            array(
4160
                'error' => false,
4161
                'datetime' => date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now),
4162
                'timestamp' => $now,
4163
                'content' => base64_encode($export_value),
4164
                'login' => $userInfo['login'],
4165
            ),
4166
            'encode'
4167
        );
4168
    }
4169
4170
    return prepareExchangedData(
4171
        array(
4172
            'error' => true,
4173
            'datetime' => '',
4174
        ),
4175
        'encode'
4176
    );
4177
}
4178
4179
/**
4180
 * Permits to load expected classes
4181
 *
4182
 * @param string $className
4183
 * @return void
4184
 */
4185
function loadClasses(string $className = ''): void
4186
{
4187
    require_once __DIR__. '/../includes/config/include.php';
4188
    require_once __DIR__. '/../includes/config/settings.php';
4189
    require_once __DIR__.'/../vendor/autoload.php';
4190
4191
    if (defined('DB_PASSWD_CLEAR') === false) {
4192
        define('DB_PASSWD_CLEAR', defuseReturnDecrypted(DB_PASSWD, []));
4193
    }
4194
4195
    if (empty($className) === false) {
4196
        // Load class DB
4197
        if ((string) $className === 'DB') {
4198
            //Connect to DB
4199
            DB::$host = DB_HOST;
4200
            DB::$user = DB_USER;
4201
            DB::$password = DB_PASSWD_CLEAR;
4202
            DB::$dbName = DB_NAME;
4203
            DB::$port = DB_PORT;
4204
            DB::$encoding = DB_ENCODING;
4205
            DB::$ssl = DB_SSL;
4206
            DB::$connect_options = DB_CONNECT_OPTIONS;
4207
        }
4208
    }
4209
4210
}
4211
4212
/**
4213
 * Returns the page the user is visiting.
4214
 *
4215
 * @return string The page name
4216
 */
4217
function getCurrectPage($SETTINGS)
4218
{
4219
    
4220
    $request = SymfonyRequest::createFromGlobals();
4221
4222
    // Parse the url
4223
    parse_str(
4224
        substr(
4225
            (string) $request->getRequestUri(),
4226
            strpos((string) $request->getRequestUri(), '?') + 1
4227
        ),
4228
        $result
4229
    );
4230
4231
    return $result['page'];
4232
}
4233
4234
/**
4235
 * Permits to return value if set
4236
 *
4237
 * @param string|int $value
4238
 * @param string|int|null $retFalse
4239
 * @param string|int $retTrue
4240
 * @return mixed
4241
 */
4242
function returnIfSet($value, $retFalse = '', $retTrue = null): mixed
4243
{
4244
4245
    return isset($value) === true ? ($retTrue === null ? $value : $retTrue) : $retFalse;
4246
}