Passed
Push — master ( 9d24f3...ec21d0 )
by Nils
05:30
created

recursiveChmod()   B

Complexity

Conditions 7
Paths 6

Size

Total Lines 40
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 7
eloc 17
c 2
b 0
f 0
nc 6
nop 3
dl 0
loc 40
rs 8.8333
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
30
if (isset($_SESSION['CPM']) === false || (int) $_SESSION['CPM'] !== 1) {
31
    //die('Hacking attempt...');
32
}
33
34
// Load config if $SETTINGS not defined
35
if (isset($SETTINGS['cpassman_dir']) === false || empty($SETTINGS['cpassman_dir']) === true) {
36
    include_once __DIR__ . '/../includes/config/tp.config.php';
37
}
38
39
header('Content-type: text/html; charset=utf-8');
40
header('Cache-Control: no-cache, must-revalidate');
41
/**
42
 * Convert language code to string.
43
 *
44
 * @param string $string String to get
45
 */
46
function langHdl(string $string): string
47
{
48
    if (empty($string) === true) {
49
        // Manage error
50
        return 'ERROR in language strings!';
51
    }
52
53
    // Load superglobal
54
    include_once __DIR__.'/../includes/libraries/protect/SuperGlobal/SuperGlobal.php';
55
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
56
    // Get language string
57
    $session_language = $superGlobal->get(trim($string), 'SESSION', 'lang');
58
    if (is_null($session_language) === true) {
59
        /* 
60
            Load the English version to $_SESSION so we don't 
61
            return bad JSON (multiple includes add BOM characters to the json returned 
62
            which makes jquery unhappy on the UI, especially on the log page)
63
            and improve performance by avoiding to include the file for every missing strings.
64
        */
65
        if (isset($_SESSION['teampass']) === false || isset($_SESSION['teampass']['en_lang'][trim($string)]) === false) {
66
            $_SESSION['teampass']['en_lang'] = include_once __DIR__. '/../includes/language/english.php';
67
            $session_language = isset($_SESSION['teampass']['en_lang'][trim($string)]) === false ? '' : $_SESSION['teampass']['en_lang'][trim($string)];
68
        } else {
69
            $session_language = $_SESSION['teampass']['en_lang'][trim($string)];
70
        }
71
    }
72
    // If after all this, we still don't have the string even in english (especially with old logs), return the language code
73
    if (empty($session_language) === true) {
74
        return trim($string);
75
    }
76
    //return (string) str_replace("'",  "&apos;", $session_language);
77
    return (string) $session_language;
78
}
79
80
/**
81
 * genHash().
82
 *
83
 * Generate a hash for user login
84
 *
85
 * @param string $password What password
86
 * @param string $cost     What cost
87
 *
88
 * @return string|void
89
 */
90
function bCrypt(
91
    string $password,
92
    string $cost
93
): ?string
94
{
95
    $salt = sprintf('$2y$%02d$', $cost);
96
    if (function_exists('openssl_random_pseudo_bytes')) {
97
        $salt .= bin2hex(openssl_random_pseudo_bytes(11));
98
    } else {
99
        $chars = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
100
        for ($i = 0; $i < 22; ++$i) {
101
            $salt .= $chars[mt_rand(0, 63)];
102
        }
103
    }
104
105
    return crypt($password, $salt);
106
}
107
108
/**
109
 * Defuse cryption function.
110
 *
111
 * @param string $message   what to de/crypt
112
 * @param string $ascii_key key to use
113
 * @param string $type      operation to perform
114
 * @param array  $SETTINGS  Teampass settings
115
 *
116
 * @return array
117
 */
118
function cryption(string $message, string $ascii_key, string $type, ?array $SETTINGS = []): array
119
{
120
    $ascii_key = empty($ascii_key) === true ? file_get_contents(SECUREPATH.'/'.SECUREFILE) : $ascii_key;
121
    $err = false;
122
    
123
    $path = __DIR__.'/../includes/libraries/Encryption/Encryption/';
124
125
    include_once $path . 'Exception/CryptoException.php';
126
    include_once $path . 'Exception/BadFormatException.php';
127
    include_once $path . 'Exception/EnvironmentIsBrokenException.php';
128
    include_once $path . 'Exception/IOException.php';
129
    include_once $path . 'Exception/WrongKeyOrModifiedCiphertextException.php';
130
    include_once $path . 'Crypto.php';
131
    include_once $path . 'Encoding.php';
132
    include_once $path . 'DerivedKeys.php';
133
    include_once $path . 'Key.php';
134
    include_once $path . 'KeyOrPassword.php';
135
    include_once $path . 'File.php';
136
    include_once $path . 'RuntimeTests.php';
137
    include_once $path . 'KeyProtectedByPassword.php';
138
    include_once $path . 'Core.php';
139
    
140
    // convert KEY
141
    $key = \Defuse\Crypto\Key::loadFromAsciiSafeString($ascii_key);
142
    try {
143
        if ($type === 'encrypt') {
144
            $text = \Defuse\Crypto\Crypto::encrypt($message, $key);
145
        } elseif ($type === 'decrypt') {
146
            $text = \Defuse\Crypto\Crypto::decrypt($message, $key);
147
        }
148
    } catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
149
        $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.';
150
    } catch (Defuse\Crypto\Exception\BadFormatException $ex) {
151
        $err = $ex;
152
    } catch (Defuse\Crypto\Exception\EnvironmentIsBrokenException $ex) {
153
        $err = $ex;
154
    } catch (Defuse\Crypto\Exception\CryptoException $ex) {
155
        $err = $ex;
156
    } catch (Defuse\Crypto\Exception\IOException $ex) {
157
        $err = $ex;
158
    }
159
    //echo \Defuse\Crypto\Crypto::decrypt($message, $key).' ## ';
160
161
    return [
162
        'string' => $text ?? '',
163
        'error' => $err,
164
    ];
165
}
166
167
/**
168
 * Generating a defuse key.
169
 *
170
 * @return string
171
 */
172
function defuse_generate_key()
173
{
174
    // load PhpEncryption library
175
    if (file_exists('../includes/config/tp.config.php') === true) {
176
        $path = '../includes/libraries/Encryption/Encryption/';
177
    } elseif (file_exists('./includes/config/tp.config.php') === true) {
178
        $path = './includes/libraries/Encryption/Encryption/';
179
    } else {
180
        $path = '../includes/libraries/Encryption/Encryption/';
181
    }
182
183
    include_once $path . 'Exception/CryptoException.php';
184
    include_once $path . 'Exception/BadFormatException.php';
185
    include_once $path . 'Exception/EnvironmentIsBrokenException.php';
186
    include_once $path . 'Exception/IOException.php';
187
    include_once $path . 'Exception/WrongKeyOrModifiedCiphertextException.php';
188
    include_once $path . 'Crypto.php';
189
    include_once $path . 'Encoding.php';
190
    include_once $path . 'DerivedKeys.php';
191
    include_once $path . 'Key.php';
192
    include_once $path . 'KeyOrPassword.php';
193
    include_once $path . 'File.php';
194
    include_once $path . 'RuntimeTests.php';
195
    include_once $path . 'KeyProtectedByPassword.php';
196
    include_once $path . 'Core.php';
197
198
    $key = \Defuse\Crypto\Key::createNewRandomKey();
199
    $key = $key->saveToAsciiSafeString();
200
    return $key;
201
}
202
203
/**
204
 * Generate a Defuse personal key.
205
 *
206
 * @param string $psk psk used
207
 *
208
 * @return string
209
 */
210
function defuse_generate_personal_key(string $psk): string
211
{
212
    // load PhpEncryption library
213
    if (file_exists('../includes/config/tp.config.php') === true) {
214
        $path = '../includes/libraries/Encryption/Encryption/';
215
    } elseif (file_exists('./includes/config/tp.config.php') === true) {
216
        $path = './includes/libraries/Encryption/Encryption/';
217
    } else {
218
        $path = '../includes/libraries/Encryption/Encryption/';
219
    }
220
221
    include_once $path . 'Exception/CryptoException.php';
222
    include_once $path . 'Exception/BadFormatException.php';
223
    include_once $path . 'Exception/EnvironmentIsBrokenException.php';
224
    include_once $path . 'Exception/IOException.php';
225
    include_once $path . 'Exception/WrongKeyOrModifiedCiphertextException.php';
226
    include_once $path . 'Crypto.php';
227
    include_once $path . 'Encoding.php';
228
    include_once $path . 'DerivedKeys.php';
229
    include_once $path . 'Key.php';
230
    include_once $path . 'KeyOrPassword.php';
231
    include_once $path . 'File.php';
232
    include_once $path . 'RuntimeTests.php';
233
    include_once $path . 'KeyProtectedByPassword.php';
234
    include_once $path . 'Core.php';
235
    
236
    $protected_key = \Defuse\Crypto\KeyProtectedByPassword::createRandomPasswordProtectedKey($psk);
237
    return $protected_key->saveToAsciiSafeString(); // save this in user table
238
}
239
240
/**
241
 * Validate persoanl key with defuse.
242
 *
243
 * @param string $psk                   the user's psk
244
 * @param string $protected_key_encoded special key
245
 *
246
 * @return string
247
 */
