Passed
Push — master ( 7da16c...c95f2a )
by Nils
06:19
created

generateKey()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 1
eloc 1
c 1
b 0
f 0
nc 1
nop 0
dl 0
loc 3
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
 * Convert date to timestamp.
1308
 *
1309
 * @param string $date        The date
1310
 * @param string $date_format Date format
1311
 *
1312
 * @return int
1313
 */
1314
function dateToStamp(string $date, string $date_format): int
1315
{
1316
    $date = date_parse_from_format($date_format, $date);
1317
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1318
        return mktime(
1319
            empty($date['hour']) === false ? $date['hour'] : 23,
1320
            empty($date['minute']) === false ? $date['minute'] : 59,
1321
            empty($date['second']) === false ? $date['second'] : 59,
1322
            $date['month'],
1323
            $date['day'],
1324
            $date['year']
1325
        );
1326
    }
1327
    return 0;
1328
}
1329
1330
/**
1331
 * Is this a date.
1332
 *
1333
 * @param string $date Date
1334
 *
1335
 * @return bool
1336
 */
1337
function isDate(string $date): bool
1338
{
1339
    return strtotime($date) !== false;
1340
}
1341
1342
/**
1343
 * Check if isUTF8().
1344
 *
1345
 * @param string|array $string Is the string
1346
 *
1347
 * @return int is the string in UTF8 format
1348
 */
1349
function isUTF8($string): int
1350
{
1351
    if (is_array($string) === true) {
1352
        $string = $string['string'];
1353
    }
1354
1355
    return preg_match(
1356
        '%^(?:
1357
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1358
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1359
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1360
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1361
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1362
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1363
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1364
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1365
        )*$%xs',
1366
        $string
1367
    );
1368
}
1369
1370
/**
1371
 * Prepare an array to UTF8 format before JSON_encode.
1372
 *
1373
 * @param array $array Array of values
1374
 *
1375
 * @return array
1376
 */
1377
function utf8Converter(array $array): array
1378
{
1379
    array_walk_recursive(
1380
        $array,
1381
        static function (&$item): void {
1382
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1383
                $item = mb_convert_encoding($item, 'ISO-8859-1', 'UTF-8');
1384
            }
1385
        }
1386
    );
1387
    return $array;
1388
}
1389
1390
/**
1391
 * Permits to prepare data to be exchanged.
1392
 *
1393
 * @param array|string $data Text
1394
 * @param string       $type Parameter
1395
 * @param string       $key  Optional key
1396
 *
1397
 * @return string|array
1398
 */
1399
function prepareExchangedData($data, string $type, ?string $key = null)
1400
{
1401
    $session = SessionManager::getSession();
1402
    
1403
    // Perform
1404
    if ($type === 'encode' && is_array($data) === true) {
1405
        // Now encode
1406
        return Encryption::encrypt(
1407
            json_encode(
1408
                $data,
1409
                JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1410
            ),
1411
            $session->get('key')
1412
        );
1413
    }
1414
    if ($type === 'decode' && is_array($data) === false) {
1415
        // check if key exists
1416
        return json_decode(
1417
            (string) Encryption::decrypt(
1418
                (string) $data,
1419
                $session->get('key')
1420
            ),
1421
            true
1422
        );
1423
    }
1424
    return '';
1425
}
1426
1427
1428
/**
1429
 * Create a thumbnail.
1430
 *
1431
 * @param string  $src           Source
1432
 * @param string  $dest          Destination
1433
 * @param int $desired_width Size of width
1434
 * 
1435
 * @return void|string|bool
1436
 */
1437
function makeThumbnail(string $src, string $dest, int $desired_width)
1438
{
1439
    /* read the source image */
1440
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1441
        $source_image = imagecreatefrompng($src);
1442
        if ($source_image === false) {
1443
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1444
        }
1445
    } else {
1446
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1447
    }
1448
1449
    // Get height and width
1450
    $width = imagesx($source_image);
1451
    $height = imagesy($source_image);
1452
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1453
    $desired_height = (int) floor($height * $desired_width / $width);
1454
    /* create a new, "virtual" image */
1455
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1456
    if ($virtual_image === false) {
1457
        return false;
1458
    }
1459
    /* copy source image at a resized size */
1460
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1461
    /* create the physical thumbnail image to its destination */
1462
    imagejpeg($virtual_image, $dest);
1463
}
1464
1465
/**
1466
 * Check table prefix in SQL query.
1467
 *
1468
 * @param string $table Table name
1469
 * 
1470
 * @return string
1471
 */
1472
function prefixTable(string $table): string
1473
{
1474
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1475
    if (empty($safeTable) === false) {
1476
        // sanitize string
1477
        return $safeTable;
1478
    }
1479
    // stop error no table
1480
    return 'table_not_exists';
1481
}
1482
1483
/**
1484
 * GenerateCryptKey
1485
 *
1486
 * @param int     $size      Length
1487
 * @param bool $secure Secure
1488
 * @param bool $numerals Numerics
1489
 * @param bool $uppercase Uppercase letters
1490
 * @param bool $symbols Symbols
1491
 * @param bool $lowercase Lowercase
1492
 * @param array   $SETTINGS  SETTINGS
1493
 * 
1494
 * @return string
1495
 */
1496
function GenerateCryptKey(
1497
    int $size = 20,
1498
    bool $secure = false,
1499
    bool $numerals = false,
1500
    bool $uppercase = false,
1501
    bool $symbols = false,
1502
    bool $lowercase = false,
1503
    array $SETTINGS = []
1504
): string {
1505
    $generator = new ComputerPasswordGenerator();
1506
    $generator->setRandomGenerator(new Php7RandomGenerator());
1507
    
1508
    // Manage size
1509
    $generator->setLength((int) $size);
1510
    if ($secure === true) {
1511
        $generator->setSymbols(true);
1512
        $generator->setLowercase(true);
1513
        $generator->setUppercase(true);
1514
        $generator->setNumbers(true);
1515
    } else {
1516
        $generator->setLowercase($lowercase);
1517
        $generator->setUppercase($uppercase);
1518
        $generator->setNumbers($numerals);
1519
        $generator->setSymbols($symbols);
1520
    }
1521
1522
    return $generator->generatePasswords()[0];
1523
}
1524
1525
/**
1526
 * Send sysLOG message
1527
 *
1528
 * @param string    $message
1529
 * @param string    $host
1530
 * @param int       $port
1531
 * @param string    $component
1532
 * 
1533
 * @return void
1534
*/
1535
function send_syslog($message, $host, $port, $component = 'teampass'): void
1536
{
1537
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1538
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1539
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1540
    socket_close($sock);
1541
}
1542
1543
/**
1544
 * Permits to log events into DB
1545
 *
1546
 * @param array  $SETTINGS Teampass settings
1547
 * @param string $type     Type
1548
 * @param string $label    Label
1549
 * @param string $who      Who
1550
 * @param string $login    Login
1551
 * @param string|int $field_1  Field
1552
 * 
1553
 * @return void
1554
 */
1555
function logEvents(
1556
    array $SETTINGS, 
1557
    string $type, 
1558
    string $label, 
1559
    string $who, 
1560
    ?string $login = null, 
1561
    $field_1 = null
1562
): void
1563
{
1564
    if (empty($who)) {
1565
        $who = getClientIpServer();
1566
    }
1567
1568
    // Load class DB
1569
    loadClasses('DB');
1570
1571
    DB::insert(
1572
        prefixTable('log_system'),
1573
        [
1574
            'type' => $type,
1575
            'date' => time(),
1576
            'label' => $label,
1577
            'qui' => $who,
1578
            'field_1' => $field_1 === null ? '' : $field_1,
1579
        ]
1580
    );
1581
    // If SYSLOG
1582
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1583
        if ($type === 'user_mngt') {
1584
            send_syslog(
1585
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1586
                $SETTINGS['syslog_host'],
1587
                $SETTINGS['syslog_port'],
1588
                'teampass'
1589
            );
1590
        } else {
1591
            send_syslog(
1592
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1593
                $SETTINGS['syslog_host'],
1594
                $SETTINGS['syslog_port'],
1595
                'teampass'
1596
            );
1597
        }
1598
    }
1599
}
1600
1601
/**
1602
 * Log events.
1603
 *
1604
 * @param array  $SETTINGS        Teampass settings
1605
 * @param int    $item_id         Item id
1606
 * @param string $item_label      Item label
1607
 * @param int    $id_user         User id
1608
 * @param string $action          Code for reason
1609
 * @param string $login           User login
1610
 * @param string $raison          Code for reason
1611
 * @param string $encryption_type Encryption on
1612
 * @param string $time Encryption Time
1613
 * @param string $old_value       Old value
1614
 * 
1615
 * @return void
1616
 */
1617
function logItems(
1618
    array $SETTINGS,
1619
    int $item_id,
1620
    string $item_label,
1621
    int $id_user,
1622
    string $action,
1623
    ?string $login = null,
1624
    ?string $raison = null,
1625
    ?string $encryption_type = null,
1626
    ?string $time = null,
1627
    ?string $old_value = null
1628
): void {
1629
    // Load class DB
1630
    loadClasses('DB');
1631
1632
    // Insert log in DB
1633
    DB::insert(
1634
        prefixTable('log_items'),
1635
        [
1636
            'id_item' => $item_id,
1637
            'date' => is_null($time) === true ? time() : $time,
1638
            'id_user' => $id_user,
1639
            'action' => $action,
1640
            'raison' => $raison,
1641
            'old_value' => $old_value,
1642
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1643
        ]
1644
    );
1645
    // Timestamp the last change
1646
    if ($action === 'at_creation' || $action === 'at_modifiation' || $action === 'at_delete' || $action === 'at_import') {
1647
        DB::update(
1648
            prefixTable('misc'),
1649
            [
1650
                'valeur' => time(),
1651
            ],
1652
            'type = %s AND intitule = %s',
1653
            'timestamp',
1654
            'last_item_change'
1655
        );
1656
    }
1657
1658
    // SYSLOG
1659
    if (isset($SETTINGS['syslog_enable']) === true && $SETTINGS['syslog_enable'] === '1') {
1660
        // Extract reason
1661
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1662
        // Get item info if not known
1663
        if (empty($item_label) === true) {
1664
            $dataItem = DB::queryfirstrow(
1665
                'SELECT id, id_tree, label
1666
                FROM ' . prefixTable('items') . '
1667
                WHERE id = %i',
1668
                $item_id
1669
            );
1670
            $item_label = $dataItem['label'];
1671
        }
1672
1673
        send_syslog(
1674
            'action=' . str_replace('at_', '', $action) .
1675
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1676
                ' itemno=' . $item_id .
1677
                ' user=' . is_null($login) === true ? '' : addslashes((string) $login) .
1678
                ' itemname="' . addslashes($item_label) . '"',
1679
            $SETTINGS['syslog_host'],
1680
            $SETTINGS['syslog_port'],
1681
            'teampass'
1682
        );
1683
    }
1684
1685
    // send notification if enabled
1686
    //notifyOnChange($item_id, $action, $SETTINGS);
1687
}
1688
1689
/**
1690
 * Prepare notification email to subscribers.
1691
 *
1692
 * @param int    $item_id  Item id
1693
 * @param string $label    Item label
1694
 * @param array  $changes  List of changes
1695
 * @param array  $SETTINGS Teampass settings
1696
 * 
1697
 * @return void
1698
 */
