Passed
Push — dev ( 3ad29f...97f4bd )
by Nils
08:12
created

isHex()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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

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

// Bar.php
namespace OtherDir;

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

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

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