248
function defuse_validate_personal_key(string $psk, string $protected_key_encoded): string
249
{
250
    // load PhpEncryption library
251
    if (file_exists('../includes/config/tp.config.php') === true) {
252
        $path = '../includes/libraries/Encryption/Encryption/';
253
    } elseif (file_exists('./includes/config/tp.config.php') === true) {
254
        $path = './includes/libraries/Encryption/Encryption/';
255
    } else {
256
        $path = '../includes/libraries/Encryption/Encryption/';
257
    }
258
259
    include_once $path . 'Exception/CryptoException.php';
260
    include_once $path . 'Exception/BadFormatException.php';
261
    include_once $path . 'Exception/EnvironmentIsBrokenException.php';
262
    include_once $path . 'Exception/IOException.php';
263
    include_once $path . 'Exception/WrongKeyOrModifiedCiphertextException.php';
264
    include_once $path . 'Crypto.php';
265
    include_once $path . 'Encoding.php';
266
    include_once $path . 'DerivedKeys.php';
267
    include_once $path . 'Key.php';
268
    include_once $path . 'KeyOrPassword.php';
269
    include_once $path . 'File.php';
270
    include_once $path . 'RuntimeTests.php';
271
    include_once $path . 'KeyProtectedByPassword.php';
272
    include_once $path . 'Core.php';
273
274
    try {
275
        $protected_key_encoded = \Defuse\Crypto\KeyProtectedByPassword::loadFromAsciiSafeString($protected_key_encoded);
276
        $user_key = $protected_key_encoded->unlockKey($psk);
277
        $user_key_encoded = $user_key->saveToAsciiSafeString();
278
    } catch (Defuse\Crypto\Exception\EnvironmentIsBrokenException $ex) {
279
        return 'Error - Major issue as the encryption is broken.';
280
    } catch (Defuse\Crypto\Exception\WrongKeyOrModifiedCiphertextException $ex) {
281
        return 'Error - The saltkey is not the correct one.';
282
    }
283
284
    return $user_key_encoded;
285
    // store it in session once user has entered his psk
286
}
287
288
/**
289
 * Decrypt a defuse string if encrypted.
290
 *
291
 * @param string $value Encrypted string
292
 *
293
 * @return string Decrypted string
294
 */
295
function defuseReturnDecrypted(string $value, $SETTINGS): string
296
{
297
    if (substr($value, 0, 3) === 'def') {
298
        $value = cryption($value, '', 'decrypt', $SETTINGS)['string'];
299
    }
300
301
    return $value;
302
}
303
304
/**
305
 * Trims a string depending on a specific string.
306
 *
307
 * @param string|array $chaine  what to trim
308
 * @param string       $element trim on what
309
 *
310
 * @return string
311
 */
312
function trimElement($chaine, string $element): string
313
{
314
    if (! empty($chaine)) {
315
        if (is_array($chaine) === true) {
316
            $chaine = implode(';', $chaine);
317
        }
318
        $chaine = trim($chaine);
319
        if (substr($chaine, 0, 1) === $element) {
320
            $chaine = substr($chaine, 1);
321
        }
322
        if (substr($chaine, strlen($chaine) - 1, 1) === $element) {
323
            $chaine = substr($chaine, 0, strlen($chaine) - 1);
324
        }
325
    }
326
327
    return $chaine;
328
}
329
330
/**
331
 * Permits to suppress all "special" characters from string.
332
 *
333
 * @param string $string  what to clean
334
 * @param bool   $special use of special chars?
335
 *
336
 * @return string
337
 */
338
function cleanString(string $string, bool $special = false): string
339
{
340
    // Create temporary table for special characters escape
341
    $tabSpecialChar = [];
342
    for ($i = 0; $i <= 31; ++$i) {
343
        $tabSpecialChar[] = chr($i);
344
    }
345
    array_push($tabSpecialChar, '<br />');
346
    if ((int) $special === 1) {
347
        $tabSpecialChar = array_merge($tabSpecialChar, ['</li>', '<ul>', '<ol>']);
348
    }
349
350
    return str_replace($tabSpecialChar, "\n", $string);
351
}
352
353
/**
354
 * Erro manager for DB.
355
 *
356
 * @param array $params output from query
357
 *
358
 * @return void
359
 */
360
function db_error_handler(array $params): void
361
{
362
    echo 'Error: ' . $params['error'] . "<br>\n";
363
    echo 'Query: ' . $params['query'] . "<br>\n";
364
    throw new Exception('Error - Query', 1);
365
}
366
367
/**
368
 * Identify user's rights
369
 *
370
 * @param string|array $groupesVisiblesUser  [description]
371
 * @param string|array $groupesInterditsUser [description]
372
 * @param string       $isAdmin              [description]
373
 * @param string       $idFonctions          [description]
374
 *
375
 * @return bool
376
 */
377
function identifyUserRights(
378
    $groupesVisiblesUser,
379
    $groupesInterditsUser,
380
    $isAdmin,
381
    $idFonctions,
382
    $SETTINGS
383
) {
384
    //load ClassLoader
385
    include_once $SETTINGS['cpassman_dir'] . '/sources/SplClassLoader.php';
386
    // Load superglobal
387
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
388
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
389
390
    // Load class DB
391
    if (class_exists('DB') === false) {
392
        loadClass('DB');
393
    }
394
    
395
    //Build tree
396
    $tree = new SplClassLoader('Tree\NestedTree', $SETTINGS['cpassman_dir'] . '/includes/libraries');
397
    $tree->register();
398
    $tree = new Tree\NestedTree\NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
399
400
    // Check if user is ADMINISTRATOR    
401
    (int) $isAdmin === 1 ?
402
        identAdmin(
403
            $idFonctions,
404
            $SETTINGS, /** @scrutinizer ignore-type */
405
            $tree
406
        )
407
        :
408
        identUser(
409
            $groupesVisiblesUser,
410
            $groupesInterditsUser,
411
            $idFonctions,
412
            $SETTINGS, /** @scrutinizer ignore-type */
413
            $tree
414
        );
415
416
    // update user's timestamp
417
    DB::update(
418
        prefixTable('users'),
419
        [
420
            'timestamp' => time(),
421
        ],
422
        'id=%i',
423
        $superGlobal->get('user_id', 'SESSION')
424
    );
425
426
    return true;
427
}
428
429
/**
430
 * Identify administrator.
431
 *
432
 * @param string $idFonctions Roles of user
433
 * @param array  $SETTINGS    Teampass settings
434
 * @param array  $tree        Tree of folders
435
 *
436
 * @return bool
437
 */
438
function identAdmin($idFonctions, $SETTINGS, $tree)
439
{
440
    // Load superglobal
441
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
442
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
443
    // Init
444
    $groupesVisibles = [];
445
    $superGlobal->put('personal_folders', [], 'SESSION');
446
    $superGlobal->put('groupes_visibles', [], 'SESSION');
447
    $superGlobal->put('no_access_folders', [], 'SESSION');
448
    $superGlobal->put('personal_visible_groups', [], 'SESSION');
449
    $superGlobal->put('read_only_folders', [], 'SESSION');
450
    $superGlobal->put('list_restricted_folders_for_items', [], 'SESSION');
451
    $superGlobal->put('list_folders_editable_by_role', [], 'SESSION');
452
    $superGlobal->put('list_folders_limited', [], 'SESSION');
453
    $superGlobal->put('no_access_folders', [], 'SESSION');
454
    $superGlobal->put('forbiden_pfs', [], 'SESSION');
455
    // Get superglobals
456
    $globalsUserId = $superGlobal->get('user_id', 'SESSION');
457
    $globalsVisibleFolders = $superGlobal->get('groupes_visibles', 'SESSION');
458
    $globalsPersonalVisibleFolders = $superGlobal->get('personal_visible_groups', 'SESSION');
459
    // Get list of Folders
460
    $rows = DB::query('SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i', 0);
461
    foreach ($rows as $record) {
462
        array_push($groupesVisibles, $record['id']);
463
    }
464
    $superGlobal->put('groupes_visibles', $groupesVisibles, 'SESSION');
465
    $superGlobal->put('all_non_personal_folders', $groupesVisibles, 'SESSION');
466
    // Exclude all PF
467
    $where = new WhereClause('and');
468
    // create a WHERE statement of pieces joined by ANDs
469
    $where->add('personal_folder=%i', 1);
470
    if (
471
        isset($SETTINGS['enable_pf_feature']) === true
472
        && (int) $SETTINGS['enable_pf_feature'] === 1
473
    ) {
474
        $where->add('title=%s', $globalsUserId);
475
        $where->negateLast();
476
    }
477
    // Get ID of personal folder
478
    $persfld = DB::queryfirstrow(
479
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE title = %s',
480
        $globalsUserId
481
    );
482
    if (empty($persfld['id']) === false) {
483
        if (in_array($persfld['id'], $globalsVisibleFolders) === false) {
484
            array_push($globalsVisibleFolders, $persfld['id']);
485
            array_push($globalsPersonalVisibleFolders, $persfld['id']);
486
            // get all descendants
487
            $tree = new Tree\NestedTree\NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
488
            $tree->rebuild();
489
            $tst = $tree->getDescendants($persfld['id']);
490
            foreach ($tst as $t) {
491
                array_push($globalsVisibleFolders, $t->id);
492
                array_push($globalsPersonalVisibleFolders, $t->id);
493
            }
494
        }
495
    }
496
497
    // get complete list of ROLES
498
    $tmp = explode(';', $idFonctions);
499
    $rows = DB::query(
500
        'SELECT * FROM ' . prefixTable('roles_title') . '
501
        ORDER BY title ASC'
502
    );
503
    foreach ($rows as $record) {
504
        if (! empty($record['id']) && ! in_array($record['id'], $tmp)) {
505
            array_push($tmp, $record['id']);
506
        }
507
    }
508
    $superGlobal->put('fonction_id', implode(';', $tmp), 'SESSION');
509
    $superGlobal->put('is_admin', 1, 'SESSION');
510
    // Check if admin has created Folders and Roles
511
    DB::query('SELECT * FROM ' . prefixTable('nested_tree') . '');
512
    $superGlobal->put('nb_folders', DB::count(), 'SESSION');
513
    DB::query('SELECT * FROM ' . prefixTable('roles_title'));
514
    $superGlobal->put('nb_roles', DB::count(), 'SESSION');
515
516
    return true;
517
}
518
519
/**
520
 * Permits to convert an element to array.
521
 *
522
 * @param string|array $element Any value to be returned as array
523
 *
524
 * @return array
525
 */