1699
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1700
{
1701
    $session = SessionManager::getSession();
1702
    $lang = new Language(); 
1703
    $globalsUserId = $session->get('user-id');
1704
    $globalsLastname = $session->get('user-lastname');
1705
    $globalsName = $session->get('user-name');
1706
    // send email to user that what to be notified
1707
    $notification = DB::queryOneColumn(
1708
        'email',
1709
        'SELECT *
1710
        FROM ' . prefixTable('notification') . ' AS n
1711
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1712
        WHERE n.item_id = %i AND n.user_id != %i',
1713
        $item_id,
1714
        $globalsUserId
1715
    );
1716
    if (DB::count() > 0) {
1717
        // Prepare path
1718
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1719
        // Get list of changes
1720
        $htmlChanges = '<ul>';
1721
        foreach ($changes as $change) {
1722
            $htmlChanges .= '<li>' . $change . '</li>';
1723
        }
1724
        $htmlChanges .= '</ul>';
1725
        // send email
1726
        DB::insert(
1727
            prefixTable('emails'),
1728
            [
1729
                'timestamp' => time(),
1730
                'subject' => $lang->get('email_subject_item_updated'),
1731
                'body' => str_replace(
1732
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1733
                    [$label, $path, $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1734
                    $lang->get('email_body_item_updated')
1735
                ),
1736
                'receivers' => implode(',', $notification),
1737
                'status' => '',
1738
            ]
1739
        );
1740
    }
1741
}
1742
1743
/**
1744
 * Returns the Item + path.
1745
 *
1746
 * @param int    $id_tree  Node id
1747
 * @param string $label    Label
1748
 * @param array  $SETTINGS TP settings
1749
 * 
1750
 * @return string
1751
 */
1752
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1753
{
1754
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1755
    $arbo = $tree->getPath($id_tree, true);
1756
    $path = '';
1757
    foreach ($arbo as $elem) {
1758
        if (empty($path) === true) {
1759
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1760
        } else {
1761
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1762
        }
1763
    }
1764
1765
    // Build text to show user
1766
    if (empty($label) === false) {
1767
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1768
    }
1769
    return empty($path) === true ? '' : $path;
1770
}
1771
1772
/**
1773
 * Get the client ip address.
1774
 *
1775
 * @return string IP address
1776
 */
1777
function getClientIpServer(): string
1778
{
1779
    if (getenv('HTTP_CLIENT_IP')) {
1780
        $ipaddress = getenv('HTTP_CLIENT_IP');
1781
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1782
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1783
    } elseif (getenv('HTTP_X_FORWARDED')) {
1784
        $ipaddress = getenv('HTTP_X_FORWARDED');
1785
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1786
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1787
    } elseif (getenv('HTTP_FORWARDED')) {
1788
        $ipaddress = getenv('HTTP_FORWARDED');
1789
    } elseif (getenv('REMOTE_ADDR')) {
1790
        $ipaddress = getenv('REMOTE_ADDR');
1791
    } else {
1792
        $ipaddress = 'UNKNOWN';
1793
    }
1794
1795
    return $ipaddress;
1796
}
1797
1798
/**
1799
 * Escape all HTML, JavaScript, and CSS.
1800
 *
1801
 * @param string $input    The input string
1802
 * @param string $encoding Which character encoding are we using?
1803
 * 
1804
 * @return string
1805
 */
1806
function noHTML(string $input, string $encoding = 'UTF-8'): string
1807
{
1808
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1809
}
1810
1811
/**
1812
 * Permits to handle the Teampass config file
1813
 * $action accepts "rebuild" and "update"
1814
 *
1815
 * @param string $action   Action to perform
1816
 * @param array  $SETTINGS Teampass settings
1817
 * @param string $field    Field to refresh
1818
 * @param string $value    Value to set
1819
 *
1820
 * @return string|bool
1821
 */
1822
function handleConfigFile($action, $SETTINGS, $field = null, $value = null)
1823
{
1824
    $tp_config_file = $SETTINGS['cpassman_dir'] . '/includes/config/tp.config.php';
1825
1826
    // Load class DB
1827
    loadClasses('DB');
1828
1829
    if (file_exists($tp_config_file) === false || $action === 'rebuild') {
1830
        // perform a copy
1831
        if (file_exists($tp_config_file)) {
1832
            if (! copy($tp_config_file, $tp_config_file . '.' . date('Y_m_d_His', time()))) {
1833
                return "ERROR: Could not copy file '" . $tp_config_file . "'";
1834
            }
1835
        }
1836
1837
        // regenerate
1838
        $data = [];
1839
        $data[0] = "<?php\n";
1840
        $data[1] = "global \$SETTINGS;\n";
1841
        $data[2] = "\$SETTINGS = array (\n";
1842
        $rows = DB::query(
1843
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s',
1844
            'admin'
1845
        );
1846
        foreach ($rows as $record) {
1847
            array_push($data, "    '" . $record['intitule'] . "' => '" . htmlspecialchars_decode($record['valeur'], ENT_COMPAT) . "',\n");
1848
        }
1849
        array_push($data, ");\n");
1850
        $data = array_unique($data);
1851
    // ---
1852
    } elseif ($action === 'update' && empty($field) === false) {
1853
        $data = file($tp_config_file);
1854
        $inc = 0;
1855
        $bFound = false;
1856
        foreach ($data as $line) {
1857
            if (stristr($line, ');')) {
1858
                break;
1859
            }
1860
1861
            if (stristr($line, "'" . $field . "' => '")) {
1862
                $data[$inc] = "    '" . $field . "' => '" . htmlspecialchars_decode($value ?? '', ENT_COMPAT) . "',\n";
1863
                $bFound = true;
1864
                break;
1865
            }
1866
            ++$inc;
1867
        }
1868
        if ($bFound === false) {
1869
            $data[$inc] = "    '" . $field . "' => '" . htmlspecialchars_decode($value ?? '', ENT_COMPAT). "',\n);\n";
1870
        }
1871
    }
1872
1873
    // update file
1874
    file_put_contents($tp_config_file, implode('', $data ?? []));
1875
    return true;
1876
}
1877
1878
/**
1879
 * Permits to replace &#92; to permit correct display
1880
 *
1881
 * @param string $input Some text
1882
 * 
1883
 * @return string
1884
 */
1885
function handleBackslash(string $input): string
1886
{
1887
    return str_replace('&amp;#92;', '&#92;', $input);
1888
}
1889
1890
/**
1891
 * Permits to load settings
1892
 * 
1893
 * @return void
1894
*/
1895
function loadSettings(): void
1896
{
1897
    global $SETTINGS;
1898
    /* LOAD CPASSMAN SETTINGS */
1899
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1900
        $SETTINGS = [];
1901
        $SETTINGS['duplicate_folder'] = 0;
1902
        //by default, this is set to 0;
1903
        $SETTINGS['duplicate_item'] = 0;
1904
        //by default, this is set to 0;
1905
        $SETTINGS['number_of_used_pw'] = 5;
1906
        //by default, this value is set to 5;
1907
        $settings = [];
1908
        $rows = DB::query(
1909
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1910
            [
1911
                'type' => 'admin',
1912
                'type2' => 'settings',
1913
            ]
1914
        );
1915
        foreach ($rows as $record) {
1916
            if ($record['type'] === 'admin') {
1917
                $SETTINGS[$record['intitule']] = $record['valeur'];
1918
            } else {
1919
                $settings[$record['intitule']] = $record['valeur'];
1920
            }
1921
        }
1922
        $SETTINGS['loaded'] = 1;
1923
        $SETTINGS['default_session_expiration_time'] = 5;
1924
    }
1925
}
1926
1927
/**
1928
 * check if folder has custom fields.
1929
 * Ensure that target one also has same custom fields
1930
 * 
1931
 * @param int $source_id
1932
 * @param int $target_id 
1933
 * 
1934
 * @return bool
1935
*/
1936
function checkCFconsistency(int $source_id, int $target_id): bool
1937
{
1938
    $source_cf = [];
1939
    $rows = DB::QUERY(
1940
        'SELECT id_category
1941
            FROM ' . prefixTable('categories_folders') . '
1942
            WHERE id_folder = %i',
1943
        $source_id
1944
    );
1945
    foreach ($rows as $record) {
1946
        array_push($source_cf, $record['id_category']);
1947
    }
1948
1949
    $target_cf = [];
1950
    $rows = DB::QUERY(
1951
        'SELECT id_category
1952
            FROM ' . prefixTable('categories_folders') . '
1953
            WHERE id_folder = %i',
1954
        $target_id
1955
    );
1956
    foreach ($rows as $record) {
1957
        array_push($target_cf, $record['id_category']);
1958
    }
1959
1960
    $cf_diff = array_diff($source_cf, $target_cf);
1961
    if (count($cf_diff) > 0) {
1962
        return false;
1963
    }
1964
1965
    return true;
1966
}
1967
1968
/**
1969
 * Will encrypte/decrypt a fil eusing Defuse.
1970
 *
1971
 * @param string $type        can be either encrypt or decrypt
1972
 * @param string $source_file path to source file
1973
 * @param string $target_file path to target file
1974
 * @param array  $SETTINGS    Settings
1975
 * @param string $password    A password
1976
 *
1977
 * @return string|bool
1978
 */
1979
function prepareFileWithDefuse(
1980
    string $type,
1981
    string $source_file,
1982
    string $target_file,
1983
    array $SETTINGS,
1984
    string $password = null
1985
) {
1986
    // Load AntiXSS
1987
    $antiXss = new AntiXSS();
1988
    // Protect against bad inputs
1989
    if (is_array($source_file) === true || is_array($target_file) === true) {
1990
        return 'error_cannot_be_array';
1991
    }
1992
1993
    // Sanitize
1994
    $source_file = $antiXss->xss_clean($source_file);
1995
    $target_file = $antiXss->xss_clean($target_file);
1996
    if (empty($password) === true || is_null($password) === true) {
1997
        // get KEY to define password
1998
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
1999
        $password = Key::loadFromAsciiSafeString($ascii_key);
2000
    }
2001
2002
    $err = '';
2003
    if ($type === 'decrypt') {
2004
        // Decrypt file
2005
        $err = defuseFileDecrypt(
2006
            $source_file,
2007
            $target_file,
2008
            $SETTINGS, /** @scrutinizer ignore-type */
2009
            $password
2010
        );
2011
    } elseif ($type === 'encrypt') {
2012
        // Encrypt file
2013
        $err = defuseFileEncrypt(
2014
            $source_file,
2015
            $target_file,
2016
            $SETTINGS, /** @scrutinizer ignore-type */
2017
            $password
2018
        );
2019
    }
2020
2021
    // return error
2022
    return $err === true ? '' : $err;
2023
}
2024
2025
/**
2026
 * Encrypt a file with Defuse.
2027
 *
2028
 * @param string $source_file path to source file
2029
 * @param string $target_file path to target file
2030
 * @param array  $SETTINGS    Settings
2031
 * @param string $password    A password
2032
 *
2033
 * @return string|bool
2034
 */
2035
function defuseFileEncrypt(
2036
    string $source_file,
2037
    string $target_file,
2038
    array $SETTINGS,
2039
    string $password = null
2040
) {
2041
    try {
2042
        CryptoFile::encryptFileWithPassword(
2043
            $source_file,
2044
            $target_file,
2045
            $password
2046
        );
2047
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
2048
        $err = 'wrong_key';
2049
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
2050
        $err = $ex;
2051
    } catch (CryptoException\IOException $ex) {
2052
        $err = $ex;
2053
    }
2054
2055
    // return error
2056
    return empty($err) === false ? $err : true;
2057
}
2058
2059
/**
2060
 * Decrypt a file with Defuse.
2061
 *
2062
 * @param string $source_file path to source file
2063
 * @param string $target_file path to target file
2064
 * @param array  $SETTINGS    Settings
2065
 * @param string $password    A password
2066
 *
2067
 * @return string|bool
2068
 */
2069
function defuseFileDecrypt(
2070
    string $source_file,
2071
    string $target_file,
2072
    array $SETTINGS,
2073
    string $password = null
2074
) {
2075
    try {
2076
        CryptoFile::decryptFileWithPassword(
2077
            $source_file,
2078
            $target_file,
2079
            $password
2080
        );
2081
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
2082
        $err = 'wrong_key';
2083
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
2084
        $err = $ex;
2085
    } catch (CryptoException\IOException $ex) {
2086
        $err = $ex;
2087
    }
2088
2089
    // return error
2090
    return empty($err) === false ? $err : true;
2091
}
2092
2093
/*
2094
* NOT TO BE USED
2095
*/
2096
/**
2097
 * Undocumented function.
2098
 *
2099
 * @param string $text Text to debug
2100
 */
