Passed
Push — master ( 161ae4...48e2c9 )
by Nils
06:11
created

prepareExchangedData()   A

Complexity

Conditions 6
Paths 6

Size

Total Lines 35
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 3
dl 0
loc 35
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\Language\Language;
35
use TeampassClasses\NestedTree\NestedTree;
36
use Defuse\Crypto\Key;
37
use Defuse\Crypto\Crypto;
38
use Defuse\Crypto\KeyProtectedByPassword;
39
use Defuse\Crypto\File as CryptoFile;
40
use Defuse\Crypto\Exception as CryptoException;
41
use PHPMailer\PHPMailer\PHPMailer;
42
use phpseclib\Crypt\RSA;
0 ignored issues
show
Bug introduced by
The type phpseclib\Crypt\RSA was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
43
use phpseclib\Crypt\AES;
0 ignored issues
show
Bug introduced by
The type phpseclib\Crypt\AES was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

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