526
function convertToArray($element): array
527
{
528
    if (is_string($element) === true) {
529
        if (empty($element) === true) {
530
            return [];
531
        }
532
        return explode(
533
            ';',
534
            trimElement($element, ';')
535
        );
536
    }
537
    return $element;
538
}
539
540
/**
541
 * Defines the rights the user has.
542
 *
543
 * @param string|array $allowedFolders  Allowed folders
544
 * @param string|array $noAccessFolders Not allowed folders
545
 * @param string|array $userRoles       Roles of user
546
 * @param array        $SETTINGS        Teampass settings
547
 * @param object       $tree            Tree of folders
548
 * 
549
 * @return bool
550
 */
551
function identUser(
552
    $allowedFolders,
553
    $noAccessFolders,
554
    $userRoles,
555
    array $SETTINGS,
556
    object $tree
557
) {
558
    // Load superglobal
559
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
560
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
561
    // Init
562
    $superGlobal->put('groupes_visibles', [], 'SESSION');
563
    $superGlobal->put('personal_folders', [], 'SESSION');
564
    $superGlobal->put('no_access_folders', [], 'SESSION');
565
    $superGlobal->put('personal_visible_groups', [], 'SESSION');
566
    $superGlobal->put('read_only_folders', [], 'SESSION');
567
    $superGlobal->put('fonction_id', $userRoles, 'SESSION');
568
    $superGlobal->put('is_admin', 0, 'SESSION');
569
    // init
570
    $personalFolders = [];
571
    $readOnlyFolders = [];
572
    $noAccessPersonalFolders = [];
573
    $restrictedFoldersForItems = [];
574
    $foldersLimited = [];
575
    $foldersLimitedFull = [];
576
    $allowedFoldersByRoles = [];
577
    // Get superglobals
578
    $globalsUserId = $superGlobal->get('user_id', 'SESSION');
579
    $globalsPersonalFolders = $superGlobal->get('personal_folder', 'SESSION');
580
    // Ensure consistency in array format
581
    $noAccessFolders = convertToArray($noAccessFolders);
582
    $userRoles = convertToArray($userRoles);
583
    $allowedFolders = convertToArray($allowedFolders);
584
    
585
    // Get list of folders depending on Roles
586
    $arrays = identUserGetFoldersFromRoles(
587
        $userRoles,
588
        $allowedFoldersByRoles,
589
        $readOnlyFolders,
590
        $allowedFolders
591
    );
592
    $allowedFoldersByRoles = $arrays['allowedFoldersByRoles'];
593
    $readOnlyFolders = $arrays['readOnlyFolders'];
594
595
    // Does this user is allowed to see other items
596
    $inc = 0;
597
    $rows = DB::query(
598
        'SELECT id, id_tree FROM ' . prefixTable('items') . '
599
            WHERE restricted_to LIKE %ss AND inactif = %s'.
600
            (count($allowedFolders) > 0 ? ' AND id_tree NOT IN ('.implode(',', $allowedFolders).')' : ''),
601
        $globalsUserId,
602
        '0'
603
    );
604
    foreach ($rows as $record) {
605
        // Exclude restriction on item if folder is fully accessible
606
        //if (in_array($record['id_tree'], $allowedFolders) === false) {
607
            $restrictedFoldersForItems[$record['id_tree']][$inc] = $record['id'];
608
            ++$inc;
609
        //}
610
    }
611
612
    // Check for the users roles if some specific rights exist on items
613
    $rows = DB::query(
614
        'SELECT i.id_tree, r.item_id
615
        FROM ' . prefixTable('items') . ' as i
616
        INNER JOIN ' . prefixTable('restriction_to_roles') . ' as r ON (r.item_id=i.id)
617
        WHERE i.id_tree <> "" '.
618
        (count($userRoles) > 0 ? 'AND r.role_id IN %li ' : '').
619
        'ORDER BY i.id_tree ASC',
620
        $userRoles
621
    );
622
    $inc = 0;
623
    foreach ($rows as $record) {
624
        //if (isset($record['id_tree'])) {
625
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
626
            array_push($foldersLimitedFull, $record['id_tree']);
627
            ++$inc;
628
        //}
629
    }
630
631
    // Get list of Personal Folders
632
    $arrays = identUserGetPFList(
633
        $globalsPersonalFolders,
634
        $allowedFolders,
635
        $globalsUserId,
636
        $personalFolders,
637
        $noAccessPersonalFolders,
638
        $foldersLimitedFull,
639
        $allowedFoldersByRoles,
640
        array_keys($restrictedFoldersForItems),
641
        $readOnlyFolders,
642
        $noAccessFolders,
643
        isset($SETTINGS['enable_pf_feature']) === true ? $SETTINGS['enable_pf_feature'] : 0,
644
        $tree
645
    );
646
    $allowedFolders = $arrays['allowedFolders'];
647
    $personalFolders = $arrays['personalFolders'];
648
    $noAccessPersonalFolders = $arrays['noAccessPersonalFolders'];
649
650
    // Return data
651
    $superGlobal->put('all_non_personal_folders', $allowedFolders, 'SESSION');
652
    $superGlobal->put('groupes_visibles', array_unique(array_merge($allowedFolders, $personalFolders), SORT_NUMERIC), 'SESSION');
653
    $superGlobal->put('read_only_folders', $readOnlyFolders, 'SESSION');
654
    $superGlobal->put('no_access_folders', $noAccessFolders, 'SESSION');
655
    $superGlobal->put('personal_folders', $personalFolders, 'SESSION');
656
    $superGlobal->put('list_folders_limited', $foldersLimited, 'SESSION');
657
    $superGlobal->put('list_folders_editable_by_role', $allowedFoldersByRoles, 'SESSION');
658
    $superGlobal->put('list_restricted_folders_for_items', $restrictedFoldersForItems, 'SESSION');
659
    $superGlobal->put('forbiden_pfs', $noAccessPersonalFolders, 'SESSION');
660
    $superGlobal->put(
661
        'all_folders_including_no_access',
662
        array_unique(array_merge(
663
            $allowedFolders,
664
            $personalFolders,
665
            $noAccessFolders,
666
            $readOnlyFolders
667
        ), SORT_NUMERIC),
668
        'SESSION'
669
    );
670
    // Folders and Roles numbers
671
    DB::queryfirstrow('SELECT id FROM ' . prefixTable('nested_tree') . '');
672
    $superGlobal->put('nb_folders', DB::count(), 'SESSION');
673
    DB::queryfirstrow('SELECT id FROM ' . prefixTable('roles_title'));
674
    $superGlobal->put('nb_roles', DB::count(), 'SESSION');
675
    // check if change proposals on User's items
676
    if (isset($SETTINGS['enable_suggestion']) === true && (int) $SETTINGS['enable_suggestion'] === 1) {
677
        $countNewItems = DB::query(
678
            'SELECT COUNT(*)
679
            FROM ' . prefixTable('items_change') . ' AS c
680
            LEFT JOIN ' . prefixTable('log_items') . ' AS i ON (c.item_id = i.id_item)
681
            WHERE i.action = %s AND i.id_user = %i',
682
            'at_creation',
683
            $globalsUserId
684
        );
685
        $superGlobal->put('nb_item_change_proposals', $countNewItems, 'SESSION');
686
    } else {
687
        $superGlobal->put('nb_item_change_proposals', 0, 'SESSION');
688
    }
689
690
    return true;
691
}
692
693
/**
694
 * Get list of folders depending on Roles
695
 * 
696
 * @param array $userRoles
697
 * @param array $allowedFoldersByRoles
698
 * @param array $readOnlyFolders
699
 * @param array $allowedFolders
700
 * 
701
 * @return array
702
 */
703
function identUserGetFoldersFromRoles($userRoles, $allowedFoldersByRoles, $readOnlyFolders, $allowedFolders) : array
704
{
705
    $rows = DB::query(
706
        'SELECT *
707
        FROM ' . prefixTable('roles_values') . '
708
        WHERE type IN %ls'.(count($userRoles) > 0 ? ' AND role_id IN %li' : ''),
709
        ['W', 'ND', 'NE', 'NDNE', 'R'],
710
        $userRoles,
711
    );
712
    foreach ($rows as $record) {
713
        if ($record['type'] === 'R') {
714
            array_push($readOnlyFolders, $record['folder_id']);
715
        } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
716
            array_push($allowedFoldersByRoles, $record['folder_id']);
717
        }
718
    }
719
    $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
720
    $readOnlyFolders = array_unique($readOnlyFolders);
721
    // Clean arrays
722
    foreach ($allowedFoldersByRoles as $value) {
723
        $key = array_search($value, $readOnlyFolders);
724
        if ($key !== false) {
725
            unset($readOnlyFolders[$key]);
726
        }
727
    }
728
729
    return [
730
        'readOnlyFolders' => $readOnlyFolders,
731
        'allowedFoldersByRoles' => $allowedFoldersByRoles
732
    ];