2101
function debugTeampass(string $text): void
2102
{
2103
    $debugFile = fopen('D:/wamp64/www/TeamPass/debug.txt', 'r+');
2104
    if ($debugFile !== false) {
2105
        fputs($debugFile, $text);
2106
        fclose($debugFile);
2107
    }
2108
}
2109
2110
/**
2111
 * DELETE the file with expected command depending on server type.
2112
 *
2113
 * @param string $file     Path to file
2114
 * @param array  $SETTINGS Teampass settings
2115
 *
2116
 * @return void
2117
 */
2118
function fileDelete(string $file, array $SETTINGS): void
2119
{
2120
    // Load AntiXSS
2121
    $antiXss = new AntiXSS();
2122
    $file = $antiXss->xss_clean($file);
2123
    if (is_file($file)) {
2124
        unlink($file);
2125
    }
2126
}
2127
2128
/**
2129
 * Permits to extract the file extension.
2130
 *
2131
 * @param string $file File name
2132
 *
2133
 * @return string
2134
 */
2135
function getFileExtension(string $file): string
2136
{
2137
    if (strpos($file, '.') === false) {
2138
        return $file;
2139
    }
2140
2141
    return substr($file, strrpos($file, '.') + 1);
2142
}
2143
2144
/**
2145
 * Chmods files and folders with different permissions.
2146
 *
2147
 * This is an all-PHP alternative to using: \n
2148
 * <tt>exec("find ".$path." -type f -exec chmod 644 {} \;");</tt> \n
2149
 * <tt>exec("find ".$path." -type d -exec chmod 755 {} \;");</tt>
2150
 *
2151
 * @author Jeppe Toustrup (tenzer at tenzer dot dk)
2152
  *
2153
 * @param string $path      An either relative or absolute path to a file or directory which should be processed.
2154
 * @param int    $filePerm The permissions any found files should get.
2155
 * @param int    $dirPerm  The permissions any found folder should get.
2156
 *
2157
 * @return bool Returns TRUE if the path if found and FALSE if not.
2158
 *
2159
 * @warning The permission levels has to be entered in octal format, which
2160
 * normally means adding a zero ("0") in front of the permission level. \n
2161
 * More info at: http://php.net/chmod.
2162
*/
2163
2164
function recursiveChmod(
2165
    string $path,
2166
    int $filePerm = 0644,
2167
    int  $dirPerm = 0755
2168
) {
2169
    // Check if the path exists
2170
    if (! file_exists($path)) {
2171
        return false;
2172
    }
2173
2174
    // See whether this is a file
2175
    if (is_file($path)) {
2176
        // Chmod the file with our given filepermissions
2177
        try {
2178
            chmod($path, $filePerm);
2179
        } catch (Exception $e) {
2180
            return false;
2181
        }
2182
    // If this is a directory...
2183
    } elseif (is_dir($path)) {
2184
        // Then get an array of the contents
2185
        $foldersAndFiles = scandir($path);
2186
        // Remove "." and ".." from the list
2187
        $entries = array_slice($foldersAndFiles, 2);
2188
        // Parse every result...
2189
        foreach ($entries as $entry) {
2190
            // And call this function again recursively, with the same permissions
2191
            recursiveChmod($path.'/'.$entry, $filePerm, $dirPerm);
2192
        }
2193
2194
        // When we are done with the contents of the directory, we chmod the directory itself
2195
        try {
2196
            chmod($path, $filePerm);
2197
        } catch (Exception $e) {
2198
            return false;
2199
        }
2200
    }
2201
2202
    // Everything seemed to work out well, return true
2203
    return true;
2204
}
2205
2206
/**
2207
 * Check if user can access to this item.
2208
 *
2209
 * @param int   $item_id ID of item
2210
 * @param array $SETTINGS
2211
 *
2212
 * @return bool|string
2213
 */
2214
function accessToItemIsGranted(int $item_id, array $SETTINGS)
2215
{
2216
    
2217
    $session = SessionManager::getSession();
2218
    $session_groupes_visibles = $session->get('user-accessible_folders');
2219
    $session_list_restricted_folders_for_items = $session->get('system-list_restricted_folders_for_items');
2220
    // Load item data
2221
    $data = DB::queryFirstRow(
2222
        'SELECT id_tree
2223
        FROM ' . prefixTable('items') . '
2224
        WHERE id = %i',
2225
        $item_id
2226
    );
2227
    // Check if user can access this folder
2228
    if (in_array($data['id_tree'], $session_groupes_visibles) === false) {
2229
        // Now check if this folder is restricted to user
2230
        if (isset($session_list_restricted_folders_for_items[$data['id_tree']]) === true
2231
            && in_array($item_id, $session_list_restricted_folders_for_items[$data['id_tree']]) === false
2232
        ) {
2233
            return 'ERR_FOLDER_NOT_ALLOWED';
2234
        }
2235
    }
2236
2237
    return true;
2238
}
2239
2240
/**
2241
 * Creates a unique key.
2242
 *
2243
 * @param int $lenght Key lenght
2244
 *
2245
 * @return string
2246
 */
2247
function uniqidReal(int $lenght = 13): string
2248
{
2249
    if (function_exists('random_bytes')) {
2250
        $bytes = random_bytes(intval(ceil($lenght / 2)));
2251
    } elseif (function_exists('openssl_random_pseudo_bytes')) {
2252
        $bytes = openssl_random_pseudo_bytes(intval(ceil($lenght / 2)));
2253
    } else {
2254
        throw new Exception('no cryptographically secure random function available');
2255
    }
2256
2257
    return substr(bin2hex($bytes), 0, $lenght);
2258
}
2259
2260
/**
2261
 * Obfuscate an email.
2262
 *
2263
 * @param string $email Email address
2264
 *
2265
 * @return string
2266
 */
2267
function obfuscateEmail(string $email): string
2268
{
2269
    $email = explode("@", $email);
2270
    $name = $email[0];
2271
    if (strlen($name) > 3) {
2272
        $name = substr($name, 0, 2);
2273
        for ($i = 0; $i < strlen($email[0]) - 3; $i++) {
2274
            $name .= "*";
2275
        }
2276
        $name .= substr($email[0], -1, 1);
2277
    }
2278
    $host = explode(".", $email[1])[0];
2279
    if (strlen($host) > 3) {
2280
        $host = substr($host, 0, 1);
2281
        for ($i = 0; $i < strlen(explode(".", $email[1])[0]) - 2; $i++) {
2282
            $host .= "*";
2283
        }
2284
        $host .= substr(explode(".", $email[1])[0], -1, 1);
2285
    }
2286
    $email = $name . "@" . $host . "." . explode(".", $email[1])[1];
2287
    return $email;
2288
}
2289
2290
/**
2291
 * Perform a Query.
2292
 *
2293
 * @param array  $SETTINGS Teamapss settings
2294
 * @param string $fields   Fields to use
2295
 * @param string $table    Table to use
2296
 *
2297
 * @return array
2298
 */
2299
function performDBQuery(array $SETTINGS, string $fields, string $table): array
2300
{
2301
    // include librairies & connect to DB
2302
    //include_once $SETTINGS['cpassman_dir'] . '/includes/config/settings.php';
2303
2304
    // Load class DB
2305
    loadClasses('DB');
2306
    
2307
    // Insert log in DB
2308
    return DB::query(
2309
        'SELECT ' . $fields . '
2310
        FROM ' . prefixTable($table)
2311
    );
2312
}
2313
2314
/**
2315
 * Undocumented function.
2316
 *
2317
 * @param int $bytes Size of file
2318
 *
2319
 * @return string
2320
 */
2321
function formatSizeUnits(int $bytes): string
2322
{
2323
    if ($bytes >= 1073741824) {
2324
        $bytes = number_format($bytes / 1073741824, 2) . ' GB';
2325
    } elseif ($bytes >= 1048576) {
2326
        $bytes = number_format($bytes / 1048576, 2) . ' MB';
2327
    } elseif ($bytes >= 1024) {
2328
        $bytes = number_format($bytes / 1024, 2) . ' KB';
2329
    } elseif ($bytes > 1) {
2330
        $bytes .= ' bytes';
2331
    } elseif ($bytes === 1) {
2332
        $bytes .= ' byte';
2333
    } else {
2334
        $bytes = '0 bytes';
2335
    }
2336
2337
    return $bytes;
2338
}
2339
2340
/**
2341
 * Generate user pair of keys.
2342
 *
2343
 * @param string $userPwd User password
2344
 *
2345
 * @return array
2346
 */
2347
function generateUserKeys(string $userPwd): array
2348
{
2349
    // Sanitize
2350
    $antiXss = new AntiXSS();
2351
    $userPwd = $antiXss->xss_clean($userPwd);
2352
    // Load classes
2353
    $rsa = new Crypt_RSA();
2354
    $cipher = new Crypt_AES();
2355
    // Create the private and public key
2356
    $res = $rsa->createKey(4096);
2357
    // Encrypt the privatekey
2358
    $cipher->setPassword($userPwd);
2359
    $privatekey = $cipher->encrypt($res['privatekey']);
2360
    return [
2361
        'private_key' => base64_encode($privatekey),
2362
        'public_key' => base64_encode($res['publickey']),
2363
        'private_key_clear' => base64_encode($res['privatekey']),
2364
    ];
2365
}
2366
2367
/**
2368
 * Permits to decrypt the user's privatekey.
2369
 *
2370
 * @param string $userPwd        User password
2371
 * @param string $userPrivateKey User private key
2372
 *
2373
 * @return string|object
2374
 */
2375
function decryptPrivateKey(string $userPwd, string $userPrivateKey)
2376
{
2377
    // Sanitize
2378
    $antiXss = new AntiXSS();
2379
    $userPwd = $antiXss->xss_clean($userPwd);
2380
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2381
2382
    if (empty($userPwd) === false) {
2383
        // Load classes
2384
        $cipher = new Crypt_AES();
2385
        // Encrypt the privatekey
2386
        $cipher->setPassword($userPwd);
2387
        try {
2388
            return base64_encode((string) $cipher->decrypt(base64_decode($userPrivateKey)));
2389
        } catch (Exception $e) {
2390
            return $e;
2391
        }
2392
    }
2393
    return '';
2394
}
2395
2396
/**
2397
 * Permits to encrypt the user's privatekey.
2398
 *
2399
 * @param string $userPwd        User password
2400
 * @param string $userPrivateKey User private key
2401
 *
2402
 * @return string
2403
 */
2404
function encryptPrivateKey(string $userPwd, string $userPrivateKey): string
2405
{
2406
    // Sanitize
2407
    $antiXss = new AntiXSS();
2408
    $userPwd = $antiXss->xss_clean($userPwd);
2409
    $userPrivateKey = $antiXss->xss_clean($userPrivateKey);
2410
2411
    if (empty($userPwd) === false) {
2412
        // Load classes
2413
        $cipher = new Crypt_AES();
2414
        // Encrypt the privatekey
2415
        $cipher->setPassword($userPwd);        
2416
        try {
2417
            return base64_encode($cipher->encrypt(base64_decode($userPrivateKey)));
2418
        } catch (Exception $e) {
2419
            return $e;
2420
        }
2421
    }
2422
    return '';
2423
}
2424
2425
/**
2426
 * Encrypts a string using AES.
2427
 *
2428
 * @param string $data String to encrypt
2429
 * @param string $key
2430
 *
2431
 * @return array
2432
 */
2433
function doDataEncryption(string $data, string $key = NULL): array
2434
{
2435
    // Sanitize
2436
    $antiXss = new AntiXSS();
2437
    $data = $antiXss->xss_clean($data);
2438
    
2439
    // Load classes
2440
    $cipher = new Crypt_AES(CRYPT_AES_MODE_CBC);
2441
    // Generate an object key
2442
    $objectKey = is_null($key) === true ? uniqidReal(KEY_LENGTH) : $antiXss->xss_clean($key);
2443
    // Set it as password
2444
    $cipher->setPassword($objectKey);
2445
    return [
2446
        'encrypted' => base64_encode($cipher->encrypt($data)),
2447
        'objectKey' => base64_encode($objectKey),
2448
    ];
2449
}
2450
2451
/**
2452
 * Decrypts a string using AES.
2453
 *
2454
 * @param string $data Encrypted data
2455
 * @param string $key  Key to uncrypt
2456
 *
2457
 * @return string
2458
 */
