Passed
Push — master ( 2b4eae...097631 )
by Nils
06:26
created

prepareExchangedData()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 6
eloc 19
c 3
b 0
f 0
nc 6
nop 4
dl 0
loc 33
rs 9.0111
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This library is distributed in the hope that it will be useful,
9
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
10
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
11
 * ---
12
 *
13
 * @project   Teampass
14
 * @file      main.functions.php
15
 * ---
16
 *
17
 * @author    Nils Laumaillé ([email protected])
18
 *
19
 * @copyright 2009-2023 Teampass.net
20
 *
21
 * @license   https://spdx.org/licenses/GPL-3.0-only.html#licenseText GPL-3.0
22
 * ---
23
 *
24
 * @see       https://www.teampass.net
25
 */
26
27
use LdapRecord\Connection;
28
use ForceUTF8\Encoding;
29
Use Elegant\Sanitizer\Sanitizer;
30
Use voku\helper\AntiXSS;
31
Use Hackzilla\PasswordGenerator\Generator\ComputerPasswordGenerator;
32
Use Hackzilla\PasswordGenerator\RandomGenerator\Php7RandomGenerator;
33
Use TeampassClasses\SuperGlobal\SuperGlobal;
34
Use TeampassClasses\NestedTree\NestedTree;
35
Use Defuse\Crypto\Key;
36
Use Defuse\Crypto\Crypto;
37
Use Defuse\Crypto\KeyProtectedByPassword;
38
Use Defuse\Crypto\File;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, File. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
39
use PHPMailer\PHPMailer\PHPMailer;
40
Use phpseclib\Crypt\RSA;
41
Use phpseclib\Crypt\AES;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, AES. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

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

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