733
}
734
735
/**
736
 * Get list of Personal Folders
737
 * 
738
 * @param int $globalsPersonalFolders
739
 * @param array $allowedFolders
740
 * @param int $globalsUserId
741
 * @param array $personalFolders
742
 * @param array $noAccessPersonalFolders
743
 * @param array $foldersLimitedFull
744
 * @param array $allowedFoldersByRoles
745
 * @param array $restrictedFoldersForItems
746
 * @param array $readOnlyFolders
747
 * @param array $noAccessFolders
748
 * @param int $enablePfFeature
749
 * @param object $tree
750
 * 
751
 * @return array
752
 */
753
function identUserGetPFList(
754
    $globalsPersonalFolders,
755
    $allowedFolders,
756
    $globalsUserId,
757
    $personalFolders,
758
    $noAccessPersonalFolders,
759
    $foldersLimitedFull,
760
    $allowedFoldersByRoles,
761
    $restrictedFoldersForItems,
762
    $readOnlyFolders,
763
    $noAccessFolders,
764
    $enablePfFeature,
765
    $tree
766
)
767
{
768
    if (
769
        (int) $enablePfFeature === 1
770
        && (int) $globalsPersonalFolders === 1
771
    ) {
772
        $persoFld = DB::queryfirstrow(
773
            'SELECT id
774
            FROM ' . prefixTable('nested_tree') . '
775
            WHERE title = %s AND personal_folder = %i'.
776
            (count($allowedFolders) > 0 ? ' AND id NOT IN ('.implode(',', $allowedFolders).')' : ''),
777
            $globalsUserId,
778
            1
779
        );
780
        if (empty($persoFld['id']) === false) {
781
            array_push($personalFolders, $persoFld['id']);
782
            array_push($allowedFolders, $persoFld['id']);
783
            // get all descendants
784
            $ids = $tree->getDescendants($persoFld['id'], false, false, true);
785
            foreach ($ids as $id) {
786
                //array_push($allowedFolders, $id);
787
                array_push($personalFolders, $id);
788
            }
789
        }
790
    }
791
    
792
    // Exclude all other PF
793
    $where = new WhereClause('and');
794
    $where->add('personal_folder=%i', 1);
795
    if (count($personalFolders) > 0) {
796
        $where->add('id NOT IN ('.implode(',', $personalFolders).')');
797
    }
798
    if (
799
        (int) $enablePfFeature === 1
800
        && (int) $globalsPersonalFolders === 1
801
    ) {
802
        $where->add('title=%s', $globalsUserId);
803
        $where->negateLast();
804
    }
805
    $persoFlds = DB::query(
806
        'SELECT id
807
        FROM ' . prefixTable('nested_tree') . '
808
        WHERE %l',
809
        $where
810
    );
811
    foreach ($persoFlds as $persoFldId) {
812
        array_push($noAccessPersonalFolders, $persoFldId['id']);
813
    }
814
815
    // All folders visibles
816
    $allowedFolders = array_unique(array_merge(
817
        $allowedFolders,
818
        $foldersLimitedFull,
819
        $allowedFoldersByRoles,
820
        $restrictedFoldersForItems,
821
        $readOnlyFolders
822
    ), SORT_NUMERIC);
823
    // Exclude from allowed folders all the specific user forbidden folders
824
    if (count($noAccessFolders) > 0) {
825
        $allowedFolders = array_diff($allowedFolders, $noAccessFolders);
826
    }
827
828
    return [
829
        'allowedFolders' => array_diff(array_diff($allowedFolders, $noAccessPersonalFolders), $personalFolders),
830
        'personalFolders' => $personalFolders,
831
        'noAccessPersonalFolders' => $noAccessPersonalFolders
832
    ];
833
}
834
835
836
/**
837
 * Update the CACHE table.
838
 *
839
 * @param string $action   What to do
840
 * @param array  $SETTINGS Teampass settings
841
 * @param int    $ident    Ident format
842
 * 
843
 * @return void
844
 */
845
function updateCacheTable(string $action, array $SETTINGS, ?int $ident = null): void
846
{
847
    if ($action === 'reload') {
848
        // Rebuild full cache table
849
        cacheTableRefresh($SETTINGS);
850
    } elseif ($action === 'update_value' && is_null($ident) === false) {
851
        // UPDATE an item
852
        cacheTableUpdate($SETTINGS, $ident);
853
    } elseif ($action === 'add_value' && is_null($ident) === false) {
854
        // ADD an item
855
        cacheTableAdd($SETTINGS, $ident);
856
    } elseif ($action === 'delete_value' && is_null($ident) === false) {
857
        // DELETE an item
858
        DB::delete(prefixTable('cache'), 'id = %i', $ident);
859
    }
860
}
861
862
/**
863
 * Cache table - refresh.
864
 *
865
 * @param array $SETTINGS Teampass settings
866
 * 
867
 * @return void
868
 */
869
function cacheTableRefresh(array $SETTINGS): void
870
{
871
    include_once $SETTINGS['cpassman_dir'] . '/sources/SplClassLoader.php';
872
873
    // Load class DB
874
    if (class_exists('DB') === false) {
875
        loadClass('DB');
876
    }
877
878
    //Load Tree
879
    $tree = new SplClassLoader('Tree\NestedTree', $SETTINGS['cpassman_dir'] .'/includes/libraries');
880
    $tree->register();
881
    $tree = new Tree\NestedTree\NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
882
    // truncate table
883
    DB::query('TRUNCATE TABLE ' . prefixTable('cache'));
884
    // reload date
885
    $rows = DB::query(
886
        'SELECT *
887
        FROM ' . prefixTable('items') . ' as i
888
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
889
        AND l.action = %s
890
        AND i.inactif = %i',
891
        'at_creation',
892
        0
893
    );
894
    foreach ($rows as $record) {
895
        if (empty($record['id_tree']) === false) {
896
            // Get all TAGS
897
            $tags = '';
898
            $itemTags = DB::query(
899
                'SELECT tag
900
                FROM ' . prefixTable('tags') . '
901
                WHERE item_id = %i AND tag != ""',
902
                $record['id']
903
            );
904
            foreach ($itemTags as $itemTag) {
905
                $tags .= $itemTag['tag'] . ' ';
906
            }
907
908
            // Get renewal period
909
            $resNT = DB::queryfirstrow(
910
                'SELECT renewal_period
911
                FROM ' . prefixTable('nested_tree') . '
912
                WHERE id = %i',
913
                $record['id_tree']
914
            );
915
            // form id_tree to full foldername
916
            $folder = [];
917
            $arbo = $tree->getPath($record['id_tree'], true);
918
            foreach ($arbo as $elem) {
919
                // Check if title is the ID of a user
920
                if (is_numeric($elem->title) === true) {
921
                    // Is this a User id?
922
                    $user = DB::queryfirstrow(
923
                        'SELECT id, login
924
                        FROM ' . prefixTable('users') . '
925
                        WHERE id = %i',
926
                        $elem->title
927
                    );
928
                    if (count($user) > 0) {
929
                        $elem->title = $user['login'];
930
                    }
931
                }
932
                // Build path
933
                array_push($folder, stripslashes($elem->title));
934
            }
935
            // store data
936
            DB::insert(
937
                prefixTable('cache'),
938
                [
939
                    'id' => $record['id'],
940
                    'label' => $record['label'],
941
                    'description' => $record['description'] ?? '',
942
                    'url' => isset($record['url']) && ! empty($record['url']) ? $record['url'] : '0',
943
                    'tags' => $tags,
944
                    'id_tree' => $record['id_tree'],
945
                    'perso' => $record['perso'],
946
                    'restricted_to' => isset($record['restricted_to']) && ! empty($record['restricted_to']) ? $record['restricted_to'] : '0',
947
                    'login' => $record['login'] ?? '',
948
                    'folder' => implode(' > ', $folder),
949
                    'author' => $record['id_user'],
950
                    'renewal_period' => $resNT['renewal_period'] ?? '0',
951
                    'timestamp' => $record['date'],
952
                ]
953
            );
954
        }
955
    }
956
}
957
958
/**
959
 * Cache table - update existing value.
960
 *
961
 * @param array  $SETTINGS Teampass settings
962
 * @param int    $ident    Ident format
963
 * 
964
 * @return void
965
 */
966
function cacheTableUpdate(array $SETTINGS, ?int $ident = null): void
967
{
968
    include_once $SETTINGS['cpassman_dir'] . '/sources/SplClassLoader.php';
969
    // Load superglobal
970
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
971
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
972
973
    // Load class DB
974
    if (class_exists('DB') === false) {
975
        loadClass('DB');
976
    }
977
978
    //Load Tree
979
    $tree = new SplClassLoader('Tree\NestedTree', '../includes/libraries');
980
    $tree->register();
981
    $tree = new Tree\NestedTree\NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
982
    // get new value from db
983
    $data = DB::queryfirstrow(
984
        'SELECT label, description, id_tree, perso, restricted_to, login, url
985
        FROM ' . prefixTable('items') . '
986
        WHERE id=%i',
987
        $ident
988
    );
989
    // Get all TAGS
990
    $tags = '';
991
    $itemTags = DB::query(
992
        'SELECT tag
993
            FROM ' . prefixTable('tags') . '
994
            WHERE item_id = %i AND tag != ""',
995
        $ident
996
    );
997
    foreach ($itemTags as $itemTag) {
998
        $tags .= $itemTag['tag'] . ' ';
999
    }
1000
    // form id_tree to full foldername
1001
    $folder = [];
1002
    $arbo = $tree->getPath($data['id_tree'], true);
1003
    foreach ($arbo as $elem) {
1004
        // Check if title is the ID of a user
1005
        if (is_numeric($elem->title) === true) {
1006
            // Is this a User id?
1007
            $user = DB::queryfirstrow(
1008
                'SELECT id, login
1009
                FROM ' . prefixTable('users') . '
1010
                WHERE id = %i',
1011
                $elem->title
1012
            );
1013
            if (count($user) > 0) {
1014
                $elem->title = $user['login'];
1015
            }
1016
        }
1017
        // Build path
1018
        array_push($folder, stripslashes($elem->title));
1019
    }
1020
    // finaly update
1021
    DB::update(
1022
        prefixTable('cache'),
1023
        [
1024
            'label' => $data['label'],
1025
            'description' => $data['description'],
1026
            'tags' => $tags,
1027
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
1028
            'id_tree' => $data['id_tree'],
1029
            'perso' => $data['perso'],
1030
            'restricted_to' => isset($data['restricted_to']) && ! empty($data['restricted_to']) ? $data['restricted_to'] : '0',
1031
            'login' => $data['login'] ?? '',
1032
            'folder' => implode(' » ', $folder),
1033
            'author' => $superGlobal->get('user_id', 'SESSION'),
1034
        ],
1035
        'id = %i',
1036
        $ident
1037
    );
1038
}
1039
1040
/**
1041
 * Cache table - add new value.
1042
 *
1043
 * @param array  $SETTINGS Teampass settings
1044
 * @param int    $ident    Ident format
1045
 * 
1046
 * @return void
1047
 */