2459
function doDataDecryption(string $data, string $key): string
2460
{
2461
    // Sanitize
2462
    $antiXss = new AntiXSS();
2463
    $data = $antiXss->xss_clean($data);
2464
    $key = $antiXss->xss_clean($key);
2465
2466
    // Load classes
2467
    $cipher = new Crypt_AES();
2468
    // Set the object key
2469
    $cipher->setPassword(base64_decode($key));
2470
    return base64_encode((string) $cipher->decrypt(base64_decode($data)));
2471
}
2472
2473
/**
2474
 * Encrypts using RSA a string using a public key.
2475
 *
2476
 * @param string $key       Key to be encrypted
2477
 * @param string $publicKey User public key
2478
 *
2479
 * @return string
2480
 */
2481
function encryptUserObjectKey(string $key, string $publicKey): string
2482
{
2483
    // Sanitize
2484
    $antiXss = new AntiXSS();
2485
    $publicKey = $antiXss->xss_clean($publicKey);
2486
    // Load classes
2487
    $rsa = new Crypt_RSA();
2488
    // Load the public key
2489
    $decodedPublicKey = base64_decode($publicKey, true);
2490
    if ($decodedPublicKey === false) {
2491
        throw new InvalidArgumentException("Error while decoding key.");
2492
    }
2493
    $rsa->loadKey($decodedPublicKey);
2494
    // Encrypt
2495
    $encrypted = $rsa->encrypt(base64_decode($key));
2496
    if ($encrypted === false) {
0 ignored issues
show
introduced by
The condition $encrypted === false is always false.
Loading history...
2497
        throw new RuntimeException("Error while encrypting key.");
2498
    }
2499
    // Return
2500
    return base64_encode($encrypted);
2501
}
2502
2503
/**
2504
 * Decrypts using RSA an encrypted string using a private key.
2505
 *
2506
 * @param string $key        Encrypted key
2507
 * @param string $privateKey User private key
2508
 *
2509
 * @return string
2510
 */
2511
function decryptUserObjectKey(string $key, string $privateKey): string
2512
{
2513
    // Sanitize
2514
    $antiXss = new AntiXSS();
2515
    $privateKey = $antiXss->xss_clean($privateKey);
2516
2517
    // Load classes
2518
    $rsa = new Crypt_RSA();
2519
    // Load the private key
2520
    $decodedPrivateKey = base64_decode($privateKey, true);
2521
    if ($decodedPrivateKey === false) {
2522
        throw new InvalidArgumentException("Error while decoding private key.");
2523
    }
2524
2525
    $rsa->loadKey($decodedPrivateKey);
2526
2527
    // Decrypt
2528
    try {
2529
        $decodedKey = base64_decode($key, true);
2530
        if ($decodedKey === false) {
2531
            throw new InvalidArgumentException("Error while decoding key.");
2532
        }
2533
2534
        $tmpValue = $rsa->decrypt($decodedKey);
2535
        if ($tmpValue !== false) {
0 ignored issues
show
introduced by
The condition $tmpValue !== false is always true.
Loading history...
2536
            return base64_encode($tmpValue);
2537
        } else {
2538
            return '';
2539
        }
2540
    } catch (Exception $e) {
2541
        error_log('TEAMPASS Error - ldap - '.$e->getMessage());
2542
        return 'Exception: could not decrypt object';
2543
    }
2544
}
2545
2546
/**
2547
 * Encrypts a file.
2548
 *
2549
 * @param string $fileInName File name
2550
 * @param string $fileInPath Path to file
2551
 *
2552
 * @return array
2553
 */
2554
function encryptFile(string $fileInName, string $fileInPath): array
2555
{
2556
    if (defined('FILE_BUFFER_SIZE') === false) {
2557
        define('FILE_BUFFER_SIZE', 128 * 1024);
2558
    }
2559
2560
    // Load classes
2561
    $cipher = new Crypt_AES();
2562
2563
    // Generate an object key
2564
    $objectKey = uniqidReal(32);
2565
    // Set it as password
2566
    $cipher->setPassword($objectKey);
2567
    // Prevent against out of memory
2568
    $cipher->enableContinuousBuffer();
2569
2570
    // Encrypt the file content
2571
    $filePath = filter_var($fileInPath . '/' . $fileInName, FILTER_SANITIZE_URL);
2572
    $fileContent = file_get_contents($filePath);
2573
    $plaintext = $fileContent;
2574
    $ciphertext = $cipher->encrypt($plaintext);
2575
2576
    // Save new file
2577
    // deepcode ignore InsecureHash: is simply used to get a unique name
2578
    $hash = md5($plaintext);
2579
    $fileOut = $fileInPath . '/' . TP_FILE_PREFIX . $hash;
2580
    file_put_contents($fileOut, $ciphertext);
2581
    unlink($fileInPath . '/' . $fileInName);
2582
    return [
2583
        'fileHash' => base64_encode($hash),
2584
        'objectKey' => base64_encode($objectKey),
2585
    ];
2586
}
2587
2588
/**
2589
 * Decrypt a file.
2590
 *
2591
 * @param string $fileName File name
2592
 * @param string $filePath Path to file
2593
 * @param string $key      Key to use
2594
 *
2595
 * @return string
2596
 */
2597
function decryptFile(string $fileName, string $filePath, string $key): string
2598
{
2599
    if (! defined('FILE_BUFFER_SIZE')) {
2600
        define('FILE_BUFFER_SIZE', 128 * 1024);
2601
    }
2602
    
2603
    // Load classes
2604
    $cipher = new Crypt_AES();
2605
    $antiXSS = new AntiXSS();
2606
    
2607
    // Get file name
2608
    $safeFileName = $antiXSS->xss_clean(base64_decode($fileName));
2609
2610
    // Set the object key
2611
    $cipher->setPassword(base64_decode($key));
2612
    // Prevent against out of memory
2613
    $cipher->enableContinuousBuffer();
2614
    $cipher->disablePadding();
2615
    // Get file content
2616
    $safeFilePath = $filePath . '/' . TP_FILE_PREFIX . $safeFileName;
2617
    $ciphertext = file_get_contents(filter_var($safeFilePath, FILTER_SANITIZE_URL));
2618
2619
    if (WIP) error_log('DEBUG: File image url -> '.filter_var($safeFilePath, FILTER_SANITIZE_URL));
2620
2621
    // Decrypt file content and return
2622
    return base64_encode($cipher->decrypt($ciphertext));
2623
}
2624
2625
/**
2626
 * Generate a simple password
2627
 *
2628
 * @param int $length Length of string
2629
 * @param bool $symbolsincluded Allow symbols
2630
 *
2631
 * @return string
2632
 */
2633
function generateQuickPassword(int $length = 16, bool $symbolsincluded = true): string
2634
{
2635
    // Generate new user password
2636
    $small_letters = range('a', 'z');
2637
    $big_letters = range('A', 'Z');
2638
    $digits = range(0, 9);
2639
    $symbols = $symbolsincluded === true ?
2640
        ['#', '_', '-', '@', '$', '+', '!'] : [];
2641
    $res = array_merge($small_letters, $big_letters, $digits, $symbols);
2642
    $count = count($res);
2643
    // first variant
2644
2645
    $random_string = '';
2646
    for ($i = 0; $i < $length; ++$i) {
2647
        $random_string .= $res[random_int(0, $count - 1)];
2648
    }
2649
2650
    return $random_string;
2651
}
2652
2653
/**
2654
 * Permit to store the sharekey of an object for users.
2655
 *
2656
 * @param string $object_name             Type for table selection
2657
 * @param int    $post_folder_is_personal Personal
2658
 * @param int    $post_folder_id          Folder
2659
 * @param int    $post_object_id          Object
2660
 * @param string $objectKey               Object key
2661
 * @param array  $SETTINGS                Teampass settings
2662
 * @param int    $user_id                 User ID if needed
2663
 * @param bool   $onlyForUser                 User ID if needed
2664
 * @param bool   $deleteAll                 User ID if needed
2665
 * @param array  $objectKeyArray                 User ID if needed
2666
 *
2667
 * @return void
2668
 */
2669
function storeUsersShareKey(
2670
    string $object_name,
2671
    int $post_folder_is_personal,
2672
    int $post_folder_id,
2673
    int $post_object_id,
2674
    string $objectKey,
2675
    array $SETTINGS,
2676
    bool $onlyForUser = false,
2677
    bool $deleteAll = true,
2678
    array $objectKeyArray = []
2679
): void {
2680
    
2681
    $session = SessionManager::getSession();
2682
    loadClasses('DB');
2683
2684
    // Delete existing entries for this object
2685
    if ($deleteAll === true) {
2686
        DB::delete(
2687
            $object_name,
2688
            'object_id = %i',
2689
            $post_object_id
2690
        );
2691
    }
2692
    
2693
    if (
2694
        $onlyForUser === true || (int) $post_folder_is_personal === 1
2695
    ) {
2696
        // Only create the sharekey for a user
2697
        $user = DB::queryFirstRow(
2698
            'SELECT public_key
2699
            FROM ' . prefixTable('users') . '
2700
            WHERE id = ' . (int) $session->get('user-id') . '
2701
            AND public_key != ""'
2702
        );
2703
2704
        if (empty($objectKey) === false) {
2705
            DB::insert(
2706
                $object_name,
2707
                [
2708
                    'object_id' => (int) $post_object_id,
2709
                    'user_id' => (int) $session->get('user-id'),
2710
                    'share_key' => encryptUserObjectKey(
2711
                        $objectKey,
2712
                        $user['public_key']
2713
                    ),
2714
                ]
2715
            );
2716
        } else if (count($objectKeyArray) > 0) {
2717
            foreach ($objectKeyArray as $object) {
2718
                DB::insert(
2719
                    $object_name,
2720
                    [
2721
                        'object_id' => (int) $object['objectId'],
2722
                        'user_id' => (int) $session->get('user-id'),
2723
                        'share_key' => encryptUserObjectKey(
2724
                            $object['objectKey'],
2725
                            $user['public_key']
2726
                        ),
2727
                    ]
2728
                );
2729
            }
2730
        }
2731
    } else {
2732
        // Create sharekey for each user
2733
        //DB::debugmode(true);
2734
        $users = DB::query(
2735
            'SELECT id, public_key
2736
            FROM ' . prefixTable('users') . '
2737
            WHERE ' . ($onlyForUser === true ? 
0 ignored issues
show
introduced by
The condition $onlyForUser === true is always false.
Loading history...
2738
                'id IN ("' . TP_USER_ID . '","' . $session->get('user-id') . '") ' : 
2739
                'id NOT IN ("' . OTV_USER_ID . '","' . SSH_USER_ID . '","' . API_USER_ID . '") ') . '
2740
            AND public_key != ""'
2741
        );
2742
        //DB::debugmode(false);
2743
        foreach ($users as $user) {
2744
            // Insert in DB the new object key for this item by user
2745
            if (count($objectKeyArray) === 0) {
2746
                DB::insert(
2747
                    $object_name,
2748
                    [
2749
                        'object_id' => $post_object_id,
2750
                        'user_id' => (int) $user['id'],
2751
                        'share_key' => encryptUserObjectKey(
2752
                            $objectKey,
2753
                            $user['public_key']
2754
                        ),
2755
                    ]
2756
                );
2757
            } else {
2758
                foreach ($objectKeyArray as $object) {
2759
                    DB::insert(
2760
                        $object_name,
2761
                        [
2762
                            'object_id' => (int) $object['objectId'],
2763
                            'user_id' => (int) $user['id'],
2764
                            'share_key' => encryptUserObjectKey(
2765
                                $object['objectKey'],
2766
                                $user['public_key']
2767
                            ),
2768
                        ]
2769
                    );
2770
                }
2771
            }
2772
        }
2773
    }