1048
function cacheTableAdd(array $SETTINGS, ?int $ident = null): void
1049
{
1050
    include_once $SETTINGS['cpassman_dir'] . '/sources/SplClassLoader.php';
1051
    // Load superglobal
1052
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
1053
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
1054
    // Get superglobals
1055
    $globalsUserId = $superGlobal->get('user_id', 'SESSION');
1056
1057
    // Load class DB
1058
    if (class_exists('DB') === false) {
1059
        loadClass('DB');
1060
    }
1061
1062
    //Load Tree
1063
    $tree = new SplClassLoader('Tree\NestedTree', '../includes/libraries');
1064
    $tree->register();
1065
    $tree = new Tree\NestedTree\NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1066
    // get new value from db
1067
    $data = DB::queryFirstRow(
1068
        'SELECT i.label, i.description, i.id_tree as id_tree, i.perso, i.restricted_to, i.id, i.login, i.url, l.date
1069
        FROM ' . prefixTable('items') . ' as i
1070
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
1071
        WHERE i.id = %i
1072
        AND l.action = %s',
1073
        $ident,
1074
        'at_creation'
1075
    );
1076
    // Get all TAGS
1077
    $tags = '';
1078
    $itemTags = DB::query(
1079
        'SELECT tag
1080
            FROM ' . prefixTable('tags') . '
1081
            WHERE item_id = %i AND tag != ""',
1082
        $ident
1083
    );
1084
    foreach ($itemTags as $itemTag) {
1085
        $tags .= $itemTag['tag'] . ' ';
1086
    }
1087
    // form id_tree to full foldername
1088
    $folder = [];
1089
    $arbo = $tree->getPath($data['id_tree'], true);
1090
    foreach ($arbo as $elem) {
1091
        // Check if title is the ID of a user
1092
        if (is_numeric($elem->title) === true) {
1093
            // Is this a User id?
1094
            $user = DB::queryfirstrow(
1095
                'SELECT id, login
1096
                FROM ' . prefixTable('users') . '
1097
                WHERE id = %i',
1098
                $elem->title
1099
            );
1100
            if (count($user) > 0) {
1101
                $elem->title = $user['login'];
1102
            }
1103
        }
1104
        // Build path
1105
        array_push($folder, stripslashes($elem->title));
1106
    }
1107
    // finaly update
1108
    DB::insert(
1109
        prefixTable('cache'),
1110
        [
1111
            'id' => $data['id'],
1112
            'label' => $data['label'],
1113
            'description' => $data['description'],
1114
            'tags' => isset($tags) && empty($tags) === false ? $tags : 'None',
1115
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
1116
            'id_tree' => $data['id_tree'],
1117
            'perso' => isset($data['perso']) && empty($data['perso']) === false && $data['perso'] !== 'None' ? $data['perso'] : '0',
1118
            'restricted_to' => isset($data['restricted_to']) && empty($data['restricted_to']) === false ? $data['restricted_to'] : '0',
1119
            'login' => $data['login'] ?? '',
1120
            'folder' => implode(' » ', $folder),
1121
            'author' => $globalsUserId,
1122
            'timestamp' => $data['date'],
1123
        ]
1124
    );
1125
}
1126
1127
/**
1128
 * Do statistics.
1129
 *
1130
 * @param array $SETTINGS Teampass settings
1131
 *
1132
 * @return array
1133
 */
1134
function getStatisticsData(array $SETTINGS): array
1135
{
1136
    DB::query(
1137
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
1138
        0
1139
    );
1140
    $counter_folders = DB::count();
1141
    DB::query(
1142
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
1143
        1
1144
    );
1145
    $counter_folders_perso = DB::count();
1146
    DB::query(
1147
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
1148
        0
1149
    );
1150
    $counter_items = DB::count();
1151
        DB::query(
1152
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
1153
        1
1154
    );
1155
    $counter_items_perso = DB::count();
1156
        DB::query(
1157
        'SELECT id FROM ' . prefixTable('users') . ''
1158
    );
1159
    $counter_users = DB::count();
1160
        DB::query(
1161
        'SELECT id FROM ' . prefixTable('users') . ' WHERE admin = %i',
1162
        1
1163
    );
1164
    $admins = DB::count();
1165
    DB::query(
1166
        'SELECT id FROM ' . prefixTable('users') . ' WHERE gestionnaire = %i',
1167
        1
1168
    );
1169
    $managers = DB::count();
1170
    DB::query(
1171
        'SELECT id FROM ' . prefixTable('users') . ' WHERE read_only = %i',
1172
        1
1173
    );
1174
    $readOnly = DB::count();
1175
    // list the languages
1176
    $usedLang = [];
1177
    $tp_languages = DB::query(
1178
        'SELECT name FROM ' . prefixTable('languages')
1179
    );
1180
    foreach ($tp_languages as $tp_language) {
1181
        DB::query(
1182
            'SELECT * FROM ' . prefixTable('users') . ' WHERE user_language = %s',
1183
            $tp_language['name']
1184
        );
1185
        $usedLang[$tp_language['name']] = round((DB::count() * 100 / $counter_users), 0);
1186
    }
1187
1188
    // get list of ips
1189
    $usedIp = [];
1190
    $tp_ips = DB::query(
1191
        'SELECT user_ip FROM ' . prefixTable('users')
1192
    );
1193
    foreach ($tp_ips as $ip) {
1194
        if (array_key_exists($ip['user_ip'], $usedIp)) {
1195
            $usedIp[$ip['user_ip']] += $usedIp[$ip['user_ip']];
1196
        } elseif (! empty($ip['user_ip']) && $ip['user_ip'] !== 'none') {
1197
            $usedIp[$ip['user_ip']] = 1;
1198
        }
1199
    }
1200
1201
    return [
1202
        'error' => '',
1203
        'stat_phpversion' => phpversion(),
1204
        'stat_folders' => $counter_folders,
1205
        'stat_folders_shared' => intval($counter_folders) - intval($counter_folders_perso),
1206
        'stat_items' => $counter_items,
1207
        'stat_items_shared' => intval($counter_items) - intval($counter_items_perso),
1208
        'stat_users' => $counter_users,
1209
        'stat_admins' => $admins,
1210
        'stat_managers' => $managers,
1211
        'stat_ro' => $readOnly,
1212
        'stat_kb' => $SETTINGS['enable_kb'],
1213
        'stat_pf' => $SETTINGS['enable_pf_feature'],
1214
        'stat_fav' => $SETTINGS['enable_favourites'],
1215
        'stat_teampassversion' => TP_VERSION,
1216
        'stat_ldap' => $SETTINGS['ldap_mode'],
1217
        'stat_agses' => $SETTINGS['agses_authentication_enabled'],
1218
        'stat_duo' => $SETTINGS['duo'],
1219
        'stat_suggestion' => $SETTINGS['enable_suggestion'],
1220
        'stat_api' => $SETTINGS['api'],
1221
        'stat_customfields' => $SETTINGS['item_extra_fields'],
1222
        'stat_syslog' => $SETTINGS['syslog_enable'],
1223
        'stat_2fa' => $SETTINGS['google_authentication'],
1224
        'stat_stricthttps' => $SETTINGS['enable_sts'],
1225
        'stat_mysqlversion' => DB::serverVersion(),
1226
        'stat_languages' => $usedLang,
1227
        'stat_country' => $usedIp,
1228
    ];
1229
}
1230
1231
/**
1232
 * Permits to prepare the way to send the email
1233
 * 
1234
 * @param string $subject       email subject
1235
 * @param string $body          email message
1236
 * @param string $email         email
1237
 * @param string $receiverName  Receiver name
1238
 * @param array  $SETTINGS      settings
1239
 *
1240
 * @return void
1241
 */
1242
function prepareSendingEmail(
1243
    $subject,
1244
    $body,
1245
    $email,
1246
    $receiverName,
1247
    $SETTINGS
1248
): void 
1249
{
1250
    DB::insert(
1251
        prefixTable('processes'),
1252
        array(
1253
            'created_at' => time(),
1254
            'process_type' => 'send_email',
1255
            'arguments' => json_encode([
1256
                'subject' => $subject,
1257
                'receivers' => $email,
1258
                'body' => $body,
1259
                'receiver_name' => $receiverName,
1260
            ], JSON_HEX_QUOT | JSON_HEX_TAG),
1261
            'updated_at' => '',
1262
            'finished_at' => '',
1263
            'output' => '',
1264
        )
1265
    );
1266
}
1267
1268
/**
1269
 * Permits to send an email.
1270
 *
1271
 * @param string $subject     email subject
1272
 * @param string $textMail    email message
1273
 * @param string $email       email
1274
 * @param array  $SETTINGS    settings
1275
 * @param string $textMailAlt email message alt
1276
 * @param bool   $silent      no errors
1277
 *
1278
 * @return string some json info
1279
 */