2774
}
2775
2776
/**
2777
 * Is this string base64 encoded?
2778
 *
2779
 * @param string $str Encoded string?
2780
 *
2781
 * @return bool
2782
 */
2783
function isBase64(string $str): bool
2784
{
2785
    $str = (string) trim($str);
2786
    if (! isset($str[0])) {
2787
        return false;
2788
    }
2789
2790
    $base64String = (string) base64_decode($str, true);
2791
    if ($base64String && base64_encode($base64String) === $str) {
2792
        return true;
2793
    }
2794
2795
    return false;
2796
}
2797
2798
/**
2799
 * Undocumented function
2800
 *
2801
 * @param string $field Parameter
2802
 *
2803
 * @return array|bool|resource|string
2804
 */
2805
function filterString(string $field)
2806
{
2807
    // Sanitize string
2808
    $field = filter_var(trim($field), FILTER_SANITIZE_FULL_SPECIAL_CHARS);
2809
    if (empty($field) === false) {
2810
        // Load AntiXSS
2811
        $antiXss = new AntiXSS();
2812
        // Return
2813
        return $antiXss->xss_clean($field);
2814
    }
2815
2816
    return false;
2817
}
2818
2819
/**
2820
 * CHeck if provided credentials are allowed on server
2821
 *
2822
 * @param string $login    User Login
2823
 * @param string $password User Pwd
2824
 * @param array  $SETTINGS Teampass settings
2825
 *
2826
 * @return bool
2827
 */
2828
function ldapCheckUserPassword(string $login, string $password, array $SETTINGS): bool
2829
{
2830
    // Build ldap configuration array
2831
    $config = [
2832
        // Mandatory Configuration Options
2833
        'hosts' => [$SETTINGS['ldap_hosts']],
2834
        'base_dn' => $SETTINGS['ldap_bdn'],
2835
        'username' => $SETTINGS['ldap_username'],
2836
        'password' => $SETTINGS['ldap_password'],
2837
2838
        // Optional Configuration Options
2839
        'port' => $SETTINGS['ldap_port'],
2840
        'use_ssl' => (int) $SETTINGS['ldap_ssl'] === 1 ? true : false,
2841
        'use_tls' => (int) $SETTINGS['ldap_tls'] === 1 ? true : false,
2842
        'version' => 3,
2843
        'timeout' => 5,
2844
        'follow_referrals' => false,
2845
2846
        // Custom LDAP Options
2847
        'options' => [
2848
            // See: http://php.net/ldap_set_option
2849
            LDAP_OPT_X_TLS_REQUIRE_CERT => (isset($SETTINGS['ldap_tls_certiface_check']) ? $SETTINGS['ldap_tls_certiface_check'] : LDAP_OPT_X_TLS_HARD),
2850
        ],
2851
    ];
2852
    
2853
    $connection = new Connection($config);
2854
    // Connect to LDAP
2855
    try {
2856
        $connection->connect();
2857
    } catch (\LdapRecord\Auth\BindException $e) {
2858
        $error = $e->getDetailedError();
2859
        echo 'Error : '.$error->getErrorCode().' - '.$error->getErrorMessage(). '<br>'.$error->getDiagnosticMessage();
2860
        return false;
2861
    }
2862
2863
    // Authenticate user
2864
    try {
2865
        if ($SETTINGS['ldap_type'] === 'ActiveDirectory') {
2866
            $connection->auth()->attempt($login, $password, $stayAuthenticated = true);
2867
        } else {
2868
            $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);
2869
        }
2870
    } catch (\LdapRecord\Auth\BindException $e) {
2871
        $error = $e->getDetailedError();
2872
        echo 'Error : '.$error->getErrorCode().' - '.$error->getErrorMessage(). '<br>'.$error->getDiagnosticMessage();
2873
        return false;
2874
    }
2875
2876
    return true;
2877
}
2878
2879
/**
2880
 * Removes from DB all sharekeys of this user
2881
 *
2882
 * @param int $userId User's id
2883
 * @param array   $SETTINGS Teampass settings
2884
 *
2885
 * @return bool
2886
 */
2887
function deleteUserObjetsKeys(int $userId, array $SETTINGS = []): bool
2888
{
2889
    // Load class DB
2890
    loadClasses('DB');
2891
2892
    // Remove all item sharekeys items
2893
    // expect if personal item
2894
    DB::delete(
2895
        prefixTable('sharekeys_items'),
2896
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2897
        $userId
2898
    );
2899
    // Remove all item sharekeys files
2900
    DB::delete(
2901
        prefixTable('sharekeys_files'),
2902
        'user_id = %i AND object_id NOT IN (
2903
            SELECT f.id 
2904
            FROM ' . prefixTable('items') . ' AS i 
2905
            INNER JOIN ' . prefixTable('files') . ' AS f ON f.id_item = i.id
2906
            WHERE i.perso = 1
2907
        )',
2908
        $userId
2909
    );
2910
    // Remove all item sharekeys fields
2911
    DB::delete(
2912
        prefixTable('sharekeys_fields'),
2913
        'user_id = %i AND object_id NOT IN (
2914
            SELECT c.id 
2915
            FROM ' . prefixTable('items') . ' AS i 
2916
            INNER JOIN ' . prefixTable('categories_items') . ' AS c ON c.item_id = i.id
2917
            WHERE i.perso = 1
2918
        )',
2919
        $userId
2920
    );
2921
    // Remove all item sharekeys logs
2922
    DB::delete(
2923
        prefixTable('sharekeys_logs'),
2924
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2925
        $userId
2926
    );
2927
    // Remove all item sharekeys suggestions
2928
    DB::delete(
2929
        prefixTable('sharekeys_suggestions'),
2930
        'user_id = %i AND object_id NOT IN (SELECT i.id FROM ' . prefixTable('items') . ' AS i WHERE i.perso = 1)',
2931
        $userId
2932
    );
2933
    return false;
2934
}
2935
2936
/**
2937
 * Manage list of timezones   $SETTINGS Teampass settings
2938
 *
2939
 * @return array
2940
 */
2941
function timezone_list()
2942
{
2943
    static $timezones = null;
2944
    if ($timezones === null) {
2945
        $timezones = [];
2946
        $offsets = [];
2947
        $now = new DateTime('now', new DateTimeZone('UTC'));
2948
        foreach (DateTimeZone::listIdentifiers() as $timezone) {
2949
            $now->setTimezone(new DateTimeZone($timezone));
2950
            $offsets[] = $offset = $now->getOffset();
2951
            $timezones[$timezone] = '(' . format_GMT_offset($offset) . ') ' . format_timezone_name($timezone);
2952
        }
2953
2954
        array_multisort($offsets, $timezones);
2955
    }
2956
2957
    return $timezones;
2958
}
2959
2960
/**
2961
 * Provide timezone offset
2962
 *
2963
 * @param int $offset Timezone offset
2964
 *
2965
 * @return string
2966
 */
2967
function format_GMT_offset($offset): string
2968
{
2969
    $hours = intval($offset / 3600);
2970
    $minutes = abs(intval($offset % 3600 / 60));
2971
    return 'GMT' . ($offset ? sprintf('%+03d:%02d', $hours, $minutes) : '');
2972
}
2973
2974
/**
2975
 * Provides timezone name
2976
 *
2977
 * @param string $name Timezone name
2978
 *
2979
 * @return string
2980
 */
2981
function format_timezone_name($name): string
2982
{
2983
    $name = str_replace('/', ', ', $name);
2984
    $name = str_replace('_', ' ', $name);
2985
2986
    return str_replace('St ', 'St. ', $name);
2987
}
2988
2989
/**
2990
 * Provides info if user should use MFA based on roles
2991
 *
2992
 * @param string $userRolesIds  User roles ids
2993
 * @param string $mfaRoles      Roles for which MFA is requested
2994
 *
2995
 * @return bool
2996
 */
2997
function mfa_auth_requested_roles(string $userRolesIds, string $mfaRoles): bool
2998
{
2999
    if (empty($mfaRoles) === true) {
3000
        return true;
3001
    }
3002
3003
    $mfaRoles = array_values(json_decode($mfaRoles, true));
3004
    $userRolesIds = array_filter(explode(';', $userRolesIds));
3005
    if (count($mfaRoles) === 0 || count(array_intersect($mfaRoles, $userRolesIds)) > 0) {
3006
        return true;
3007
    }
3008
3009
    return false;
3010
}
3011
3012
/**
3013
 * Permits to clean a string for export purpose
3014
 *
3015
 * @param string $text
3016
 * @param bool $emptyCheckOnly
3017
 * 
3018
 * @return string
3019
 */
3020
function cleanStringForExport(string $text, bool $emptyCheckOnly = false): string
3021
{
3022
    if (is_null($text) === true || empty($text) === true) {
3023
        return '';
3024
    }
3025
    // only expected to check if $text was empty
3026
    elseif ($emptyCheckOnly === true) {
3027
        return $text;
3028
    }
3029
3030
    return strip_tags(
3031
        cleanString(
3032
            html_entity_decode($text, ENT_QUOTES | ENT_XHTML, 'UTF-8'),
3033
            true)
3034
        );
3035
}
3036
3037
/**
3038
 * Permits to check if user ID is valid
3039
 *
3040
 * @param integer $post_user_id
3041
 * @return bool
3042
 */
3043
function isUserIdValid($userId): bool
3044
{
3045
    if (is_null($userId) === false
3046
        && isset($userId) === true
3047
        && empty($userId) === false
3048
    ) {
3049
        return true;
3050
    }
3051
    return false;
3052
}
3053
3054
/**
3055
 * Check if a key exists and if its value equal the one expected
3056
 *
3057
 * @param string $key
3058
 * @param integer|string $value
3059
 * @param array $array
3060
 * 
3061
 * @return boolean
3062
 */
3063
function isKeyExistingAndEqual(
3064
    string $key,
3065
    /*PHP8 - integer|string*/$value,
3066
    array $array
3067
): bool
3068
{
3069
    if (isset($array[$key]) === true
3070
        && (is_int($value) === true ?
3071
            (int) $array[$key] === $value :
3072
            (string) $array[$key] === $value)
3073
    ) {
3074
        return true;
3075
    }
3076
    return false;
3077
}
3078
3079
/**
3080
 * Check if a variable is not set or equal to a value
3081
 *
3082
 * @param string|null $var
3083
 * @param integer|string $value
3084
 * 
3085
 * @return boolean
3086
 */
3087
function isKeyNotSetOrEqual(
3088
    /*PHP8 - string|null*/$var,
3089
    /*PHP8 - integer|string*/$value
3090
): bool
3091
{
3092
    if (isset($var) === false
3093
        || (is_int($value) === true ?
3094
            (int) $var === $value :
3095
            (string) $var === $value)
3096
    ) {
3097
        return true;
3098
    }
3099
    return false;
3100
}
3101
3102
/**
3103
 * Check if a key exists and if its value < to the one expected
3104
 *
3105
 * @param string $key
3106
 * @param integer $value
3107
 * @param array $array
3108
 * 
3109
 * @return boolean
3110
 */
3111
function isKeyExistingAndInferior(string $key, int $value, array $array): bool
3112
{
3113
    if (isset($array[$key]) === true && (int) $array[$key] < $value) {
3114
        return true;
3115
    }
3116
    return false;
3117
}
3118
3119
/**
3120
 * Check if a key exists and if its value > to the one expected
3121
 *
3122
 * @param string $key
3123
 * @param integer $value
3124
 * @param array $array
3125
 * 
3126
 * @return boolean
3127
 */
3128
function isKeyExistingAndSuperior(string $key, int $value, array $array): bool
3129
{
3130
    if (isset($array[$key]) === true && (int) $array[$key] > $value) {
3131
        return true;
3132
    }
3133
    return false;
3134
}
3135
3136
/**
3137
 * Check if values in array are set
3138
 * Return true if all set
3139
 * Return false if one of them is not set
3140
 *
3141
 * @param array $arrayOfValues
3142
 * @return boolean
3143
 */
3144
function isSetArrayOfValues(array $arrayOfValues): bool
3145
{
3146
    foreach($arrayOfValues as $value) {
3147
        if (isset($value) === false) {
3148
            return false;
3149
        }
3150
    }
3151
    return true;
3152
}
3153
3154
/**
3155
 * Check if values in array are set
3156
 * Return true if all set
3157
 * Return false if one of them is not set
3158
 *
3159
 * @param array $arrayOfValues
3160
 * @param integer|string $value
3161
 * @return boolean
3162
 */
3163
function isArrayOfVarsEqualToValue(
3164
    array $arrayOfVars,
3165
    /*PHP8 - integer|string*/$value
3166
) : bool
3167
{
3168
    foreach($arrayOfVars as $variable) {
3169
        if ($variable !== $value) {
3170
            return false;
3171
        }
3172
    }
3173
    return true;
3174
}
3175
3176
/**
3177
 * Checks if at least one variable in array is equal to value
3178
 *
3179
 * @param array $arrayOfValues
3180
 * @param integer|string $value
3181
 * @return boolean
3182
 */
3183
function isOneVarOfArrayEqualToValue(
3184
    array $arrayOfVars,
3185
    /*PHP8 - integer|string*/$value
3186
) : bool
3187
{
3188
    foreach($arrayOfVars as $variable) {
3189
        if ($variable === $value) {
3190
            return true;
3191
        }
3192
    }
3193
    return false;
3194
}
3195
3196
/**
3197
 * Checks is value is null, not set OR empty
3198
 *
3199
 * @param string|int|null $value
3200
 * @return boolean
3201
 */
3202
function isValueSetNullEmpty(/*PHP8 - string|int|null*/ $value) : bool
3203
{
3204
    if (is_null($value) === true || isset($value) === false || empty($value) === true) {
3205
        return true;
3206
    }
3207
    return false;
3208
}
3209
3210
/**
3211
 * Checks if value is set and if empty is equal to passed boolean
3212
 *
3213
 * @param string|int $value
3214
 * @param boolean $boolean
3215
 * @return boolean
3216
 */
3217
function isValueSetEmpty($value, $boolean = true) : bool
3218
{
3219
    if (isset($value) === true && empty($value) === $boolean) {
3220
        return true;
3221
    }
3222
    return false;
3223
}
3224
3225
/**
3226
 * Ensure Complexity is translated
3227
 *
3228
 * @return void
3229
 */
3230
function defineComplexity() : void
3231
{
3232
    // Load user's language
3233
    $lang = new Language(); 
3234
    
3235
    if (defined('TP_PW_COMPLEXITY') === false) {
3236
        define(
3237
            'TP_PW_COMPLEXITY',
3238
            [
3239
                TP_PW_STRENGTH_1 => array(TP_PW_STRENGTH_1, $lang->get('complex_level1'), 'fas fa-thermometer-empty text-danger'),
3240
                TP_PW_STRENGTH_2 => array(TP_PW_STRENGTH_2, $lang->get('complex_level2'), 'fas fa-thermometer-quarter text-warning'),
3241
                TP_PW_STRENGTH_3 => array(TP_PW_STRENGTH_3, $lang->get('complex_level3'), 'fas fa-thermometer-half text-warning'),
3242
                TP_PW_STRENGTH_4 => array(TP_PW_STRENGTH_4, $lang->get('complex_level4'), 'fas fa-thermometer-three-quarters text-success'),
3243
                TP_PW_STRENGTH_5 => array(TP_PW_STRENGTH_5, $lang->get('complex_level5'), 'fas fa-thermometer-full text-success'),
3244
            ]
3245
        );
3246
    }
3247
}
3248
3249
/**
3250
 * Uses Sanitizer to perform data sanitization
3251
 *
3252
 * @param array     $data
3253
 * @param array     $filters
3254
 * @return array|string
3255
 */
3256
function dataSanitizer(array $data, array $filters): array|string
3257
{
3258
    // Load Sanitizer library
3259
    $sanitizer = new Sanitizer($data, $filters);
3260
3261
    // Load AntiXSS
3262
    $antiXss = new AntiXSS();
3263
3264
    // Sanitize post and get variables
3265
    return $antiXss->xss_clean($sanitizer->sanitize());
3266
}
3267
3268
/**
3269
 * Permits to manage the cache tree for a user
3270
 *
3271
 * @param integer $user_id
3272
 * @param string $data
3273
 * @param array $SETTINGS
3274
 * @param string $field_update
3275
 * @return void
3276
 */
3277
function cacheTreeUserHandler(int $user_id, string $data, array $SETTINGS, string $field_update = '')
3278
{
3279
    // Load class DB
3280
    loadClasses('DB');
3281
3282
    // Exists ?
3283
    $userCacheId = DB::queryfirstrow(
3284
        'SELECT increment_id
3285
        FROM ' . prefixTable('cache_tree') . '
3286
        WHERE user_id = %i',
3287
        $user_id
3288
    );
3289
    
3290
    if (is_null($userCacheId) === true || count($userCacheId) === 0) {
3291
        // insert in table
3292
        DB::insert(
3293
            prefixTable('cache_tree'),
3294
            array(
3295
                'data' => $data,
3296
                'timestamp' => time(),
3297
                'user_id' => $user_id,
3298
                'visible_folders' => '',
3299
            )
3300
        );
3301
    } else {
3302
        if (empty($field_update) === true) {
3303
            DB::update(
3304
                prefixTable('cache_tree'),
3305
                [
3306
                    'timestamp' => time(),
3307
                    'data' => $data,
3308
                ],
3309
                'increment_id = %i',
3310
                $userCacheId['increment_id']
3311
            );
3312
        /* USELESS
3313
        } else {
3314
            DB::update(
3315
                prefixTable('cache_tree'),
3316
                [
3317
                    $field_update => $data,
3318
                ],
3319
                'increment_id = %i',
3320
                $userCacheId['increment_id']
3321
            );*/
3322
        }
3323
    }
3324
}
3325
3326
/**
3327
 * Permits to calculate a %
3328
 *
3329
 * @param float $nombre
3330
 * @param float $total
3331
 * @param float $pourcentage
3332
 * @return float
3333
 */
3334
function pourcentage(float $nombre, float $total, float $pourcentage): float
3335
{ 
3336
    $resultat = ($nombre/$total) * $pourcentage;
3337
    return round($resultat);
3338
}
3339
3340
/**
3341
 * Load the folders list from the cache
3342
 *
3343
 * @param string $fieldName
3344
 * @param string $sessionName
3345
 * @param boolean $forceRefresh
3346
 * @return array
3347
 */
3348
function loadFoldersListByCache(
3349
    string $fieldName,
3350
    string $sessionName,
3351
    bool $forceRefresh = false
3352
): array
3353
{
3354
    // Case when refresh is EXPECTED / MANDATORY
3355
    if ($forceRefresh === true) {
3356
        return [
3357
            'state' => false,
3358
            'data' => [],
3359
        ];
3360
    }
3361
    
3362
    $session = SessionManager::getSession();
3363
3364
    // Get last folder update
3365
    $lastFolderChange = DB::queryfirstrow(
3366
        'SELECT valeur FROM ' . prefixTable('misc') . '
3367
        WHERE type = %s AND intitule = %s',
3368
        'timestamp',
3369
        'last_folder_change'
3370
    );
3371
    if (DB::count() === 0) {
3372
        $lastFolderChange['valeur'] = 0;
3373
    }
3374
3375
    // Case when an update in the tree has been done
3376
    // Refresh is then mandatory
3377
    if ((int) $lastFolderChange['valeur'] > (int) (null !== $session->get('user-tree_last_refresh_timestamp') ? $session->get('user-tree_last_refresh_timestamp') : 0)) {
3378
        return [
3379
            'state' => false,
3380
            'data' => [],
3381
        ];
3382
    }
3383
3384
    // Does this user has the tree structure in session?
3385
    // If yes then use it
3386
    if (count(null !== $session->get('user-folders_list') ? $session->get('user-folders_list') : []) > 0) {
3387
        return [
3388
            'state' => true,
3389
            'data' => json_encode($session->get('user-folders_list')),
3390
        ];
3391
    }
3392
3393
    // Does this user has a tree cache
3394
    $userCacheTree = DB::queryfirstrow(
3395
        'SELECT '.$fieldName.'
3396
        FROM ' . prefixTable('cache_tree') . '
3397
        WHERE user_id = %i',
3398
        $session->get('user-id')
3399
    );
3400
    if (empty($userCacheTree[$fieldName]) === false && $userCacheTree[$fieldName] !== '[]') {
3401
        SessionManager::addRemoveFromSessionAssociativeArray(
3402
            'user-folders_list',
3403
            [$userCacheTree[$fieldName]],
3404
            'add'
3405
        );
3406
        return [
3407
            'state' => true,
3408
            'data' => $userCacheTree[$fieldName],
3409
        ];
3410
    }
3411
3412
    return [
3413
        'state' => false,
3414
        'data' => [],
3415
    ];
3416
}
3417
3418
3419
/**
3420
 * Permits to refresh the categories of folders
3421
 *
3422
 * @param array $folderIds
3423
 * @return void
3424
 */
3425
function handleFoldersCategories(
3426
    array $folderIds
3427
)
3428
{
3429
    // Load class DB
3430
    loadClasses('DB');
3431
3432
    $arr_data = array();
3433
3434
    // force full list of folders
3435
    if (count($folderIds) === 0) {
3436
        $folderIds = DB::queryFirstColumn(
3437
            'SELECT id
3438
            FROM ' . prefixTable('nested_tree') . '
3439
            WHERE personal_folder=%i',
3440
            0
3441
        );
3442
    }
3443
3444
    // Get complexity
3445
    defineComplexity();
3446
3447
    // update
3448
    foreach ($folderIds as $folder) {
3449
        // Do we have Categories
3450
        // get list of associated Categories
3451
        $arrCatList = array();
3452
        $rows_tmp = DB::query(
3453
            'SELECT c.id, c.title, c.level, c.type, c.masked, c.order, c.encrypted_data, c.role_visibility, c.is_mandatory,
3454
            f.id_category AS category_id
3455
            FROM ' . prefixTable('categories_folders') . ' AS f
3456
            INNER JOIN ' . prefixTable('categories') . ' AS c ON (f.id_category = c.parent_id)
3457
            WHERE id_folder=%i',
3458
            $folder
3459
        );
3460
        if (DB::count() > 0) {
3461
            foreach ($rows_tmp as $row) {
3462
                $arrCatList[$row['id']] = array(
3463
                    'id' => $row['id'],
3464
                    'title' => $row['title'],
3465
                    'level' => $row['level'],
3466
                    'type' => $row['type'],
3467
                    'masked' => $row['masked'],
3468
                    'order' => $row['order'],
3469
                    'encrypted_data' => $row['encrypted_data'],
3470
                    'role_visibility' => $row['role_visibility'],
3471
                    'is_mandatory' => $row['is_mandatory'],
3472
                    'category_id' => $row['category_id'],
3473
                );
3474
            }
3475
        }
3476
        $arr_data['categories'] = $arrCatList;
3477
3478
        // Now get complexity
3479
        $valTemp = '';
3480
        $data = DB::queryFirstRow(
3481
            'SELECT valeur
3482
            FROM ' . prefixTable('misc') . '
3483
            WHERE type = %s AND intitule=%i',
3484
            'complex',
3485
            $folder
3486
        );
3487
        if (DB::count() > 0 && empty($data['valeur']) === false) {
3488
            $valTemp = array(
3489
                'value' => $data['valeur'],
3490
                'text' => TP_PW_COMPLEXITY[$data['valeur']][1],
3491
            );
3492
        }
3493
        $arr_data['complexity'] = $valTemp;
3494
3495
        // Now get Roles
3496
        $valTemp = '';
3497
        $rows_tmp = DB::query(
3498
            'SELECT t.title
3499
            FROM ' . prefixTable('roles_values') . ' as v
3500
            INNER JOIN ' . prefixTable('roles_title') . ' as t ON (v.role_id = t.id)
3501
            WHERE v.folder_id = %i
3502
            GROUP BY title',
3503
            $folder
3504
        );
3505
        foreach ($rows_tmp as $record) {
3506
            $valTemp .= (empty($valTemp) === true ? '' : ' - ') . $record['title'];
3507
        }
3508
        $arr_data['visibilityRoles'] = $valTemp;
3509
3510
        // now save in DB
3511
        DB::update(
3512
            prefixTable('nested_tree'),
3513
            array(
3514
                'categories' => json_encode($arr_data),
3515
            ),
3516
            'id = %i',
3517
            $folder
3518
        );
3519
    }
3520
}
3521
3522
/**
3523
 * List all users that have specific roles
3524
 *
3525
 * @param array $roles
3526
 * @return array
3527
 */