1280
function sendEmail(
1281
    $subject,
1282
    $textMail,
1283
    $email,
1284
    $SETTINGS,
1285
    $textMailAlt = null,
1286
    $silent = true,
1287
    $cron = false
1288
) {
1289
    // CAse where email not defined
1290
    if ($email === 'none' || empty($email) === true) {
1291
        return json_encode(
1292
            [
1293
                'error' => true,
1294
                'message' => langHdl('forgot_my_pw_email_sent'),
1295
            ]
1296
        );
1297
    }
1298
1299
    // Build and send email
1300
    $email = buildEmail(
1301
        $subject,
1302
        $textMail,
1303
        $email,
1304
        $SETTINGS,
1305
        $textMailAlt = null,
1306
        $silent = true,
1307
        $cron
1308
    );
1309
1310
    if ($silent === false) {
0 ignored issues
show
introduced by
The condition $silent === false is always false.
Loading history...
1311
        return json_encode(
1312
            [
1313
                'error' => false,
1314
                'message' => langHdl('forgot_my_pw_email_sent'),
1315
            ]
1316
        );
1317
    }
1318
    // Debug purpose
1319
    if ((int) $SETTINGS['email_debug_level'] !== 0 && $cron === false) {
1320
        return json_encode(
1321
            [
1322
                'error' => true,
1323
                'message' => isset($email['ErrorInfo']) === true ? $email['ErrorInfo'] : '',
1324
            ]
1325
        );
1326
    }
1327
    return json_encode(
1328
        [
1329
            'error' => false,
1330
            'message' => langHdl('share_sent_ok'),
1331
        ]
1332
    );
1333
}
1334
1335
1336
function buildEmail(
1337
    $subject,
1338
    $textMail,
1339
    $email,
1340
    $SETTINGS,
1341
    $textMailAlt = null,
1342
    $silent = true,
1343
    $cron = false
1344
)
1345
{
1346
    // Load settings
1347
    //include_once $SETTINGS['cpassman_dir'] . '/includes/config/settings.php';
1348
    // Load superglobal
1349
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
1350
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
1351
    // Get user language
1352
    include_once $SETTINGS['cpassman_dir'] . '/includes/language/' . (null !== $superGlobal->get('user_language', 'SESSION', 'user') ? $superGlobal->get('user_language', 'SESSION', 'user') : 'english') . '.php';
1353
    // Load library
1354
    include_once $SETTINGS['cpassman_dir'] . '/sources/SplClassLoader.php';
1355
    // load PHPMailer
1356
    $mail = new SplClassLoader('PHPMailer\PHPMailer', $SETTINGS['cpassman_dir'] . '/includes/libraries');
1357
    $mail->register();
1358
    $mail = new PHPMailer\PHPMailer\PHPMailer(true);
1359
1360
    // send to user
1361
    $mail->setLanguage('en', $SETTINGS['cpassman_dir'] . '/includes/libraries/PHPMailer/PHPMailer/language/');
1362
    $mail->SMTPDebug = isset($SETTINGS['email_debug_level']) === true && $cron === false && $silent === false ? $SETTINGS['email_debug_level'] : 0;
1363
    $mail->Port = (int) $SETTINGS['email_port'];
1364
    //COULD BE USED
1365
    $mail->CharSet = 'utf-8';
1366
    $mail->SMTPSecure = $SETTINGS['email_security'] !== 'none' ? $SETTINGS['email_security'] : '';
1367
    $mail->SMTPAutoTLS = $SETTINGS['email_security'] !== 'none' ? true : false;
1368
    $mail->SMTPOptions = [
1369
        'ssl' => [
1370
            'verify_peer' => false,
1371
            'verify_peer_name' => false,
1372
            'allow_self_signed' => true,
1373
        ],
1374
    ];
1375
    $mail->isSmtp();
1376
    // send via SMTP
1377
    $mail->Host = $SETTINGS['email_smtp_server'];
1378
    // SMTP servers
1379
    $mail->SMTPAuth = (int) $SETTINGS['email_smtp_auth'] === 1 ? true : false;
1380
    // turn on SMTP authentication
1381
    $mail->Username = $SETTINGS['email_auth_username'];
1382
    // SMTP username
1383
    $mail->Password = $SETTINGS['email_auth_pwd'];
1384
    // SMTP password
1385
    $mail->From = $SETTINGS['email_from'];
1386
    $mail->FromName = $SETTINGS['email_from_name'];
1387
    // Prepare for each person
1388
    foreach (array_filter(explode(',', $email)) as $dest) {
1389
        $mail->addAddress($dest);
1390
    }
1391
    
1392
    // Prepare HTML
1393
    $text_html = emailBody($textMail);
1394
    $mail->WordWrap = 80;
1395
    // set word wrap
1396
    $mail->isHtml(true);
1397
    // send as HTML
1398
    $mail->Subject = $subject;
1399
    $mail->Body = $text_html;
1400
    $mail->AltBody = is_null($textMailAlt) === false ? $textMailAlt : '';
1401
1402
    try {
1403
        // send email
1404
        $mail->send();
1405
    } catch (Exception $e) {
1406
        if ($silent === false || (int) $SETTINGS['email_debug_level'] !== 0) {
1407
            return json_encode(
1408
                [
1409
                    'error' => true,
1410
                    'errorInfo' => str_replace(["\n", "\t", "\r"], '', $mail->ErrorInfo),
1411
                ]
1412
            );
1413
        }
1414
        return '';
1415
    }
1416
    $mail->smtpClose();
1417
1418
    return json_encode(
1419
        [
1420
            'error' => true,
1421
            'errorInfo' => str_replace(["\n", "\t", "\r"], '', $mail->ErrorInfo),
1422
        ]
1423
    );
1424
}
1425
1426
/**
1427
 * Returns the email body.
1428
 *
1429
 * @param string $textMail Text for the email
1430
 */
1431
function emailBody(string $textMail): string
1432
{
1433
    return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
1434
    w3.org/TR/html4/loose.dtd"><html>
1435
    <head><title>Email Template</title>
1436
    <style type="text/css">
1437
    body { background-color: #f0f0f0; padding: 10px 0; margin:0 0 10px =0; }
1438
    </style></head>
1439
    <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">
1440
    <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#f0f0f0" style="border-spacing: 0;">
1441
    <tr><td style="border-collapse: collapse;"><br>
1442
        <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#17357c" style="border-spacing: 0; margin-bottom: 25px;">
1443
        <tr><td style="border-collapse: collapse; padding: 11px 20px;">
1444
            <div style="max-width:150px; max-height:34px; color:#f0f0f0; font-weight:bold;">Teampass</div>
1445
        </td></tr></table></td>
1446
    </tr>
1447
    <tr><td align="center" valign="top" bgcolor="#f0f0f0" style="border-collapse: collapse; background-color: #f0f0f0;">
1448
        <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;">
1449
        <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;">
1450
        <br><div style="float:right;">' .
1451
        $textMail .
1452
        '<br><br></td></tr></table>
1453
    </td></tr></table>
1454
    <br></body></html>';
1455
}
1456
1457
/**
1458
 * Generate a Key.
1459
 * 
1460
 * @return string
1461
 */
1462
function generateKey(): string
1463
{
1464
    return substr(md5(rand() . rand()), 0, 15);
1465
}
1466
1467
/**
1468
 * Convert date to timestamp.
1469
 *
1470
 * @param string $date        The date
1471
 * @param string $date_format Date format
1472
 *
1473
 * @return int
1474
 */
1475
function dateToStamp(string $date, string $date_format): int
1476
{
1477
    $date = date_parse_from_format($date_format, $date);
1478
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1479
        return mktime(
1480
            empty($date['hour']) === false ? $date['hour'] : 23,
1481
            empty($date['minute']) === false ? $date['minute'] : 59,
1482
            empty($date['second']) === false ? $date['second'] : 59,
1483
            $date['month'],
1484
            $date['day'],
1485
            $date['year']
1486
        );
1487
    }
1488
    return 0;
1489
}
1490
1491
/**
1492
 * Is this a date.
1493
 *
1494
 * @param string $date Date
1495
 *
1496
 * @return bool
1497
 */
1498
function isDate(string $date): bool
1499
{
1500
    return strtotime($date) !== false;
1501
}
1502
1503
/**
1504
 * Check if isUTF8().
1505
 *
1506
 * @param string|array $string Is the string
1507
 *
1508
 * @return int is the string in UTF8 format
1509
 */
1510
function isUTF8($string): int
1511
{
1512
    if (is_array($string) === true) {
1513
        $string = $string['string'];
1514
    }
1515
1516
    return preg_match(
1517
        '%^(?:
1518
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1519
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1520
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1521
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1522
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1523
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1524
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1525
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1526
        )*$%xs',
1527
        $string
1528
    );
1529
}
1530
1531
/**
1532
 * Prepare an array to UTF8 format before JSON_encode.
1533
 *
1534
 * @param array $array Array of values
1535
 *
1536
 * @return array
1537
 */
1538
function utf8Converter(array $array): array
1539
{
1540
    array_walk_recursive(
1541
        $array,
1542
        static function (&$item): void {
1543
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1544
                $item = utf8_encode($item);
1545
            }
1546
        }
1547
    );