3528
function getUsersWithRoles(
3529
    array $roles
3530
): array
3531
{
3532
    $session = SessionManager::getSession();
3533
    $arrUsers = array();
3534
3535
    foreach ($roles as $role) {
3536
        // loop on users and check if user has this role
3537
        $rows = DB::query(
3538
            'SELECT id, fonction_id
3539
            FROM ' . prefixTable('users') . '
3540
            WHERE id != %i AND admin = 0 AND fonction_id IS NOT NULL AND fonction_id != ""',
3541
            $session->get('user-id')
3542
        );
3543
        foreach ($rows as $user) {
3544
            $userRoles = is_null($user['fonction_id']) === false && empty($user['fonction_id']) === false ? explode(';', $user['fonction_id']) : [];
3545
            if (in_array($role, $userRoles, true) === true) {
3546
                array_push($arrUsers, $user['id']);
3547
            }
3548
        }
3549
    }
3550
3551
    return $arrUsers;
3552
}
3553
3554
3555
/**
3556
 * Get all users informations
3557
 *
3558
 * @param integer $userId
3559
 * @return array
3560
 */
3561
function getFullUserInfos(
3562
    int $userId
3563
): array
3564
{
3565
    if (empty($userId) === true) {
3566
        return array();
3567
    }
3568
3569
    $val = DB::queryfirstrow(
3570
        'SELECT *
3571
        FROM ' . prefixTable('users') . '
3572
        WHERE id = %i',
3573
        $userId
3574
    );
3575
3576
    return $val;
3577
}
3578
3579
/**
3580
 * Is required an upgrade
3581
 *
3582
 * @return boolean
3583
 */
3584
function upgradeRequired(): bool
3585
{
3586
    // Get settings.php
3587
    include_once __DIR__. '/../includes/config/settings.php';
3588
3589
    // Get timestamp in DB
3590
    $val = DB::queryfirstrow(
3591
        'SELECT valeur
3592
        FROM ' . prefixTable('misc') . '
3593
        WHERE type = %s AND intitule = %s',
3594
        'admin',
3595
        'upgrade_timestamp'
3596
    );
3597
    
3598
    // if not exists then error
3599
    if (is_null($val) === true || count($val) === 0 || defined('UPGRADE_MIN_DATE') === false) return true;
3600
3601
    // if empty or too old then error
3602
    if (empty($val['valeur']) === true || (int) $val['valeur'] < (int) UPGRADE_MIN_DATE) {
3603
        return true;
3604
    }
3605
3606
    return false;
3607
}
3608
3609
/**
3610
 * Permits to change the user keys on his demand
3611
 *
3612
 * @param integer $userId
3613
 * @param string $passwordClear
3614
 * @param integer $nbItemsToTreat
3615
 * @param string $encryptionKey
3616
 * @param boolean $deleteExistingKeys
3617
 * @param boolean $sendEmailToUser
3618
 * @param boolean $encryptWithUserPassword
3619
 * @param boolean $generate_user_new_password
3620
 * @param string $emailBody
3621
 * @param boolean $user_self_change
3622
 * @param string $recovery_public_key
3623
 * @param string $recovery_private_key
3624
 * @return string
3625
 */
3626
function handleUserKeys(
3627
    int $userId,
3628
    string $passwordClear,
3629
    int $nbItemsToTreat,
3630
    string $encryptionKey = '',
3631
    bool $deleteExistingKeys = false,
3632
    bool $sendEmailToUser = true,
3633
    bool $encryptWithUserPassword = false,
3634
    bool $generate_user_new_password = false,
3635
    string $emailBody = '',
3636
    bool $user_self_change = false,
3637
    string $recovery_public_key = '',
3638
    string $recovery_private_key = ''
3639
): string
3640
{
3641
    $session = SessionManager::getSession();
3642
    $lang = new Language(); 
3643
3644
    // prepapre background tasks for item keys generation        
3645
    $userTP = DB::queryFirstRow(
3646
        'SELECT pw, public_key, private_key
3647
        FROM ' . prefixTable('users') . '
3648
        WHERE id = %i',
3649
        TP_USER_ID
3650
    );
3651
    if (DB::count() === 0) {
3652
        return prepareExchangedData(
3653
            array(
3654
                'error' => true,
3655
                'message' => 'User not exists',
3656
            ),
3657
            'encode'
3658
        );
3659
    }
3660
3661
    // Do we need to generate new user password
3662
    if ($generate_user_new_password === true) {
3663
        // Generate a new password
3664
        $passwordClear = GenerateCryptKey(20, false, true, true, false, true);
3665
    }
3666
3667
    // Hash the password
3668
    $pwdlib = new PasswordLib();
3669
    $hashedPassword = $pwdlib->createPasswordHash($passwordClear);
3670
    if ($pwdlib->verifyPasswordHash($passwordClear, $hashedPassword) === false) {
3671
        return prepareExchangedData(
3672
            array(
3673
                'error' => true,
3674
                'message' => $lang->get('pw_hash_not_correct'),
3675
            ),
3676
            'encode'
3677
        );
3678
    }
3679
3680
    // Generate new keys
3681
    if ($user_self_change === true && empty($recovery_public_key) === false && empty($recovery_private_key) === false){
3682
        $userKeys = [
3683
            'public_key' => $recovery_public_key,
3684
            'private_key_clear' => $recovery_private_key,
3685
            'private_key' => encryptPrivateKey($passwordClear, $recovery_private_key),
3686
        ];
3687
    } else {
3688
        $userKeys = generateUserKeys($passwordClear);
3689
    }
3690
3691
    // Save in DB
3692
    DB::update(
3693
        prefixTable('users'),
3694
        array(
3695
            'pw' => $hashedPassword,
3696
            'public_key' => $userKeys['public_key'],
3697
            'private_key' => $userKeys['private_key'],
3698
        ),
3699
        'id=%i',
3700
        $userId
3701
    );
3702
3703
    // update session too
3704
    if ($userId === $session->get('user-id')) {
3705
        $session->set('user-private_key', $userKeys['private_key_clear']);
3706
        $session->set('user-public_key', $userKeys['public_key']);
3707
    }
3708
3709
    // Manage empty encryption key
3710
    // Let's take the user's password if asked and if no encryption key provided
3711
    $encryptionKey = $encryptWithUserPassword === true && empty($encryptionKey) === true ? $passwordClear : $encryptionKey;
3712
3713
    // Create process
3714
    DB::insert(
3715
        prefixTable('processes'),
3716
        array(
3717
            'created_at' => time(),
3718
            'process_type' => 'create_user_keys',
3719
            'arguments' => json_encode([
3720
                'new_user_id' => (int) $userId,
3721
                'new_user_pwd' => cryption($passwordClear, '','encrypt')['string'],
3722
                'new_user_code' => cryption(empty($encryptionKey) === true ? uniqidReal(20) : $encryptionKey, '','encrypt')['string'],
3723
                'owner_id' => (int) TP_USER_ID,
3724
                'creator_pwd' => $userTP['pw'],
3725
                'send_email' => $sendEmailToUser === true ? 1 : 0,
3726
                'otp_provided_new_value' => 1,
3727
                'email_body' => empty($emailBody) === true ? '' : $lang->get($emailBody),
3728
                'user_self_change' => $user_self_change === true ? 1 : 0,
3729
            ]),
3730
            'updated_at' => '',
3731
            'finished_at' => '',
3732
            'output' => '',
3733
        )
3734
    );
3735
    $processId = DB::insertId();
3736
3737
    // Delete existing keys
3738
    if ($deleteExistingKeys === true) {
3739
        deleteUserObjetsKeys(
3740
            (int) $userId,
3741
        );
3742
    }
3743
3744
    // Create tasks
3745
    createUserTasks($processId, $nbItemsToTreat);
3746
3747
    // update user's new status
3748
    DB::update(
3749
        prefixTable('users'),
3750
        [
3751
            'is_ready_for_usage' => 0,
3752
            'otp_provided' => 1,
3753
            'ongoing_process_id' => $processId,
3754
            'special' => 'generate-keys',
3755
        ],
3756
        'id=%i',
3757
        $userId
3758
    );
3759
3760
    return prepareExchangedData(
3761
        array(
3762
            'error' => false,
3763
            'message' => '',
3764
        ),
3765
        'encode'
3766
    );
3767
}
3768
3769
/**
3770
 * Permits to generate a new password for a user
3771
 *
3772
 * @param integer $processId
3773
 * @param integer $nbItemsToTreat
3774
 * @return void
3775
 
3776
 */
3777
function createUserTasks($processId, $nbItemsToTreat): void
3778
{
3779
    DB::insert(
3780
        prefixTable('processes_tasks'),
3781
        array(
3782
            'process_id' => $processId,
3783
            'created_at' => time(),
3784
            'task' => json_encode([
3785
                'step' => 'step0',
3786
                'index' => 0,
3787
                'nb' => $nbItemsToTreat,
3788
            ]),
3789
        )
3790
    );
3791
3792
    DB::insert(
3793
        prefixTable('processes_tasks'),
3794
        array(
3795
            'process_id' => $processId,
3796
            'created_at' => time(),
3797
            'task' => json_encode([
3798
                'step' => 'step10',
3799
                'index' => 0,
3800
                'nb' => $nbItemsToTreat,
3801
            ]),
3802
        )
3803
    );
3804
3805
    DB::insert(
3806
        prefixTable('processes_tasks'),
3807
        array(
3808
            'process_id' => $processId,
3809
            'created_at' => time(),
3810
            'task' => json_encode([
3811
                'step' => 'step20',
3812
                'index' => 0,
3813
                'nb' => $nbItemsToTreat,
3814
            ]),
3815
        )
3816
    );
3817
3818
    DB::insert(
3819
        prefixTable('processes_tasks'),
3820
        array(
3821
            'process_id' => $processId,
3822
            'created_at' => time(),
3823
            'task' => json_encode([
3824
                'step' => 'step30',
3825
                'index' => 0,
3826
                'nb' => $nbItemsToTreat,
3827
            ]),
3828
        )
3829
    );
3830
3831
    DB::insert(
3832
        prefixTable('processes_tasks'),
3833
        array(
3834
            'process_id' => $processId,
3835
            'created_at' => time(),
3836
            'task' => json_encode([
3837
                'step' => 'step40',
3838
                'index' => 0,
3839
                'nb' => $nbItemsToTreat,
3840
            ]),
3841
        )
3842
    );
3843
3844
    DB::insert(
3845
        prefixTable('processes_tasks'),
3846
        array(
3847
            'process_id' => $processId,
3848
            'created_at' => time(),
3849
            'task' => json_encode([
3850
                'step' => 'step50',
3851
                'index' => 0,
3852
                'nb' => $nbItemsToTreat,
3853
            ]),
3854
        )
3855
    );
3856
3857
    DB::insert(
3858
        prefixTable('processes_tasks'),
3859
        array(
3860
            'process_id' => $processId,
3861
            'created_at' => time(),
3862
            'task' => json_encode([
3863
                'step' => 'step60',
3864
                'index' => 0,
3865
                'nb' => $nbItemsToTreat,
3866
            ]),
3867
        )
3868
    );
3869
}
3870
3871
/**
3872
 * Permeits to check the consistency of date versus columns definition
3873
 *
3874
 * @param string $table
3875
 * @param array $dataFields
3876
 * @return array
3877
 */