1548
    return $array;
1549
}
1550
1551
/**
1552
 * Permits to prepare data to be exchanged.
1553
 *
1554
 * @param string       $teampassDir
1555
 * @param array|string $data Text
1556
 * @param string       $type Parameter
1557
 * @param string       $key  Optional key
1558
 *
1559
 * @return string|array
1560
 */
1561
function prepareExchangedData($teampassDir, $data, string $type, ?string $key = null)
1562
{
1563
    $teampassDir = __DIR__ . '/..';
1564
    // Load superglobal
1565
    include_once $teampassDir . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
1566
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
1567
    // Get superglobals
1568
    if ($key !== null) {
1569
        $superGlobal->put('key', $key, 'SESSION');
1570
        $globalsKey = $key;
1571
    } else {
1572
        $globalsKey = $superGlobal->get('key', 'SESSION');
1573
    }
1574
1575
    //load Encoding
1576
    include_once $teampassDir . '/includes/libraries/ForceUTF8/Encoding.php';
1577
    
1578
    //Load CRYPTOJS
1579
    include_once $teampassDir . '/includes/libraries/Encryption/CryptoJs/Encryption.php';
1580
1581
    // Perform
1582
    if ($type === 'encode' && is_array($data) === true) {
1583
        // Now encode
1584
        return Encryption\CryptoJs\Encryption::encrypt(
1585
            json_encode(
1586
                $data,
1587
                JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1588
            ),
1589
            $globalsKey
1590
        );
1591
    }
1592
    if ($type === 'decode' && is_array($data) === false) {
1593
        // check if key exists
1594
        return json_decode(
1595
            (string) Encryption\CryptoJs\Encryption::decrypt(
1596
                (string) $data,
1597
                $globalsKey
1598
            ),
1599
            true
1600
        );
1601
    }
1602
}
1603
1604
1605
/**
1606
 * Create a thumbnail.
1607
 *
1608
 * @param string  $src           Source
1609
 * @param string  $dest          Destination
1610
 * @param int $desired_width Size of width
1611
 * 
1612
 * @return void|string|bool
1613
 */
1614
function makeThumbnail(string $src, string $dest, int $desired_width)
1615
{
1616
    /* read the source image */
1617
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1618
        $source_image = imagecreatefrompng($src);
1619
        if ($source_image === false) {
1620
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1621
        }
1622
    } else {
1623
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1624
    }
1625
1626
    // Get height and width
1627
    $width = imagesx($source_image);
1628
    $height = imagesy($source_image);
1629
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1630
    $desired_height = (int) floor($height * $desired_width / $width);
1631
    /* create a new, "virtual" image */
1632
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1633
    if ($virtual_image === false) {
1634
        return false;
1635
    }
1636
    /* copy source image at a resized size */
1637
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1638
    /* create the physical thumbnail image to its destination */
1639
    imagejpeg($virtual_image, $dest);
1640
}
1641
1642
/**
1643
 * Check table prefix in SQL query.
1644
 *
1645
 * @param string $table Table name
1646
 * 
1647
 * @return string
1648
 */
1649
function prefixTable(string $table): string
1650
{
1651
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1652
    if (empty($safeTable) === false) {
1653
        // sanitize string
1654
        return $safeTable;
1655
    }
1656
    // stop error no table
1657
    return 'table_not_exists';
1658
}
1659
1660
/**
1661
 * GenerateCryptKey
1662
 *
1663
 * @param int     $size      Length
1664
 * @param bool $secure Secure
1665
 * @param bool $numerals Numerics
1666
 * @param bool $uppercase Uppercase letters
1667
 * @param bool $symbols Symbols
1668
 * @param bool $lowercase Lowercase
1669
 * @param array   $SETTINGS  SETTINGS
1670
 * 
1671
 * @return string
1672
 */
1673
function GenerateCryptKey(
1674
    int $size = 20,
1675
    bool $secure = false,
1676
    bool $numerals = false,
1677
    bool $uppercase = false,
1678
    bool $symbols = false,
1679
    bool $lowercase = false,
1680
    array $SETTINGS = []
1681
): string {
1682
    include_once __DIR__ . '/../sources/SplClassLoader.php';
1683
    $generator = new SplClassLoader('PasswordGenerator\Generator', __DIR__. '/../includes/libraries');
1684
    $generator->register();
1685
    $generator = new PasswordGenerator\Generator\ComputerPasswordGenerator();
1686
    // Is PHP7 being used?
1687
    if (version_compare(PHP_VERSION, '7.0.0', '>=')) {
1688
        $php7generator = new SplClassLoader('PasswordGenerator\RandomGenerator', __DIR__ . '/../includes/libraries');
1689
        $php7generator->register();
1690
        $generator->setRandomGenerator(new PasswordGenerator\RandomGenerator\Php7RandomGenerator());
1691
    }
1692
    
1693
    // Manage size
1694
    $generator->setLength((int) $size);
1695
    if ($secure === true) {
1696
        $generator->setSymbols(true);
1697
        $generator->setLowercase(true);
1698
        $generator->setUppercase(true);
1699
        $generator->setNumbers(true);
1700
    } else {
1701
        $generator->setLowercase($lowercase);
1702
        $generator->setUppercase($uppercase);
1703
        $generator->setNumbers($numerals);
1704
        $generator->setSymbols($symbols);
1705
    }
1706
1707
    return $generator->generatePasswords()[0];
1708
}
1709
1710
/**
1711
 * Send sysLOG message
1712
 *
1713
 * @param string    $message
1714
 * @param string    $host
1715
 * @param int       $port
1716
 * @param string    $component
1717
 * 
1718
 * @return void
1719
*/
1720
function send_syslog($message, $host, $port, $component = 'teampass'): void
1721
{
1722
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1723
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1724
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1725
    socket_close($sock);
1726
}
1727
1728
/**
1729
 * Permits to log events into DB
1730
 *
1731
 * @param array  $SETTINGS Teampass settings
1732
 * @param string $type     Type
1733
 * @param string $label    Label
1734
 * @param string $who      Who
1735
 * @param string $login    Login
1736
 * @param string $field_1  Field
1737
 * 
1738
 * @return void
1739
 */
1740
function logEvents(
1741
    array $SETTINGS, 
1742
    string $type, 
1743
    string $label, 
1744
    string $who, 
1745
    ?string $login = null, 
1746
    ?string $field_1 = null
1747
): void
1748
{
1749
    if (empty($who)) {
1750
        $who = getClientIpServer();
1751
    }
1752
1753
    // Load class DB
1754
    if (class_exists('DB') === false) {
1755
        loadClass('DB');
1756
    }
1757
1758
    DB::insert(
1759
        prefixTable('log_system'),
1760
        [
1761
            'type' => $type,
1762
            'date' => time(),
1763
            'label' => $label,
1764
            'qui' => $who,
1765
            'field_1' => $field_1 === null ? '' : $field_1,
1766
        ]
1767
    );
1768
    // If SYSLOG
1769
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1770
        if ($type === 'user_mngt') {
1771
            send_syslog(
1772
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1773
                $SETTINGS['syslog_host'],
1774
                $SETTINGS['syslog_port'],
1775
                'teampass'
1776
            );
1777
        } else {
1778
            send_syslog(
1779
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1780
                $SETTINGS['syslog_host'],
1781
                $SETTINGS['syslog_port'],
1782
                'teampass'
1783
            );
1784
        }
1785
    }
1786
}
1787
1788
/**
1789
 * Log events.
1790
 *
1791
 * @param array  $SETTINGS        Teampass settings
1792
 * @param int    $item_id         Item id
1793
 * @param string $item_label      Item label
1794
 * @param int    $id_user         User id
1795
 * @param string $action          Code for reason
1796
 * @param string $login           User login
1797
 * @param string $raison          Code for reason
1798
 * @param string $encryption_type Encryption on
1799
 * @param string $time Encryption Time
1800
 * @param string $old_value       Old value
1801
 * 
1802
 * @return void
1803
 */
1804
function logItems(
1805
    array $SETTINGS,
1806
    int $item_id,
1807
    string $item_label,
1808
    int $id_user,
1809
    string $action,
1810
    ?string $login = null,
1811
    ?string $raison = null,
1812
    ?string $encryption_type = null,
1813
    ?string $time = null,
1814
    ?string $old_value = null
1815
): void {
1816
    // Load class DB
1817
    if (class_exists('DB') === false) {
1818
        loadClass('DB');
1819
    }
1820
1821
    // Insert log in DB
1822
    DB::insert(
1823
        prefixTable('log_items'),
1824
        [
1825
            'id_item' => $item_id,
1826
            'date' => is_null($time) === true ? time() : $time,
1827
            'id_user' => $id_user,
1828
            'action' => $action,
1829
            'raison' => $raison,
1830
            'old_value' => $old_value,
1831
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1832
        ]
1833
    );
1834
    // Timestamp the last change
1835
    if ($action === 'at_creation' || $action === 'at_modifiation' || $action === 'at_delete' || $action === 'at_import') {
1836
        DB::update(
1837
            prefixTable('misc'),
1838
            [
1839
                'valeur' => time(),
1840
            ],
1841
            'type = %s AND intitule = %s',
1842
            'timestamp',
1843
            'last_item_change'
1844
        );
1845
    }
1846
1847
    // SYSLOG
1848
    if (isset($SETTINGS['syslog_enable']) === true && $SETTINGS['syslog_enable'] === '1') {
1849
        // Extract reason
1850
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1851
        // Get item info if not known
1852
        if (empty($item_label) === true) {
1853
            $dataItem = DB::queryfirstrow(
1854
                'SELECT id, id_tree, label
1855
                FROM ' . prefixTable('items') . '
1856
                WHERE id = %i',
1857
                $item_id
1858
            );
1859
            $item_label = $dataItem['label'];
1860
        }
1861
1862
        send_syslog(
1863
            'action=' . str_replace('at_', '', $action) .
1864
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1865
                ' itemno=' . $item_id .
1866
                ' user=' . is_null($login) === true ? '' : addslashes((string) $login) .
1867
                ' itemname="' . addslashes($item_label) . '"',
1868
            $SETTINGS['syslog_host'],
1869
            $SETTINGS['syslog_port'],
1870
            'teampass'
1871
        );
1872
    }
1873
1874
    // send notification if enabled
1875
    //notifyOnChange($item_id, $action, $SETTINGS);
1876
}
1877
1878
/**
1879
 * If enabled, then notify admin/manager.
1880
 *
1881
 * @param int    $item_id  Item id
1882
 * @param string $action   Action to do
1883
 * @param array  $SETTINGS Teampass settings
1884
 * 
1885
 * @return void
1886
 */