3878
function validateDataFields(
3879
    string $table,
3880
    array $dataFields
3881
): array
3882
{
3883
    // Get table structure
3884
    $result = DB::query(
3885
        "SELECT `COLUMN_NAME`, `CHARACTER_MAXIMUM_LENGTH` FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = '%l' AND TABLE_NAME = '%l';",
3886
        DB_NAME,
3887
        $table
3888
    );
3889
3890
    foreach ($result as $row) {
3891
        $field = $row['COLUMN_NAME'];
3892
        $maxLength = is_null($row['CHARACTER_MAXIMUM_LENGTH']) === false ? (int) $row['CHARACTER_MAXIMUM_LENGTH'] : '';
3893
3894
        if (isset($dataFields[$field]) === true && is_array($dataFields[$field]) === false && empty($maxLength) === false) {
3895
            if (strlen((string) $dataFields[$field]) > $maxLength) {
3896
                return [
3897
                    'state' => false,
3898
                    'field' => $field,
3899
                    'maxLength' => $maxLength,
3900
                    'currentLength' => strlen((string) $dataFields[$field]),
3901
                ];
3902
            }
3903
        }
3904
    }
3905
    
3906
    return [
3907
        'state' => true,
3908
        'message' => '',
3909
    ];
3910
}
3911
3912
/**
3913
 * Adapt special characters sanitized during filter_var with option FILTER_SANITIZE_SPECIAL_CHARS operation
3914
 *
3915
 * @param string $string
3916
 * @return string
3917
 */
3918
function filterVarBack(string $string): string
3919
{
3920
    $arr = [
3921
        '&#060;' => '<',
3922
        '&#062;' => '>',
3923
        '&#034;' => '"',
3924
        '&#039;' => "'",
3925
        '&#038;' => '&',
3926
    ];
3927
3928
    foreach ($arr as $key => $value) {
3929
        $string = str_replace($key, $value, $string);
3930
    }
3931
3932
    return $string;
3933
}
3934
3935
/**
3936
 * 
3937
 */
3938
function storeTask(
3939
    string $taskName,
3940
    int $user_id,
3941
    int $is_personal_folder,
3942
    int $folder_destination_id,
3943
    int $item_id,
3944
    string $object_keys
3945
)
3946
{
3947
    if (in_array($taskName, ['item_copy', 'new_item', 'update_item'])) {
3948
        // Create process
3949
        DB::insert(
3950
            prefixTable('processes'),
3951
            array(
3952
                'created_at' => time(),
3953
                'process_type' => $taskName,
3954
                'arguments' => json_encode([
3955
                    'item_id' => $item_id,
3956
                    'object_key' => $object_keys,
3957
                ]),
3958
                'updated_at' => '',
3959
                'finished_at' => '',
3960
                'output' => '',
3961
                'item_id' => $item_id,
3962
            )
3963
        );
3964
        $processId = DB::insertId();
3965
3966
        // Create tasks
3967
        // 1- Create password sharekeys for users of this new ITEM
3968
        DB::insert(
3969
            prefixTable('processes_tasks'),
3970
            array(
3971
                'process_id' => $processId,
3972
                'created_at' => time(),
3973
                'task' => json_encode([
3974
                    'step' => 'create_users_pwd_key',
3975
                    'index' => 0,
3976
                ]),
3977
            )
3978
        );
3979
3980
        // 2- Create fields sharekeys for users of this new ITEM
3981
        DB::insert(
3982
            prefixTable('processes_tasks'),
3983
            array(
3984
                'process_id' => $processId,
3985
                'created_at' => time(),
3986
                'task' => json_encode([
3987
                    'step' => 'create_users_fields_key',
3988
                    'index' => 0,
3989
                ]),
3990
            )
3991
        );
3992
3993
        // 3- Create files sharekeys for users of this new ITEM
3994
        DB::insert(
3995
            prefixTable('processes_tasks'),
3996
            array(
3997
                'process_id' => $processId,
3998
                'created_at' => time(),
3999
                'task' => json_encode([
4000
                    'step' => 'create_users_files_key',
4001
                    'index' => 0,
4002
                ]),
4003
            )
4004
        );
4005
    }
4006
}
4007
4008
/**
4009
 * Return PHP binary path
4010
 *
4011
 * @return string
4012
 */
4013
function getPHPBinary(): string
4014
{
4015
    // Get PHP binary path
4016
    $phpBinaryFinder = new PhpExecutableFinder();
4017
    $phpBinaryPath = $phpBinaryFinder->find();
4018
    return $phpBinaryPath === false ? 'false' : $phpBinaryPath;
4019
}
4020
4021
4022
4023
/**
4024
 * Delete unnecessary keys for personal items
4025
 *
4026
 * @param boolean $allUsers
4027
 * @param integer $user_id
4028
 * @return void
4029
 */
4030
function purgeUnnecessaryKeys(bool $allUsers = true, int $user_id=0)
4031
{
4032
    if ($allUsers === true) {
4033
        // Load class DB
4034
        if (class_exists('DB') === false) {
4035
            loadClasses('DB');
4036
        }
4037
4038
        $users = DB::query(
4039
            'SELECT id
4040
            FROM ' . prefixTable('users') . '
4041
            WHERE id NOT IN ('.OTV_USER_ID.', '.TP_USER_ID.', '.SSH_USER_ID.', '.API_USER_ID.')
4042
            ORDER BY login ASC'
4043
        );
4044
        foreach ($users as $user) {
4045
            purgeUnnecessaryKeysForUser((int) $user['id']);
4046
        }
4047
    } else {
4048
        purgeUnnecessaryKeysForUser((int) $user_id);
4049
    }
4050
}
4051
4052
/**
4053
 * Delete unnecessary keys for personal items
4054
 *
4055
 * @param integer $user_id
4056
 * @return void
4057
 */
4058
function purgeUnnecessaryKeysForUser(int $user_id=0)
4059
{
4060
    if ($user_id === 0) {
4061
        return;
4062
    }
4063
4064
    // Load class DB
4065
    loadClasses('DB');
4066
4067
    $personalItems = DB::queryFirstColumn(
4068
        'SELECT id
4069
        FROM ' . prefixTable('items') . ' AS i
4070
        INNER JOIN ' . prefixTable('log_items') . ' AS li ON li.id_item = i.id
4071
        WHERE i.perso = 1 AND li.action = "at_creation" AND li.id_user IN (%i, '.TP_USER_ID.')',
4072
        $user_id
4073
    );
4074
    if (count($personalItems) > 0) {
4075
        // Item keys
4076
        DB::delete(
4077
            prefixTable('sharekeys_items'),
4078
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4079
            $personalItems,
4080
            $user_id
4081
        );
4082
        // Files keys
4083
        DB::delete(
4084
            prefixTable('sharekeys_files'),
4085
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4086
            $personalItems,
4087
            $user_id
4088
        );
4089
        // Fields keys
4090
        DB::delete(
4091
            prefixTable('sharekeys_fields'),
4092
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4093
            $personalItems,
4094
            $user_id
4095
        );
4096
        // Logs keys
4097
        DB::delete(
4098
            prefixTable('sharekeys_logs'),
4099
            'object_id IN %li AND user_id NOT IN (%i, '.TP_USER_ID.')',
4100
            $personalItems,
4101
            $user_id
4102
        );
4103
    }
4104
}
4105
4106
/**
4107
 * Generate recovery keys file
4108
 *
4109
 * @param integer $userId
4110
 * @param array $SETTINGS
4111
 * @return string
4112
 */
4113
function handleUserRecoveryKeysDownload(int $userId, array $SETTINGS):string
4114
{
4115
    $session = SessionManager::getSession();
4116
    // Check if user exists
4117
    $userInfo = DB::queryFirstRow(
4118
        'SELECT pw, public_key, private_key, login, name
4119
        FROM ' . prefixTable('users') . '
4120
        WHERE id = %i',
4121
        $userId
4122
    );
4123
4124
    if (DB::count() > 0) {
4125
        $now = (int) time();
4126
4127
        // Prepare file content
4128
        $export_value = file_get_contents(__DIR__."/../includes/core/teampass_ascii.txt")."\n".
4129
            "Generation date: ".date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now)."\n\n".
4130
            "RECOVERY KEYS - Not to be shared - To be store safely\n\n".
4131
            "Public Key:\n".$userInfo['public_key']."\n\n".
4132
            "Private Key:\n".decryptPrivateKey($session->get('user-password'), $userInfo['private_key'])."\n\n";
4133
4134
        // Update user's keys_recovery_time
4135
        DB::update(
4136
            prefixTable('users'),
4137
            [
4138
                'keys_recovery_time' => $now,
4139
            ],
4140
            'id=%i',
4141
            $userId
4142
        );
4143
        $session->set('user-keys_recovery_time', $now);
4144
4145
        //Log into DB the user's disconnection
4146
        logEvents($SETTINGS, 'user_mngt', 'at_user_keys_download', (string) $userId, $userInfo['login']);
4147
        
4148
        // Return data
4149
        return prepareExchangedData(
4150
            array(
4151
                'error' => false,
4152
                'datetime' => date($SETTINGS['date_format'] . ' ' . $SETTINGS['time_format'], $now),
4153
                'timestamp' => $now,
4154
                'content' => base64_encode($export_value),
4155
                'login' => $userInfo['login'],
4156
            ),
4157
            'encode'
4158
        );
4159
    }
4160
4161
    return prepareExchangedData(
4162
        array(
4163
            'error' => true,
4164
            'datetime' => '',
4165
        ),
4166
        'encode'
4167
    );
4168
}
4169
4170
/**
4171
 * Permits to load expected classes
4172
 *
4173
 * @param string $className
4174
 * @return void
4175
 */
4176
function loadClasses(string $className = ''): void
4177
{
4178
    require_once __DIR__. '/../includes/config/include.php';
4179
    require_once __DIR__. '/../includes/config/settings.php';
4180
    require_once __DIR__.'/../vendor/autoload.php';
4181
4182
    if (defined('DB_PASSWD_CLEAR') === false) {
4183
        define('DB_PASSWD_CLEAR', defuseReturnDecrypted(DB_PASSWD, []));
4184
    }
4185
4186
    if (empty($className) === false) {
4187
        // Load class DB
4188
        if ((string) $className === 'DB') {
4189
            //Connect to DB
4190
            DB::$host = DB_HOST;
4191
            DB::$user = DB_USER;
4192
            DB::$password = DB_PASSWD_CLEAR;
4193
            DB::$dbName = DB_NAME;
4194
            DB::$port = DB_PORT;
4195
            DB::$encoding = DB_ENCODING;
4196
            DB::$ssl = DB_SSL;
4197
            DB::$connect_options = DB_CONNECT_OPTIONS;
4198
        }
4199
    }
4200
4201
}
4202
4203
/**
4204
 * Returns the page the user is visiting.
4205
 *
4206
 * @return string The page name
4207
 */
4208
function getCurrectPage($SETTINGS)
4209
{
4210
    
4211
    $request = SymfonyRequest::createFromGlobals();
4212
4213
    // Parse the url
4214
    parse_str(
4215
        substr(
4216
            (string) $request->getRequestUri(),
4217
            strpos((string) $request->getRequestUri(), '?') + 1
4218
        ),
4219
        $result
4220
    );
4221
4222
    return $result['page'];
4223
}
4224
4225
/**
4226
 * Permits to return value if set
4227
 *
4228
 * @param string|int $value
4229
 * @param string|int|null $retFalse
4230
 * @param string|int $retTrue
4231
 * @return mixed
4232
 */
4233
function returnIfSet($value, $retFalse = '', $retTrue = null): mixed
4234
{
4235
4236
    return isset($value) === true ? ($retTrue === null ? $value : $retTrue) : $retFalse;
4237
}