1887
/*
1888
function notifyOnChange(int $item_id, string $action, array $SETTINGS): void
1889
{
1890
    if (
1891
        isset($SETTINGS['enable_email_notification_on_item_shown']) === true
1892
        && (int) $SETTINGS['enable_email_notification_on_item_shown'] === 1
1893
        && $action === 'at_shown'
1894
    ) {
1895
        // Load superglobal
1896
        include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
1897
        $superGlobal = new protect\SuperGlobal\SuperGlobal();
1898
        // Get superglobals
1899
        $globalsLastname = $superGlobal->get('lastname', 'SESSION');
1900
        $globalsName = $superGlobal->get('name', 'SESSION');
1901
        $globalsNotifiedEmails = $superGlobal->get('listNotificationEmails', 'SESSION');
1902
        // Get info about item
1903
        $dataItem = DB::queryfirstrow(
1904
            'SELECT id, id_tree, label
1905
            FROM ' . prefixTable('items') . '
1906
            WHERE id = %i',
1907
            $item_id
1908
        );
1909
        $item_label = $dataItem['label'];
1910
        // send back infos
1911
        DB::insert(
1912
            prefixTable('emails'),
1913
            [
1914
                'timestamp' => time(),
1915
                'subject' => langHdl('email_on_open_notification_subject'),
1916
                'body' => str_replace(
1917
                    ['#tp_user#', '#tp_item#', '#tp_link#'],
1918
                    [
1919
                        addslashes($globalsName . ' ' . $globalsLastname),
1920
                        addslashes($item_label),
1921
                        $SETTINGS['cpassman_url'] . '/index.php?page=items&group=' . $dataItem['id_tree'] . '&id=' . $item_id,
1922
                    ],
1923
                    langHdl('email_on_open_notification_mail')
1924
                ),
1925
                'receivers' => $globalsNotifiedEmails,
1926
                'status' => '',
1927
            ]
1928
        );
1929
    }
1930
}
1931
*/
1932
1933
/**
1934
 * Prepare notification email to subscribers.
1935
 *
1936
 * @param int    $item_id  Item id
1937
 * @param string $label    Item label
1938
 * @param array  $changes  List of changes
1939
 * @param array  $SETTINGS Teampass settings
1940
 * 
1941
 * @return void
1942
 */
1943
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1944
{
1945
    // Load superglobal
1946
    include_once $SETTINGS['cpassman_dir'] . '/includes/libraries/protect/SuperGlobal/SuperGlobal.php';
1947
    $superGlobal = new protect\SuperGlobal\SuperGlobal();
1948
    // Get superglobals
1949
    $globalsUserId = $superGlobal->get('user_id', 'SESSION');
1950
    $globalsLastname = $superGlobal->get('lastname', 'SESSION');
1951
    $globalsName = $superGlobal->get('name', 'SESSION');
1952
    // send email to user that what to be notified
1953
    $notification = DB::queryOneColumn(
1954
        'email',
1955
        'SELECT *
1956
        FROM ' . prefixTable('notification') . ' AS n
1957
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1958
        WHERE n.item_id = %i AND n.user_id != %i',
1959
        $item_id,
1960
        $globalsUserId
1961
    );
1962
    if (DB::count() > 0) {
1963
        // Prepare path
1964
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1965
        // Get list of changes
1966
        $htmlChanges = '<ul>';
1967
        foreach ($changes as $change) {
1968
            $htmlChanges .= '<li>' . $change . '</li>';
1969
        }
1970
        $htmlChanges .= '</ul>';
1971
        // send email
1972
        DB::insert(
1973
            prefixTable('emails'),
1974
            [
1975
                'timestamp' => time(),
1976
                'subject' => langHdl('email_subject_item_updated'),
1977
                'body' => str_replace(
1978
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1979
                    [$label, $path, $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1980
                    langHdl('email_body_item_updated')
1981
                ),
1982
                'receivers' => implode(',', $notification),
1983
                'status' => '',
1984
            ]
1985
        );
1986
    }
1987
}
1988
1989
/**
1990
 * Returns the Item + path.
1991
 *
1992
 * @param int    $id_tree  Node id
1993
 * @param string $label    Label
1994
 * @param array  $SETTINGS TP settings
1995
 * 
1996
 * @return string
1997
 */
1998
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1999
{
2000
    // Class loader
2001
    include_once $SETTINGS['cpassman_dir'] . '/sources/SplClassLoader.php';
2002
    //Load Tree
2003
    $tree = new SplClassLoader('Tree\NestedTree', '../includes/libraries');
2004
    $tree->register();
2005
    $tree = new Tree\NestedTree\NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
2006
    $arbo = $tree->getPath($id_tree, true);
2007
    $path = '';
2008
    foreach ($arbo as $elem) {
2009
        if (empty($path) === true) {
2010
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
2011
        } else {
2012
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
2013
        }
2014
    }
2015
2016
    // Build text to show user
2017
    if (empty($label) === false) {
2018
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
2019
    }
2020
    return empty($path) === true ? '' : $path;
2021
}
2022
2023
/**
2024
 * Get the client ip address.
2025
 *
2026
 * @return string IP address
2027
 */
2028
function getClientIpServer(): string
2029
{
2030
    if (getenv('HTTP_CLIENT_IP')) {
2031
        $ipaddress = getenv('HTTP_CLIENT_IP');
2032
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
2033
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
2034
    } elseif (getenv('HTTP_X_FORWARDED')) {
2035
        $ipaddress = getenv('HTTP_X_FORWARDED');
2036
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
2037
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
2038
    } elseif (getenv('HTTP_FORWARDED')) {
2039
        $ipaddress = getenv('HTTP_FORWARDED');
2040
    } elseif (getenv('REMOTE_ADDR')) {
2041
        $ipaddress = getenv('REMOTE_ADDR');
2042
    } else {
2043
        $ipaddress = 'UNKNOWN';
2044
    }
2045
2046
    return $ipaddress;
2047
}
2048
2049
/**
2050
 * Escape all HTML, JavaScript, and CSS.
2051
 *
2052
 * @param string $input    The input string
2053
 * @param string $encoding Which character encoding are we using?
2054
 * 
2055
 * @return string
2056
 */
2057
function noHTML(string $input, string $encoding = 'UTF-8'): string
2058
{
2059
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
2060
}
2061
2062
/**
2063
 * Permits to handle the Teampass config file
2064
 * $action accepts "rebuild" and "update"
2065
 *
2066
 * @param string $action   Action to perform
2067
 * @param array  $SETTINGS Teampass settings
2068
 * @param string $field    Field to refresh
2069
 * @param string $value    Value to set
2070
 *
2071
 * @return string|bool
2072
 */
2073
function handleConfigFile($action, $SETTINGS, $field = null, $value = null)
2074
{
2075
    $tp_config_file = $SETTINGS['cpassman_dir'] . '/includes/config/tp.config.php';
2076
2077
    // Load class DB
2078
    if (class_exists('DB') === false) {
2079
        loadClass('DB');
2080
    }
2081
2082
    if (file_exists($tp_config_file) === false || $action === 'rebuild') {
2083
        // perform a copy
2084
        if (file_exists($tp_config_file)) {
2085
            if (! copy($tp_config_file, $tp_config_file . '.' . date('Y_m_d_His', time()))) {
2086
                return "ERROR: Could not copy file '" . $tp_config_file . "'";
2087
            }
2088
        }
2089
2090
        // regenerate
2091
        $data = [];
2092
        $data[0] = "<?php\n";
2093
        $data[1] = "global \$SETTINGS;\n";
2094
        $data[2] = "\$SETTINGS = array (\n";
2095
        $rows = DB::query(
2096
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s',
2097
            'admin'
2098
        );
2099
        foreach ($rows as $record) {
2100
            array_push($data, "    '" . $record['intitule'] . "' => '" . htmlspecialchars_decode($record['valeur'], ENT_COMPAT) . "',\n");
2101
        }
2102
        array_push($data, ");\n");
2103
        $data = array_unique($data);
2104
    // ---
2105
    } elseif ($action === 'update' && empty($field) === false) {
2106
        $data = file($tp_config_file);
2107
        $inc = 0;
2108
        $bFound = false;
2109
        foreach ($data as $line) {
2110
            if (stristr($line, ');')) {
2111
                break;
2112
            }
2113
2114
            if (stristr($line, "'" . $field . "' => '")) {
2115
                $data[$inc] = "    '" . $field . "' => '" . htmlspecialchars_decode($value, ENT_COMPAT) . "',\n";
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type null; however, parameter $string of htmlspecialchars_decode() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

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

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