Issues (14)

Security Analysis    no vulnerabilities found

This project does not seem to handle request data directly as such no vulnerable execution paths were found.

  File Inclusion
File Inclusion enables an attacker to inject custom files into PHP's file loading mechanism, either explicitly passed to include, or for example via PHP's auto-loading mechanism.
  Regex Injection
Regex Injection enables an attacker to execute arbitrary code in your PHP process.
  SQL Injection
SQL Injection enables an attacker to execute arbitrary SQL code on your database server gaining access to user data, or manipulating user data.
  Response Splitting
Response Splitting can be used to send arbitrary responses.
  File Manipulation
File Manipulation enables an attacker to write custom data to files. This potentially leads to injection of arbitrary code on the server.
  Object Injection
Object Injection enables an attacker to inject an object into PHP code, and can lead to arbitrary code execution, file exposure, or file manipulation attacks.
  File Exposure
File Exposure allows an attacker to gain access to local files that he should not be able to access. These files can for example include database credentials, or other configuration files.
  XML Injection
XML Injection enables an attacker to read files on your local filesystem including configuration files, or can be abused to freeze your web-server process.
  Code Injection
Code Injection enables an attacker to execute arbitrary code on the server.
  Variable Injection
Variable Injection enables an attacker to overwrite program variables with custom data, and can lead to further vulnerabilities.
  XPath Injection
XPath Injection enables an attacker to modify the parts of XML document that are read. If that XML document is for example used for authentication, this can lead to further vulnerabilities similar to SQL Injection.
  Other Vulnerability
This category comprises other attack vectors such as manipulating the PHP runtime, loading custom extensions, freezing the runtime, or similar.
  Command Injection
Command Injection enables an attacker to inject a shell command that is execute with the privileges of the web-server. This can be used to expose sensitive data, or gain access of your server.
  LDAP Injection
LDAP Injection enables an attacker to inject LDAP statements potentially granting permission to run unauthorized queries, or modify content inside the LDAP tree.
  Cross-Site Scripting
Cross-Site Scripting enables an attacker to inject code into the response of a web-request that is viewed by other users. It can for example be used to bypass access controls, or even to take over other users' accounts.
  Header Injection
Unfortunately, the security analysis is currently not available for your project. If you are a non-commercial open-source project, please contact support to gain access.

sources/main.functions.php (4 issues)

Code
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      main.functions.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use LdapRecord\Connection;
33
use Elegant\Sanitizer\Sanitizer;
34
use voku\helper\AntiXSS;
35
use Hackzilla\PasswordGenerator\Generator\ComputerPasswordGenerator;
36
use Hackzilla\PasswordGenerator\RandomGenerator\Php7RandomGenerator;
37
use TeampassClasses\SessionManager\SessionManager;
38
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
39
use TeampassClasses\Language\Language;
40
use TeampassClasses\NestedTree\NestedTree;
41
use Defuse\Crypto\Key;
42
use Defuse\Crypto\Crypto;
43
use Defuse\Crypto\KeyProtectedByPassword;
44
use Defuse\Crypto\File as CryptoFile;
45
use Defuse\Crypto\Exception as CryptoException;
46
use TeampassClasses\PasswordManager\PasswordManager;
47
use Symfony\Component\Process\PhpExecutableFinder;
48
use TeampassClasses\Encryption\Encryption;
49
use TeampassClasses\ConfigManager\ConfigManager;
50
use TeampassClasses\EmailService\EmailService;
51
use TeampassClasses\EmailService\EmailSettings;
52
53
header('Content-type: text/html; charset=utf-8');
54
header('Cache-Control: no-cache, must-revalidate');
55
56
loadClasses('DB');
57
$session = SessionManager::getSession();
58
59
// Load config if $SETTINGS not defined
60
$configManager = new ConfigManager($session);
61
$SETTINGS = $configManager->getAllSettings();
62
63
/**
64
 * genHash().
65
 *
66
 * Generate a hash for user login
67
 *
68
 * @param string $password What password
69
 * @param string $cost     What cost
70
 *
71
 * @return string|void
72
 */
73
/* TODO - Remove this function
74
function bCrypt(
75
    string $password,
76
    string $cost
77
): ?string
78
{
79
    $salt = sprintf('$2y$%02d$', $cost);
80
    if (function_exists('openssl_random_pseudo_bytes')) {
81
        $salt .= bin2hex(openssl_random_pseudo_bytes(11));
82
    } else {
83
        $chars = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
84
        for ($i = 0; $i < 22; ++$i) {
85
            $salt .= $chars[mt_rand(0, 63)];
86
        }
87
    }
88
89
    return crypt($password, $salt);
90
}
91
*/
92
93
/**
94
 * Checks if a string is hex encoded
95
 *
96
 * @param string $str
97
 * @return boolean
98
 */
99
function isHex(string $str): bool
100
{
101
    if (str_starts_with(strtolower($str), '0x')) {
102
        $str = substr($str, 2);
103
    }
104
105
    return ctype_xdigit($str);
106
}
107
108
/**
109
 * Defuse cryption function.
110
 *
111
 * @param string $message   what to de/crypt
112
 * @param string $ascii_key key to use
113
 * @param string $type      operation to perform
114
 * @param array  $SETTINGS  Teampass settings
115
 *
116
 * @return array
117
 */
118
function cryption(string $message, string $ascii_key, string $type, ?array $SETTINGS = []): array
119
{
120
    $ascii_key = empty($ascii_key) === true ? file_get_contents(SECUREPATH.'/'.SECUREFILE) : $ascii_key;
121
    $err = false;
122
    
123
    // convert KEY
124
    $key = Key::loadFromAsciiSafeString($ascii_key);
125
    try {
126
        if ($type === 'encrypt') {
127
            $text = Crypto::encrypt($message, $key);
128
        } elseif ($type === 'decrypt') {
129
            $text = Crypto::decrypt($message, $key);
130
        }
131
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
132
        error_log('TEAMPASS-Error-Wrong key or modified ciphertext: ' . $ex->getMessage());
133
        $err = 'wrong_key_or_modified_ciphertext';
134
    } catch (CryptoException\BadFormatException $ex) {
135
        error_log('TEAMPASS-Error-Bad format exception: ' . $ex->getMessage());
136
        $err = 'bad_format';
137
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
138
        error_log('TEAMPASS-Error-Environment: ' . $ex->getMessage());
139
        $err = 'environment_error';
140
    } catch (CryptoException\IOException $ex) {
141
        error_log('TEAMPASS-Error-IO: ' . $ex->getMessage());
142
        $err = 'io_error';
143
    } catch (Exception $ex) {
144
        error_log('TEAMPASS-Error-Unexpected exception: ' . $ex->getMessage());
145
        $err = 'unexpected_error';
146
    }
147
148
    return [
149
        'string' => $text ?? '',
150
        'error' => $err,
151
    ];
152
}
153
154
/**
155
 * Generating a defuse key.
156
 *
157
 * @return string
158
 */
159
function defuse_generate_key()
160
{
161
    $key = Key::createNewRandomKey();
162
    $key = $key->saveToAsciiSafeString();
163
    return $key;
164
}
165
166
/**
167
 * Generate a Defuse personal key.
168
 *
169
 * @param string $psk psk used
170
 *
171
 * @return string
172
 */
173
function defuse_generate_personal_key(string $psk): string
174
{
175
    $protected_key = KeyProtectedByPassword::createRandomPasswordProtectedKey($psk);
176
    return $protected_key->saveToAsciiSafeString(); // save this in user table
177
}
178
179
/**
180
 * Validate persoanl key with defuse.
181
 *
182
 * @param string $psk                   the user's psk
183
 * @param string $protected_key_encoded special key
184
 *
185
 * @return string
186
 */
187
function defuse_validate_personal_key(string $psk, string $protected_key_encoded): string
188
{
189
    try {
190
        $protected_key_encoded = KeyProtectedByPassword::loadFromAsciiSafeString($protected_key_encoded);
191
        $user_key = $protected_key_encoded->unlockKey($psk);
192
        $user_key_encoded = $user_key->saveToAsciiSafeString();
193
    } catch (CryptoException\EnvironmentIsBrokenException $ex) {
194
        return 'Error - Major issue as the encryption is broken.';
195
    } catch (CryptoException\WrongKeyOrModifiedCiphertextException $ex) {
196
        return 'Error - The saltkey is not the correct one.';
197
    }
198
199
    return $user_key_encoded;
200
    // store it in session once user has entered his psk
201
}
202
203
/**
204
 * Decrypt a defuse string if encrypted.
205
 *
206
 * @param string $value Encrypted string
207
 *
208
 * @return string Decrypted string
209
 */
210
function defuseReturnDecrypted(string $value): string
211
{
212
    if (substr($value, 0, 3) === 'def') {
213
        $value = cryption($value, '', 'decrypt')['string'];
214
    }
215
216
    return $value;
217
}
218
219
/**
220
 * Trims a string depending on a specific string.
221
 *
222
 * @param string|array $chaine  what to trim
223
 * @param string       $element trim on what
224
 *
225
 * @return string
226
 */
227
function trimElement($chaine, string $element): string
228
{
229
    if (! empty($chaine)) {
230
        if (is_array($chaine) === true) {
231
            $chaine = implode(';', $chaine);
232
        }
233
        $chaine = trim($chaine);
234
        if (substr($chaine, 0, 1) === $element) {
235
            $chaine = substr($chaine, 1);
236
        }
237
        if (substr($chaine, strlen($chaine) - 1, 1) === $element) {
238
            $chaine = substr($chaine, 0, strlen($chaine) - 1);
239
        }
240
    }
241
242
    return $chaine;
243
}
244
245
/**
246
 * Permits to suppress all "special" characters from string.
247
 *
248
 * @param string $string  what to clean
249
 * @param bool   $special use of special chars?
250
 *
251
 * @return string
252
 */
253
function cleanString(string $string, bool $special = false): string
254
{
255
    // Create temporary table for special characters escape
256
    $tabSpecialChar = [];
257
    for ($i = 0; $i <= 31; ++$i) {
258
        $tabSpecialChar[] = chr($i);
259
    }
260
    array_push($tabSpecialChar, '<br />');
261
    if ((int) $special === 1) {
262
        $tabSpecialChar = array_merge($tabSpecialChar, ['</li>', '<ul>', '<ol>']);
263
    }
264
265
    return str_replace($tabSpecialChar, "\n", $string);
266
}
267
268
/**
269
 * Erro manager for DB.
270
 *
271
 * @param array $params output from query
272
 *
273
 * @return void
274
 */
275
function db_error_handler(array $params): void
276
{
277
    echo 'Error: ' . $params['error'] . "<br>\n";
278
    echo 'Query: ' . $params['query'] . "<br>\n";
279
    throw new Exception('Error - Query', 1);
280
}
281
282
/**
283
 * Identify user's rights
284
 *
285
 * @param string|array $groupesVisiblesUser  [description]
286
 * @param string|array $groupesInterditsUser [description]
287
 * @param string       $isAdmin              [description]
288
 * @param string       $idFonctions          [description]
289
 *
290
 * @return bool
291
 */
292
function identifyUserRights(
293
    $groupesVisiblesUser,
294
    $groupesInterditsUser,
295
    $isAdmin,
296
    $idFonctions,
297
    $SETTINGS
298
) {
299
    $session = SessionManager::getSession();
300
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
301
302
    // Check if user is ADMINISTRATOR    
303
    (int) $isAdmin === 1 ?
304
        identAdmin(
305
            $idFonctions,
306
            $SETTINGS, /** @scrutinizer ignore-type */
307
            $tree
308
        )
309
        :
310
        identUser(
311
            $groupesVisiblesUser,
312
            $groupesInterditsUser,
313
            $idFonctions,
314
            $SETTINGS, /** @scrutinizer ignore-type */
315
            $tree
316
        );
317
318
    // update user's timestamp
319
    DB::update(
320
        prefixTable('users'),
321
        [
322
            'timestamp' => time(),
323
        ],
324
        'id=%i',
325
        $session->get('user-id')
326
    );
327
328
    return true;
329
}
330
331
/**
332
 * Identify administrator.
333
 *
334
 * @param string $idFonctions Roles of user
335
 * @param array  $SETTINGS    Teampass settings
336
 * @param object $tree        Tree of folders
337
 *
338
 * @return bool
339
 */
340
function identAdmin($idFonctions, $SETTINGS, $tree)
341
{
342
    
343
    $session = SessionManager::getSession();
344
    $groupesVisibles = [];
345
    $session->set('user-personal_folders', []);
346
    $session->set('user-accessible_folders', []);
347
    $session->set('user-no_access_folders', []);
348
    $session->set('user-personal_visible_folders', []);
349
    $session->set('user-read_only_folders', []);
350
    $session->set('system-list_restricted_folders_for_items', []);
351
    $session->set('system-list_folders_editable_by_role', []);
352
    $session->set('user-list_folders_limited', []);
353
    $session->set('user-forbiden_personal_folders', []);
354
    
355
    // Get list of Folders
356
    $rows = DB::query('SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i', 0);
357
    foreach ($rows as $record) {
358
        array_push($groupesVisibles, $record['id']);
359
    }
360
    $session->set('user-accessible_folders', $groupesVisibles);
361
    $session->set('user-all_non_personal_folders', $groupesVisibles);
362
363
    // get complete list of ROLES
364
    $tmp = explode(';', $idFonctions);
365
    $rows = DB::query(
366
        'SELECT * FROM ' . prefixTable('roles_title') . '
367
        ORDER BY title ASC'
368
    );
369
    foreach ($rows as $record) {
370
        if (! empty($record['id']) && ! in_array($record['id'], $tmp)) {
371
            array_push($tmp, $record['id']);
372
        }
373
    }
374
    $session->set('user-roles', implode(';', $tmp));
375
    $session->set('user-admin', 1);
376
    // Check if admin has created Folders and Roles
377
    DB::query('SELECT * FROM ' . prefixTable('nested_tree') . '');
378
    $session->set('user-nb_folders', DB::count());
379
    DB::query('SELECT * FROM ' . prefixTable('roles_title'));
380
    $session->set('user-nb_roles', DB::count());
381
382
    return true;
383
}
384
385
/**
386
 * Permits to convert an element to array.
387
 *
388
 * @param string|array $element Any value to be returned as array
389
 *
390
 * @return array
391
 */
392
function convertToArray($element): ?array
393
{
394
    if (is_string($element) === true) {
395
        if (empty($element) === true) {
396
            return [];
397
        }
398
        return explode(
399
            ';',
400
            trimElement($element, ';')
401
        );
402
    }
403
    return $element;
404
}
405
406
/**
407
 * Defines the rights the user has.
408
 *
409
 * @param string|array $allowedFolders  Allowed folders
410
 * @param string|array $noAccessFolders Not allowed folders
411
 * @param string|array $userRoles       Roles of user
412
 * @param array        $SETTINGS        Teampass settings
413
 * @param object       $tree            Tree of folders
414
 * 
415
 * @return bool
416
 */
417
function identUser(
418
    $allowedFolders,
419
    $noAccessFolders,
420
    $userRoles,
421
    array $SETTINGS,
422
    object $tree
423
) {
424
    $session = SessionManager::getSession();
425
    // Init
426
    $session->set('user-accessible_folders', []);
427
    $session->set('user-personal_folders', []);
428
    $session->set('user-no_access_folders', []);
429
    $session->set('user-personal_visible_folders', []);
430
    $session->set('user-read_only_folders', []);
431
    $session->set('user-user-roles', $userRoles);
432
    $session->set('user-admin', 0);
433
    // init
434
    $personalFolders = [];
435
    $readOnlyFolders = [];
436
    $noAccessPersonalFolders = [];
437
    $restrictedFoldersForItems = [];
438
    $foldersLimited = [];
439
    $foldersLimitedFull = [];
440
    $allowedFoldersByRoles = [];
441
    $globalsUserId = $session->get('user-id');
442
    $globalsPersonalFolders = $session->get('user-personal_folder_enabled');
443
    // Ensure consistency in array format
444
    $noAccessFolders = convertToArray($noAccessFolders);
445
    $userRoles = convertToArray($userRoles);
446
    $allowedFolders = convertToArray($allowedFolders);
447
    $session->set('user-allowed_folders_by_definition', $allowedFolders);
448
    
449
    // Get list of folders depending on Roles
450
    $arrays = identUserGetFoldersFromRoles(
451
        $userRoles,
452
        $allowedFoldersByRoles,
453
        $readOnlyFolders,
454
        $allowedFolders
455
    );
456
    $allowedFoldersByRoles = $arrays['allowedFoldersByRoles'];
457
    $readOnlyFolders = $arrays['readOnlyFolders'];
458
459
    // Does this user is allowed to see other items
460
    $inc = 0;
461
    $rows = DB::query(
462
        'SELECT id, id_tree FROM ' . prefixTable('items') . '
463
            WHERE restricted_to LIKE %ss AND inactif = %s'.
464
            (count($allowedFolders) > 0 ? ' AND id_tree NOT IN ('.implode(',', $allowedFolders).')' : ''),
465
        $globalsUserId,
466
        '0'
467
    );
468
    foreach ($rows as $record) {
469
        // Exclude restriction on item if folder is fully accessible
470
        //if (in_array($record['id_tree'], $allowedFolders) === false) {
471
            $restrictedFoldersForItems[$record['id_tree']][$inc] = $record['id'];
472
            ++$inc;
473
        //}
474
    }
475
476
    // Check for the users roles if some specific rights exist on items
477
    $rows = DB::query(
478
        'SELECT i.id_tree, r.item_id
479
        FROM ' . prefixTable('items') . ' as i
480
        INNER JOIN ' . prefixTable('restriction_to_roles') . ' as r ON (r.item_id=i.id)
481
        WHERE i.id_tree <> "" '.
482
        (count($userRoles) > 0 ? 'AND r.role_id IN %li ' : '').
483
        'ORDER BY i.id_tree ASC',
484
        $userRoles
485
    );
486
    $inc = 0;
487
    foreach ($rows as $record) {
488
        //if (isset($record['id_tree'])) {
489
            $foldersLimited[$record['id_tree']][$inc] = $record['item_id'];
490
            array_push($foldersLimitedFull, $record['id_tree']);
491
            ++$inc;
492
        //}
493
    }
494
495
    // Get list of Personal Folders
496
    $arrays = identUserGetPFList(
497
        $globalsPersonalFolders,
498
        $allowedFolders,
499
        $globalsUserId,
500
        $personalFolders,
501
        $noAccessPersonalFolders,
502
        $foldersLimitedFull,
503
        $allowedFoldersByRoles,
504
        array_keys($restrictedFoldersForItems),
505
        $readOnlyFolders,
506
        $noAccessFolders,
507
        isset($SETTINGS['enable_pf_feature']) === true ? $SETTINGS['enable_pf_feature'] : 0,
508
        $tree
509
    );
510
    $allowedFolders = $arrays['allowedFolders'];
511
    $personalFolders = $arrays['personalFolders'];
512
    $noAccessPersonalFolders = $arrays['noAccessPersonalFolders'];
513
514
    // Return data
515
    $session->set('user-all_non_personal_folders', $allowedFolders);
516
    $session->set('user-accessible_folders', array_unique(array_merge($allowedFolders, $personalFolders), SORT_NUMERIC));
517
    $session->set('user-read_only_folders', $readOnlyFolders);
518
    $session->set('user-no_access_folders', $noAccessFolders);
519
    $session->set('user-personal_folders', $personalFolders);
520
    $session->set('user-list_folders_limited', $foldersLimited);
521
    $session->set('system-list_folders_editable_by_role', $allowedFoldersByRoles, 'SESSION');
522
    $session->set('system-list_restricted_folders_for_items', $restrictedFoldersForItems);
523
    $session->set('user-forbiden_personal_folders', $noAccessPersonalFolders);
524
    $session->set(
525
        'all_folders_including_no_access',
526
        array_unique(array_merge(
527
            $allowedFolders,
528
            $personalFolders,
529
            $noAccessFolders,
530
            $readOnlyFolders
531
        ), SORT_NUMERIC)
532
    );
533
    // Folders and Roles numbers
534
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('nested_tree') . '');
535
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('nested_tree') . '');
536
    $session->set('user-nb_folders', DB::count());
537
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('roles_title'));
538
    DB::queryFirstRow('SELECT id FROM ' . prefixTable('roles_title'));
539
    $session->set('user-nb_roles', DB::count());
540
    // check if change proposals on User's items
541
    if (isset($SETTINGS['enable_suggestion']) === true && (int) $SETTINGS['enable_suggestion'] === 1) {
542
        $countNewItems = DB::query(
543
            'SELECT COUNT(*)
544
            FROM ' . prefixTable('items_change') . ' AS c
545
            LEFT JOIN ' . prefixTable('log_items') . ' AS i ON (c.item_id = i.id_item)
546
            WHERE i.action = %s AND i.id_user = %i',
547
            'at_creation',
548
            $globalsUserId
549
        );
550
        $session->set('user-nb_item_change_proposals', $countNewItems);
551
    } else {
552
        $session->set('user-nb_item_change_proposals', 0);
553
    }
554
555
    return true;
556
}
557
558
/**
559
 * Get list of folders depending on Roles
560
 * 
561
 * @param array $userRoles
562
 * @param array $allowedFoldersByRoles
563
 * @param array $readOnlyFolders
564
 * @param array $allowedFolders
565
 * 
566
 * @return array
567
 */
568
function identUserGetFoldersFromRoles(array $userRoles, array $allowedFoldersByRoles = [], array $readOnlyFolders = [], array $allowedFolders = []) : array
569
{
570
    $rows = DB::query(
571
        'SELECT *
572
        FROM ' . prefixTable('roles_values') . '
573
        WHERE type IN %ls'.(count($userRoles) > 0 ? ' AND role_id IN %li' : ''),
574
        ['W', 'ND', 'NE', 'NDNE', 'R'],
575
        $userRoles,
576
    );
577
    foreach ($rows as $record) {
578
        if ($record['type'] === 'R') {
579
            array_push($readOnlyFolders, $record['folder_id']);
580
        } elseif (in_array($record['folder_id'], $allowedFolders) === false) {
581
            array_push($allowedFoldersByRoles, $record['folder_id']);
582
        }
583
    }
584
    $allowedFoldersByRoles = array_unique($allowedFoldersByRoles);
585
    $readOnlyFolders = array_unique($readOnlyFolders);
586
    
587
    // Clean arrays
588
    foreach ($allowedFoldersByRoles as $value) {
589
        $key = array_search($value, $readOnlyFolders);
590
        if ($key !== false) {
591
            unset($readOnlyFolders[$key]);
592
        }
593
    }
594
    return [
595
        'readOnlyFolders' => $readOnlyFolders,
596
        'allowedFoldersByRoles' => $allowedFoldersByRoles
597
    ];
598
}
599
600
/**
601
 * Get list of Personal Folders
602
 * 
603
 * @param int $globalsPersonalFolders
604
 * @param array $allowedFolders
605
 * @param int $globalsUserId
606
 * @param array $personalFolders
607
 * @param array $noAccessPersonalFolders
608
 * @param array $foldersLimitedFull
609
 * @param array $allowedFoldersByRoles
610
 * @param array $restrictedFoldersForItems
611
 * @param array $readOnlyFolders
612
 * @param array $noAccessFolders
613
 * @param int $enablePfFeature
614
 * @param object $tree
615
 * 
616
 * @return array
617
 */
618
function identUserGetPFList(
619
    $globalsPersonalFolders,
620
    $allowedFolders,
621
    $globalsUserId,
622
    $personalFolders,
623
    $noAccessPersonalFolders,
624
    $foldersLimitedFull,
625
    $allowedFoldersByRoles,
626
    $restrictedFoldersForItems,
627
    $readOnlyFolders,
628
    $noAccessFolders,
629
    $enablePfFeature,
630
    $tree
631
)
632
{
633
    if (
634
        (int) $enablePfFeature === 1
635
        && (int) $globalsPersonalFolders === 1
636
    ) {
637
        $persoFld = DB::queryFirstRow(
638
            'SELECT id
639
            FROM ' . prefixTable('nested_tree') . '
640
            WHERE title = %s AND personal_folder = %i'.
641
            (count($allowedFolders) > 0 ? ' AND id NOT IN ('.implode(',', $allowedFolders).')' : ''),
642
            $globalsUserId,
643
            1
644
        );
645
        if (empty($persoFld['id']) === false) {
646
            array_push($personalFolders, $persoFld['id']);
647
            array_push($allowedFolders, $persoFld['id']);
648
            // get all descendants
649
            $ids = $tree->getDescendants($persoFld['id'], false, false, true);
650
            foreach ($ids as $id) {
651
                //array_push($allowedFolders, $id);
652
                array_push($personalFolders, $id);
653
            }
654
        }
655
    }
656
    
657
    // Exclude all other PF
658
    $where = new WhereClause('and');
659
    $where->add('personal_folder=%i', 1);
660
    if (count($personalFolders) > 0) {
661
        $where->add('id NOT IN ('.implode(',', $personalFolders).')');
662
    }
663
    if (
664
        (int) $enablePfFeature === 1
665
        && (int) $globalsPersonalFolders === 1
666
    ) {
667
        $where->add('title=%s', $globalsUserId);
668
        $where->negateLast();
669
    }
670
    $persoFlds = DB::query(
671
        'SELECT id
672
        FROM ' . prefixTable('nested_tree') . '
673
        WHERE %l',
674
        $where
675
    );
676
    foreach ($persoFlds as $persoFldId) {
677
        array_push($noAccessPersonalFolders, $persoFldId['id']);
678
    }
679
680
    // All folders visibles
681
    $allowedFolders = array_unique(array_merge(
682
        $allowedFolders,
683
        $foldersLimitedFull,
684
        $allowedFoldersByRoles,
685
        $restrictedFoldersForItems,
686
        $readOnlyFolders
687
    ), SORT_NUMERIC);
688
    // Exclude from allowed folders all the specific user forbidden folders
689
    if (is_array($noAccessFolders) && count($noAccessFolders) > 0) {
690
        $allowedFolders = array_diff($allowedFolders, $noAccessFolders);
691
    }
692
693
    return [
694
        'allowedFolders' => array_diff(array_diff($allowedFolders, $noAccessPersonalFolders), $personalFolders),
695
        'personalFolders' => $personalFolders,
696
        'noAccessPersonalFolders' => $noAccessPersonalFolders
697
    ];
698
}
699
700
701
/**
702
 * Update the CACHE table.
703
 *
704
 * @param string $action   What to do
705
 * @param array  $SETTINGS Teampass settings
706
 * @param int    $ident    Ident format
707
 * 
708
 * @return void
709
 */
710
function updateCacheTable(string $action, ?int $ident = null): void
711
{
712
    if ($action === 'reload') {
713
        // Rebuild full cache table
714
        cacheTableRefresh();
715
    } elseif ($action === 'update_value' && is_null($ident) === false) {
716
        // UPDATE an item
717
        cacheTableUpdate($ident);
718
    } elseif ($action === 'add_value' && is_null($ident) === false) {
719
        // ADD an item
720
        cacheTableAdd($ident);
721
    } elseif ($action === 'delete_value' && is_null($ident) === false) {
722
        // DELETE an item
723
        DB::delete(prefixTable('cache'), 'id = %i', $ident);
724
    }
725
}
726
727
/**
728
 * Cache table - refresh.
729
 *
730
 * @return void
731
 */
732
function cacheTableRefresh(): void
733
{
734
    // Load class DB
735
    loadClasses('DB');
736
737
    //Load Tree
738
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
739
    // truncate table
740
    DB::query('TRUNCATE TABLE ' . prefixTable('cache'));
741
    // reload date
742
    $rows = DB::query(
743
        'SELECT *
744
        FROM ' . prefixTable('items') . ' as i
745
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
746
        AND l.action = %s
747
        AND i.inactif = %i',
748
        'at_creation',
749
        0
750
    );
751
    foreach ($rows as $record) {
752
        if (empty($record['id_tree']) === false) {
753
            // Get all TAGS
754
            $tags = '';
755
            $itemTags = DB::query(
756
                'SELECT tag
757
                FROM ' . prefixTable('tags') . '
758
                WHERE item_id = %i AND tag != ""',
759
                $record['id']
760
            );
761
            foreach ($itemTags as $itemTag) {
762
                $tags .= $itemTag['tag'] . ' ';
763
            }
764
765
            // Get renewal period
766
            $resNT = DB::queryFirstRow(
767
                'SELECT renewal_period
768
                FROM ' . prefixTable('nested_tree') . '
769
                WHERE id = %i',
770
                $record['id_tree']
771
            );
772
            // form id_tree to full foldername
773
            $folder = [];
774
            $arbo = $tree->getPath($record['id_tree'], true);
775
            foreach ($arbo as $elem) {
776
                // Check if title is the ID of a user
777
                if (is_numeric($elem->title) === true) {
778
                    // Is this a User id?
779
                    $user = DB::queryFirstRow(
780
                        'SELECT id, login
781
                        FROM ' . prefixTable('users') . '
782
                        WHERE id = %i',
783
                        $elem->title
784
                    );
785
                    if (count($user) > 0) {
786
                        $elem->title = $user['login'];
787
                    }
788
                }
789
                // Build path
790
                array_push($folder, stripslashes($elem->title));
791
            }
792
            // store data
793
            DB::insert(
794
                prefixTable('cache'),
795
                [
796
                    'id' => $record['id'],
797
                    'label' => $record['label'],
798
                    'description' => $record['description'] ?? '',
799
                    'url' => isset($record['url']) && ! empty($record['url']) ? $record['url'] : '0',
800
                    'tags' => $tags,
801
                    'id_tree' => $record['id_tree'],
802
                    'perso' => $record['perso'],
803
                    'restricted_to' => isset($record['restricted_to']) && ! empty($record['restricted_to']) ? $record['restricted_to'] : '0',
804
                    'login' => $record['login'] ?? '',
805
                    'folder' => implode(' » ', $folder),
806
                    'author' => $record['id_user'],
807
                    'renewal_period' => $resNT['renewal_period'] ?? '0',
808
                    'timestamp' => $record['date'],
809
                ]
810
            );
811
        }
812
    }
813
}
814
815
/**
816
 * Cache table - update existing value.
817
 *
818
 * @param int    $ident    Ident format
819
 * 
820
 * @return void
821
 */
822
function cacheTableUpdate(?int $ident = null): void
823
{
824
    $session = SessionManager::getSession();
825
    loadClasses('DB');
826
827
    //Load Tree
828
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
829
    // get new value from db
830
    $data = DB::queryFirstRow(
831
        'SELECT label, description, id_tree, perso, restricted_to, login, url
832
        FROM ' . prefixTable('items') . '
833
        WHERE id=%i',
834
        $ident
835
    );
836
    // Get all TAGS
837
    $tags = '';
838
    $itemTags = DB::query(
839
        'SELECT tag
840
            FROM ' . prefixTable('tags') . '
841
            WHERE item_id = %i AND tag != ""',
842
        $ident
843
    );
844
    foreach ($itemTags as $itemTag) {
845
        $tags .= $itemTag['tag'] . ' ';
846
    }
847
    // form id_tree to full foldername
848
    $folder = [];
849
    $arbo = $tree->getPath($data['id_tree'], true);
850
    foreach ($arbo as $elem) {
851
        // Check if title is the ID of a user
852
        if (is_numeric($elem->title) === true) {
853
            // Is this a User id?
854
            $user = DB::queryFirstRow(
855
                'SELECT id, login
856
                FROM ' . prefixTable('users') . '
857
                WHERE id = %i',
858
                $elem->title
859
            );
860
            if (count($user) > 0) {
861
                $elem->title = $user['login'];
862
            }
863
        }
864
        // Build path
865
        array_push($folder, stripslashes($elem->title));
866
    }
867
    // finaly update
868
    DB::update(
869
        prefixTable('cache'),
870
        [
871
            'label' => $data['label'],
872
            'description' => $data['description'],
873
            'tags' => $tags,
874
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
875
            'id_tree' => $data['id_tree'],
876
            'perso' => $data['perso'],
877
            'restricted_to' => isset($data['restricted_to']) && ! empty($data['restricted_to']) ? $data['restricted_to'] : '0',
878
            'login' => $data['login'] ?? '',
879
            'folder' => implode(' » ', $folder),
880
            'author' => $session->get('user-id'),
881
        ],
882
        'id = %i',
883
        $ident
884
    );
885
}
886
887
/**
888
 * Cache table - add new value.
889
 *
890
 * @param int    $ident    Ident format
891
 * 
892
 * @return void
893
 */
894
function cacheTableAdd(?int $ident = null): void
895
{
896
    $session = SessionManager::getSession();
897
    $globalsUserId = $session->get('user-id');
898
899
    // Load class DB
900
    loadClasses('DB');
901
902
    //Load Tree
903
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
904
    // get new value from db
905
    $data = DB::queryFirstRow(
906
        'SELECT i.label, i.description, i.id_tree as id_tree, i.perso, i.restricted_to, i.id, i.login, i.url, l.date
907
        FROM ' . prefixTable('items') . ' as i
908
        INNER JOIN ' . prefixTable('log_items') . ' as l ON (l.id_item = i.id)
909
        WHERE i.id = %i
910
        AND l.action = %s',
911
        $ident,
912
        'at_creation'
913
    );
914
    // Get all TAGS
915
    $tags = '';
916
    $itemTags = DB::query(
917
        'SELECT tag
918
            FROM ' . prefixTable('tags') . '
919
            WHERE item_id = %i AND tag != ""',
920
        $ident
921
    );
922
    foreach ($itemTags as $itemTag) {
923
        $tags .= $itemTag['tag'] . ' ';
924
    }
925
    // form id_tree to full foldername
926
    $folder = [];
927
    $arbo = $tree->getPath($data['id_tree'], true);
928
    foreach ($arbo as $elem) {
929
        // Check if title is the ID of a user
930
        if (is_numeric($elem->title) === true) {
931
            // Is this a User id?
932
            $user = DB::queryFirstRow(
933
                'SELECT id, login
934
                FROM ' . prefixTable('users') . '
935
                WHERE id = %i',
936
                $elem->title
937
            );
938
            if (count($user) > 0) {
939
                $elem->title = $user['login'];
940
            }
941
        }
942
        // Build path
943
        array_push($folder, stripslashes($elem->title));
944
    }
945
    // finaly update
946
    DB::insert(
947
        prefixTable('cache'),
948
        [
949
            'id' => $data['id'],
950
            'label' => $data['label'],
951
            'description' => $data['description'],
952
            'tags' => empty($tags) === false ? $tags : 'None',
953
            'url' => isset($data['url']) && ! empty($data['url']) ? $data['url'] : '0',
954
            'id_tree' => $data['id_tree'],
955
            'perso' => isset($data['perso']) && empty($data['perso']) === false && $data['perso'] !== 'None' ? $data['perso'] : '0',
956
            'restricted_to' => isset($data['restricted_to']) && empty($data['restricted_to']) === false ? $data['restricted_to'] : '0',
957
            'login' => $data['login'] ?? '',
958
            'folder' => implode(' » ', $folder),
959
            'author' => $globalsUserId,
960
            'timestamp' => $data['date'],
961
        ]
962
    );
963
}
964
965
/**
966
 * Do statistics.
967
 *
968
 * @param array $SETTINGS Teampass settings
969
 *
970
 * @return array
971
 */
972
function getStatisticsData(array $SETTINGS): array
973
{
974
    DB::query(
975
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
976
        0
977
    );
978
    $counter_folders = DB::count();
979
    DB::query(
980
        'SELECT id FROM ' . prefixTable('nested_tree') . ' WHERE personal_folder = %i',
981
        1
982
    );
983
    $counter_folders_perso = DB::count();
984
    DB::query(
985
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
986
        0
987
    );
988
    $counter_items = DB::count();
989
        DB::query(
990
        'SELECT id FROM ' . prefixTable('items') . ' WHERE perso = %i',
991
        1
992
    );
993
    $counter_items_perso = DB::count();
994
        DB::query(
995
        'SELECT id FROM ' . prefixTable('users') . ' WHERE login NOT IN (%s, %s, %s)',
996
        'OTV', 'TP', 'API'
997
    );
998
    $counter_users = DB::count();
999
        DB::query(
1000
        'SELECT id FROM ' . prefixTable('users') . ' WHERE admin = %i',
1001
        1
1002
    );
1003
    $admins = DB::count();
1004
    DB::query(
1005
        'SELECT id FROM ' . prefixTable('users') . ' WHERE gestionnaire = %i',
1006
        1
1007
    );
1008
    $managers = DB::count();
1009
    DB::query(
1010
        'SELECT id FROM ' . prefixTable('users') . ' WHERE read_only = %i',
1011
        1
1012
    );
1013
    $readOnly = DB::count();
1014
    // list the languages
1015
    $usedLang = [];
1016
    $tp_languages = DB::query(
1017
        'SELECT name FROM ' . prefixTable('languages')
1018
    );
1019
    foreach ($tp_languages as $tp_language) {
1020
        DB::query(
1021
            'SELECT * FROM ' . prefixTable('users') . ' WHERE user_language = %s',
1022
            $tp_language['name']
1023
        );
1024
        $usedLang[$tp_language['name']] = round((DB::count() * 100 / $counter_users), 0);
1025
    }
1026
1027
    // get list of ips
1028
    $usedIp = [];
1029
    $tp_ips = DB::query(
1030
        'SELECT user_ip FROM ' . prefixTable('users')
1031
    );
1032
    foreach ($tp_ips as $ip) {
1033
        if (array_key_exists($ip['user_ip'], $usedIp)) {
1034
            $usedIp[$ip['user_ip']] += $usedIp[$ip['user_ip']];
1035
        } elseif (! empty($ip['user_ip']) && $ip['user_ip'] !== 'none') {
1036
            $usedIp[$ip['user_ip']] = 1;
1037
        }
1038
    }
1039
1040
    return [
1041
        'error' => '',
1042
        'stat_phpversion' => phpversion(),
1043
        'stat_folders' => $counter_folders,
1044
        'stat_folders_shared' => intval($counter_folders) - intval($counter_folders_perso),
1045
        'stat_items' => $counter_items,
1046
        'stat_items_shared' => intval($counter_items) - intval($counter_items_perso),
1047
        'stat_users' => $counter_users,
1048
        'stat_admins' => $admins,
1049
        'stat_managers' => $managers,
1050
        'stat_ro' => $readOnly,
1051
        'stat_kb' => $SETTINGS['enable_kb'],
1052
        'stat_pf' => $SETTINGS['enable_pf_feature'],
1053
        'stat_fav' => $SETTINGS['enable_favourites'],
1054
        'stat_teampassversion' => TP_VERSION,
1055
        'stat_ldap' => $SETTINGS['ldap_mode'],
1056
        'stat_agses' => $SETTINGS['agses_authentication_enabled'],
1057
        'stat_duo' => $SETTINGS['duo'],
1058
        'stat_suggestion' => $SETTINGS['enable_suggestion'],
1059
        'stat_api' => $SETTINGS['api'],
1060
        'stat_customfields' => $SETTINGS['item_extra_fields'],
1061
        'stat_syslog' => $SETTINGS['syslog_enable'],
1062
        'stat_2fa' => $SETTINGS['google_authentication'],
1063
        'stat_stricthttps' => $SETTINGS['enable_sts'],
1064
        'stat_mysqlversion' => DB::serverVersion(),
1065
        'stat_languages' => $usedLang,
1066
        'stat_country' => $usedIp,
1067
    ];
1068
}
1069
1070
/**
1071
 * Permits to prepare the way to send the email
1072
 * 
1073
 * @param string $subject       email subject
1074
 * @param string $body          email message
1075
 * @param string $email         email
1076
 * @param string $receiverName  Receiver name
1077
 * @param string $encryptedUserPassword      encryptedUserPassword
1078
 *
1079
 * @return void
1080
 */
1081
function prepareSendingEmail(
1082
    $subject,
1083
    $body,
1084
    $email,
1085
    $receiverName = '',
1086
    $encryptedUserPassword = ''
1087
): void 
1088
{
1089
    DB::insert(
1090
        prefixTable('background_tasks'),
1091
        array(
1092
            'created_at' => time(),
1093
            'process_type' => 'send_email',
1094
            'arguments' => json_encode([
1095
                'subject' => $subject,
1096
                'receivers' => $email,
1097
                'body' => $body,
1098
                'receiver_name' => $receiverName,
1099
                'encryptedUserPassword' => $encryptedUserPassword,
1100
            ], JSON_HEX_QUOT | JSON_HEX_TAG),
1101
        )
1102
    );
1103
}
1104
1105
/**
1106
 * Returns the email body.
1107
 *
1108
 * @param string $textMail Text for the email
1109
 */
1110
function emailBody(string $textMail): string
1111
{
1112
    return '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.=
1113
    w3.org/TR/html4/loose.dtd"><html>
1114
    <head><title>Email Template</title>
1115
    <style type="text/css">
1116
    body { background-color: #f0f0f0; padding: 10px 0; margin:0 0 10px =0; }
1117
    </style></head>
1118
    <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">
1119
    <table border="0" width="100%" height="100%" cellpadding="0" cellspacing="0" bgcolor="#f0f0f0" style="border-spacing: 0;">
1120
    <tr><td style="border-collapse: collapse;"><br>
1121
        <table border="0" width="100%" cellpadding="0" cellspacing="0" bgcolor="#17357c" style="border-spacing: 0; margin-bottom: 25px;">
1122
        <tr><td style="border-collapse: collapse; padding: 11px 20px;">
1123
            <div style="max-width:150px; max-height:34px; color:#f0f0f0; font-weight:bold;">Teampass</div>
1124
        </td></tr></table></td>
1125
    </tr>
1126
    <tr><td align="center" valign="top" bgcolor="#f0f0f0" style="border-collapse: collapse; background-color: #f0f0f0;">
1127
        <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;">
1128
        <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;">
1129
        <br><div style="float:right;">' .
1130
        $textMail .
1131
        '<br><br></td></tr></table>
1132
    </td></tr></table>
1133
    <br></body></html>';
1134
}
1135
1136
/**
1137
 * Convert date to timestamp.
1138
 *
1139
 * @param string $date        The date
1140
 * @param string $date_format Date format
1141
 *
1142
 * @return int
1143
 */
1144
function dateToStamp(string $date, string $date_format): int
1145
{
1146
    $date = date_parse_from_format($date_format, $date);
1147
    if ((int) $date['warning_count'] === 0 && (int) $date['error_count'] === 0) {
1148
        return mktime(
1149
            empty($date['hour']) === false ? $date['hour'] : 23,
1150
            empty($date['minute']) === false ? $date['minute'] : 59,
1151
            empty($date['second']) === false ? $date['second'] : 59,
1152
            $date['month'],
1153
            $date['day'],
1154
            $date['year']
1155
        );
1156
    }
1157
    return 0;
1158
}
1159
1160
/**
1161
 * Is this a date.
1162
 *
1163
 * @param string $date Date
1164
 *
1165
 * @return bool
1166
 */
1167
function isDate(string $date): bool
1168
{
1169
    return strtotime($date) !== false;
1170
}
1171
1172
/**
1173
 * Check if isUTF8().
1174
 *
1175
 * @param string|array $string Is the string
1176
 *
1177
 * @return int is the string in UTF8 format
1178
 */
1179
function isUTF8($string): int
1180
{
1181
    if (is_array($string) === true) {
1182
        $string = $string['string'];
1183
    }
1184
1185
    return preg_match(
1186
        '%^(?:
1187
        [\x09\x0A\x0D\x20-\x7E] # ASCII
1188
        | [\xC2-\xDF][\x80-\xBF] # non-overlong 2-byte
1189
        | \xE0[\xA0-\xBF][\x80-\xBF] # excluding overlongs
1190
        | [\xE1-\xEC\xEE\xEF][\x80-\xBF]{2} # straight 3-byte
1191
        | \xED[\x80-\x9F][\x80-\xBF] # excluding surrogates
1192
        | \xF0[\x90-\xBF][\x80-\xBF]{2} # planes 1-3
1193
        | [\xF1-\xF3][\x80-\xBF]{3} # planes 4-15
1194
        | \xF4[\x80-\x8F][\x80-\xBF]{2} # plane 16
1195
        )*$%xs',
1196
        $string
1197
    );
1198
}
1199
1200
/**
1201
 * Prepare an array to UTF8 format before JSON_encode.
1202
 *
1203
 * @param array $array Array of values
1204
 *
1205
 * @return array
1206
 */
1207
function utf8Converter(array $array): array
1208
{
1209
    array_walk_recursive(
1210
        $array,
1211
        static function (&$item): void {
1212
            if (mb_detect_encoding((string) $item, 'utf-8', true) === false) {
1213
                $item = mb_convert_encoding($item, 'ISO-8859-1', 'UTF-8');
1214
            }
1215
        }
1216
    );
1217
    return $array;
1218
}
1219
1220
/**
1221
 * Permits to prepare data to be exchanged.
1222
 *
1223
 * @param array|string $data Text
1224
 * @param string       $type Parameter
1225
 * @param string       $key  Optional key
1226
 *
1227
 * @return string|array
1228
 */
1229
function prepareExchangedData($data, string $type, ?string $key = null)
1230
{
1231
    $session = SessionManager::getSession();
1232
    $key = empty($key) ? $session->get('key') : $key;
1233
    
1234
    // Perform
1235
    if ($type === 'encode' && is_array($data) === true) {
1236
        // json encoding
1237
        $data = json_encode(
1238
            $data,
1239
            JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_QUOT | JSON_HEX_AMP
1240
        );
1241
        
1242
        // Now encrypt
1243
        if ((int) $session->get('teampass-settings')['encryptClientServer'] === 1) {
1244
            $data = Encryption::encrypt(
1245
                $data,
1246
                $key
1247
            );
1248
        }
1249
1250
        return $data;
1251
    }
1252
1253
    if ($type === 'decode' && is_array($data) === false) {
1254
        // Decrypt if needed
1255
        if ((int) $session->get('teampass-settings')['encryptClientServer'] === 1) {
1256
            $data = (string) Encryption::decrypt(
1257
                (string) $data,
1258
                $key
1259
            );
1260
        } else {
1261
            // Double html encoding received
1262
            $data = html_entity_decode(html_entity_decode(/** @scrutinizer ignore-type */$data)); // @codeCoverageIgnore Is always a string (not an array)
1263
        }
1264
1265
        // Check if $data is a valid string before json_decode
1266
        if (is_string($data) && !empty($data)) {
1267
            // Return data array
1268
            return json_decode($data, true);
1269
        }
1270
    }
1271
1272
    return '';
1273
}
1274
1275
1276
/**
1277
 * Create a thumbnail.
1278
 *
1279
 * @param string  $src           Source
1280
 * @param string  $dest          Destination
1281
 * @param int $desired_width Size of width
1282
 * 
1283
 * @return void|string|bool
1284
 */
1285
function makeThumbnail(string $src, string $dest, int $desired_width)
1286
{
1287
    /* read the source image */
1288
    if (is_file($src) === true && mime_content_type($src) === 'image/png') {
1289
        $source_image = imagecreatefrompng($src);
1290
        if ($source_image === false) {
1291
            return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1292
        }
1293
    } else {
1294
        return "Error: Not a valid PNG file! It's type is ".mime_content_type($src);
1295
    }
1296
1297
    // Get height and width
1298
    $width = imagesx($source_image);
1299
    $height = imagesy($source_image);
1300
    /* find the "desired height" of this thumbnail, relative to the desired width  */
1301
    $desired_height = (int) floor($height * $desired_width / $width);
1302
    /* create a new, "virtual" image */
1303
    $virtual_image = imagecreatetruecolor($desired_width, $desired_height);
1304
    if ($virtual_image === false) {
1305
        return false;
1306
    }
1307
    /* copy source image at a resized size */
1308
    imagecopyresampled($virtual_image, $source_image, 0, 0, 0, 0, $desired_width, $desired_height, $width, $height);
1309
    /* create the physical thumbnail image to its destination */
1310
    imagejpeg($virtual_image, $dest);
1311
}
1312
1313
/**
1314
 * Check table prefix in SQL query.
1315
 *
1316
 * @param string $table Table name
1317
 * 
1318
 * @return string
1319
 */
1320
function prefixTable(string $table): string
1321
{
1322
    $safeTable = htmlspecialchars(DB_PREFIX . $table);
1323
    return $safeTable;
1324
}
1325
1326
/**
1327
 * GenerateGenericPassword
1328
 *
1329
 * @param int     $size      Length
1330
 * @param bool $secure Secure
1331
 * @param bool $numerals Numerics
1332
 * @param bool $uppercase Uppercase letters
1333
 * @param bool $symbols Symbols
1334
 * @param bool $lowercase Lowercase
1335
 * @param array   $SETTINGS  SETTINGS
1336
 * 
1337
 * @return string
1338
 */
1339
function generateGenericPassword(
1340
    int $size,
1341
    bool $secure,
1342
    bool $lowercase,
1343
    bool $capitalize,
1344
    bool $numerals,
1345
    bool $symbols,
1346
    array $SETTINGS
1347
): string
1348
{
1349
    if ((int) $size > (int) $SETTINGS['pwd_maximum_length']) {
1350
        return prepareExchangedData(
1351
            array(
1352
                'error_msg' => 'Password length is too long! ',
1353
                'error' => 'true',
1354
            ),
1355
            'encode'
1356
        );
1357
    }
1358
    // Generate password
1359
    $passwordManager = new PasswordManager();
1360
    $newPassword = $passwordManager->generatePassword(
1361
        $size,
1362
        $secure,
1363
        $lowercase,
1364
        $capitalize,
1365
        $numerals,
1366
        $symbols
1367
    );
1368
1369
    return prepareExchangedData(
1370
        array(
1371
            'key' => $newPassword,
1372
            'error' => '',
1373
        ),
1374
        'encode'
1375
    );
1376
}
1377
1378
/**
1379
 * Send sysLOG message
1380
 *
1381
 * @param string    $message
1382
 * @param string    $host
1383
 * @param int       $port
1384
 * @param string    $component
1385
 * 
1386
 * @return void
1387
*/
1388
function send_syslog($message, $host, $port, $component = 'teampass'): void
1389
{
1390
    $sock = socket_create(AF_INET, SOCK_DGRAM, SOL_UDP);
1391
    $syslog_message = '<123>' . date('M d H:i:s ') . $component . ': ' . $message;
1392
    socket_sendto($sock, (string) $syslog_message, strlen($syslog_message), 0, (string) $host, (int) $port);
1393
    socket_close($sock);
1394
}
1395
1396
/**
1397
 * Permits to log events into DB
1398
 *
1399
 * @param array  $SETTINGS Teampass settings
1400
 * @param string $type     Type
1401
 * @param string $label    Label
1402
 * @param string $who      Who
1403
 * @param string $login    Login
1404
 * @param string|int $field_1  Field
1405
 * 
1406
 * @return void
1407
 */
1408
function logEvents(
1409
    array $SETTINGS, 
1410
    string $type, 
1411
    string $label, 
1412
    string $who, 
1413
    ?string $login = null, 
1414
    $field_1 = null
1415
): void
1416
{
1417
    if (empty($who)) {
1418
        $who = getClientIpServer();
1419
    }
1420
1421
    // Load class DB
1422
    loadClasses('DB');
1423
1424
    DB::insert(
1425
        prefixTable('log_system'),
1426
        [
1427
            'type' => $type,
1428
            'date' => time(),
1429
            'label' => $label,
1430
            'qui' => $who,
1431
            'field_1' => $field_1 === null ? '' : $field_1,
1432
        ]
1433
    );
1434
    // If SYSLOG
1435
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1436
        if ($type === 'user_mngt') {
1437
            send_syslog(
1438
                'action=' . str_replace('at_', '', $label) . ' attribute=user user=' . $who . ' userid="' . $login . '" change="' . $field_1 . '" ',
1439
                $SETTINGS['syslog_host'],
1440
                $SETTINGS['syslog_port'],
1441
                'teampass'
1442
            );
1443
        } else {
1444
            send_syslog(
1445
                'action=' . $type . ' attribute=' . $label . ' user=' . $who . ' userid="' . $login . '" ',
1446
                $SETTINGS['syslog_host'],
1447
                $SETTINGS['syslog_port'],
1448
                'teampass'
1449
            );
1450
        }
1451
    }
1452
}
1453
1454
/**
1455
 * Log events.
1456
 *
1457
 * @param array  $SETTINGS        Teampass settings
1458
 * @param int    $item_id         Item id
1459
 * @param string $item_label      Item label
1460
 * @param int    $id_user         User id
1461
 * @param string $action          Code for reason
1462
 * @param string $login           User login
1463
 * @param string $raison          Code for reason
1464
 * @param string $encryption_type Encryption on
1465
 * @param string $time Encryption Time
1466
 * @param string $old_value       Old value
1467
 * 
1468
 * @return void
1469
 */
1470
function logItems(
1471
    array $SETTINGS,
1472
    int $item_id,
1473
    string $item_label,
1474
    int $id_user,
1475
    string $action,
1476
    ?string $login = null,
1477
    ?string $raison = null,
1478
    ?string $encryption_type = null,
1479
    ?string $time = null,
1480
    ?string $old_value = null
1481
): void {
1482
    // Load class DB
1483
    loadClasses('DB');
1484
1485
    // Insert log in DB
1486
    DB::insert(
1487
        prefixTable('log_items'),
1488
        [
1489
            'id_item' => $item_id,
1490
            'date' => is_null($time) === true ? time() : $time,
1491
            'id_user' => $id_user,
1492
            'action' => $action,
1493
            'raison' => $raison,
1494
            'old_value' => $old_value,
1495
            'encryption_type' => is_null($encryption_type) === true ? TP_ENCRYPTION_NAME : $encryption_type,
1496
        ]
1497
    );
1498
    // Timestamp the last change
1499
    if (in_array($action, ['at_creation', 'at_modifiation', 'at_delete', 'at_import'], true)) {
1500
        DB::update(
1501
            prefixTable('misc'),
1502
            [
1503
                'valeur' => time(),
1504
                'updated_at' => time(),
1505
            ],
1506
            'type = %s AND intitule = %s',
1507
            'timestamp',
1508
            'last_item_change'
1509
        );
1510
    }
1511
1512
    // SYSLOG
1513
    if (isset($SETTINGS['syslog_enable']) === true && (int) $SETTINGS['syslog_enable'] === 1) {
1514
        // Extract reason
1515
        $attribute = is_null($raison) === true ? Array('') : explode(' : ', $raison);
1516
        // Get item info if not known
1517
        if (empty($item_label) === true) {
1518
            $dataItem = DB::queryFirstRow(
1519
                'SELECT id, id_tree, label
1520
                FROM ' . prefixTable('items') . '
1521
                WHERE id = %i',
1522
                $item_id
1523
            );
1524
            $item_label = $dataItem['label'];
1525
        }
1526
1527
        send_syslog(
1528
            'action=' . str_replace('at_', '', $action) .
1529
                ' attribute=' . str_replace('at_', '', $attribute[0]) .
1530
                ' itemno=' . $item_id .
1531
                ' user=' . (is_null($login) === true ? '' : addslashes((string) $login)) .
1532
                ' itemname="' . addslashes($item_label) . '"',
1533
            $SETTINGS['syslog_host'],
1534
            $SETTINGS['syslog_port'],
1535
            'teampass'
1536
        );
1537
    }
1538
1539
    // send notification if enabled
1540
    //notifyOnChange($item_id, $action, $SETTINGS);
1541
}
1542
1543
/**
1544
 * Prepare notification email to subscribers.
1545
 *
1546
 * @param int    $item_id  Item id
1547
 * @param string $label    Item label
1548
 * @param array  $changes  List of changes
1549
 * @param array  $SETTINGS Teampass settings
1550
 * 
1551
 * @return void
1552
 */
1553
function notifyChangesToSubscribers(int $item_id, string $label, array $changes, array $SETTINGS): void
1554
{
1555
    $session = SessionManager::getSession();
1556
    $lang = new Language($session->get('user-language') ?? 'english');
1557
    $globalsUserId = $session->get('user-id');
1558
    $globalsLastname = $session->get('user-lastname');
1559
    $globalsName = $session->get('user-name');
1560
    // send email to user that what to be notified
1561
    $notification = DB::queryFirstField(
1562
        'SELECT email
1563
        FROM ' . prefixTable('notification') . ' AS n
1564
        INNER JOIN ' . prefixTable('users') . ' AS u ON (n.user_id = u.id)
1565
        WHERE n.item_id = %i AND n.user_id != %i',
1566
        $item_id,
1567
        $globalsUserId
1568
    );
1569
    if (DB::count() > 0) {
1570
        // Prepare path
1571
        $path = geItemReadablePath($item_id, '', $SETTINGS);
1572
        // Get list of changes
1573
        $htmlChanges = '<ul>';
1574
        foreach ($changes as $change) {
1575
            $htmlChanges .= '<li>' . $change . '</li>';
1576
        }
1577
        $htmlChanges .= '</ul>';
1578
        // send email
1579
        DB::insert(
1580
            prefixTable('emails'),
1581
            [
1582
                'timestamp' => time(),
1583
                'subject' => $lang->get('email_subject_item_updated'),
1584
                'body' => str_replace(
1585
                    ['#item_label#', '#folder_name#', '#item_id#', '#url#', '#name#', '#lastname#', '#changes#'],
1586
                    [$label, $path, (string) $item_id, $SETTINGS['cpassman_url'], $globalsName, $globalsLastname, $htmlChanges],
1587
                    $lang->get('email_body_item_updated')
1588
                ),
1589
                'receivers' => implode(',', $notification),
1590
                'status' => '',
1591
            ]
1592
        );
1593
    }
1594
}
1595
1596
/**
1597
 * Returns the Item + path.
1598
 *
1599
 * @param int    $id_tree  Node id
1600
 * @param string $label    Label
1601
 * @param array  $SETTINGS TP settings
1602
 * 
1603
 * @return string
1604
 */
1605
function geItemReadablePath(int $id_tree, string $label, array $SETTINGS): string
1606
{
1607
    $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
1608
    $arbo = $tree->getPath($id_tree, true);
1609
    $path = '';
1610
    foreach ($arbo as $elem) {
1611
        if (empty($path) === true) {
1612
            $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES) . ' ';
1613
        } else {
1614
            $path .= '&#8594; ' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
1615
        }
1616
    }
1617
1618
    // Build text to show user
1619
    if (empty($label) === false) {
1620
        return empty($path) === true ? addslashes($label) : addslashes($label) . ' (' . $path . ')';
1621
    }
1622
    return empty($path) === true ? '' : $path;
1623
}
1624
1625
/**
1626
 * Get the client ip address.
1627
 *
1628
 * @return string IP address
1629
 */
1630
function getClientIpServer(): string
1631
{
1632
    if (getenv('HTTP_CLIENT_IP')) {
1633
        $ipaddress = getenv('HTTP_CLIENT_IP');
1634
    } elseif (getenv('HTTP_X_FORWARDED_FOR')) {
1635
        $ipaddress = getenv('HTTP_X_FORWARDED_FOR');
1636
    } elseif (getenv('HTTP_X_FORWARDED')) {
1637
        $ipaddress = getenv('HTTP_X_FORWARDED');
1638
    } elseif (getenv('HTTP_FORWARDED_FOR')) {
1639
        $ipaddress = getenv('HTTP_FORWARDED_FOR');
1640
    } elseif (getenv('HTTP_FORWARDED')) {
1641
        $ipaddress = getenv('HTTP_FORWARDED');
1642
    } elseif (getenv('REMOTE_ADDR')) {
1643
        $ipaddress = getenv('REMOTE_ADDR');
1644
    } else {
1645
        $ipaddress = 'UNKNOWN';
1646
    }
1647
1648
    return $ipaddress;
1649
}
1650
1651
/**
1652
 * Escape all HTML, JavaScript, and CSS.
1653
 *
1654
 * @param string $input    The input string
1655
 * @param string $encoding Which character encoding are we using?
1656
 * 
1657
 * @return string
1658
 */
1659
function noHTML(string $input, string $encoding = 'UTF-8'): string
1660
{
1661
    return htmlspecialchars($input, ENT_QUOTES | ENT_XHTML, $encoding, false);
1662
}
1663
1664
/**
1665
 * Rebuilds the Teampass config file.
1666
 *
1667
 * @param string $configFilePath Path to the config file.
1668
 * @param array  $settings       Teampass settings.
1669
 *
1670
 * @return string|bool
1671
 */
1672
function rebuildConfigFile(string $configFilePath, array $settings)
1673
{
1674
    // Perform a copy if the file exists
1675
    if (file_exists($configFilePath)) {
1676
        $backupFilePath = $configFilePath . '.' . date('Y_m_d_His', time());
1677
        if (!copy($configFilePath, $backupFilePath)) {
1678
            return "ERROR: Could not copy file '$configFilePath'";
1679
        }
1680
    }
1681
1682
    // Regenerate the config file
1683
    $data = ["<?php\n", "global \$SETTINGS;\n", "\$SETTINGS = array (\n"];
1684
    $rows = DB::query('SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s', 'admin');
1685
    foreach ($rows as $record) {
1686
        $value = getEncryptedValue($record['valeur'], $record['is_encrypted']);
1687
        $data[] = "    '{$record['intitule']}' => '". htmlspecialchars_decode($value, ENT_COMPAT) . "',\n";
1688
    }
1689
    $data[] = ");\n";
1690
    $data = array_unique($data);
1691
1692
    // Update the file
1693
    file_put_contents($configFilePath, implode('', $data));
1694
1695
    return true;
1696
}
1697
1698
/**
1699
 * Returns the encrypted value if needed.
1700
 *
1701
 * @param string $value       Value to encrypt.
1702
 * @param int   $isEncrypted Is the value encrypted?
1703
 *
1704
 * @return string
1705
 */
1706
function getEncryptedValue(string $value, int $isEncrypted): string
1707
{
1708
    return $isEncrypted ? cryption($value, '', 'encrypt')['string'] : $value;
1709
}
1710
1711
/**
1712
 * Permits to replace &#92; to permit correct display
1713
 *
1714
 * @param string $input Some text
1715
 * 
1716
 * @return string
1717
 */
1718
function handleBackslash(string $input): string
1719
{
1720
    return str_replace('&amp;#92;', '&#92;', $input);
1721
}
1722
1723
/**
1724
 * Permits to load settings
1725
 * 
1726
 * @return void
1727
*/
1728
function loadSettings(): void
1729
{
1730
    global $SETTINGS;
1731
    /* LOAD CPASSMAN SETTINGS */
1732
    if (! isset($SETTINGS['loaded']) || $SETTINGS['loaded'] !== 1) {
1733
        $SETTINGS = [];
1734
        $SETTINGS['duplicate_folder'] = 0;
1735
        //by default, this is set to 0;
1736
        $SETTINGS['duplicate_item'] = 0;
1737
        //by default, this is set to 0;
1738
        $SETTINGS['number_of_used_pw'] = 5;
1739
        //by default, this value is set to 5;
1740
        $settings = [];
1741
        $rows = DB::query(
1742
            'SELECT * FROM ' . prefixTable('misc') . ' WHERE type=%s_type OR type=%s_type2',
1743
            [
1744
                'type' => 'admin',
1745
                'type2' => 'settings',
1746
            ]
1747
        );
1748
        foreach ($rows as $record) {
1749
            if ($record['type'] === 'admin') {
1750
                $SETTINGS[$record['intitule']] = $record['valeur'];
1751
            } else {
1752
                $settings[$record['intitule']] = $record['valeur'];
1753
            }
1754
        }
1755
        $SETTINGS['loaded'] = 1;
1756
        $SETTINGS['default_session_expiration_time'] = 5;
1757
    }
1758
}
1759
1760
/**
1761
 * check if folder has custom fields.
1762
 * Ensure that target one also has same custom fields
1763
 * 
1764
 * @param int $source_id
1765
 * @param int $target_id 
1766
 * 
1767
 * @return bool
1768
*/
1769
function checkCFconsistency(int $source_id, int $target_id): bool
1770
{
1771
    $source_cf = [];
1772
    $rows = DB::query(
1773
        'SELECT id_category
1774
            FROM ' . prefixTable('categories_folders') . '
1775
            WHERE id_folder = %i',
1776
        $source_id
1777
    );
1778
    foreach ($rows as $record) {
1779
        array_push($source_cf, $record['id_category']);
1780
    }
1781
1782
    $target_cf = [];
1783
    $rows = DB::query(
1784
        'SELECT id_category
1785
            FROM ' . prefixTable('categories_folders') . '
1786
            WHERE id_folder = %i',
1787
        $target_id
1788
    );
1789
    foreach ($rows as $record) {
1790
        array_push($target_cf, $record['id_category']);
1791
    }
1792
1793
    $cf_diff = array_diff($source_cf, $target_cf);
1794
    if (count($cf_diff) > 0) {
1795
        return false;
1796
    }
1797
1798
    return true;
1799
}
1800
1801
/**
1802
 * Will encrypte/decrypt a fil eusing Defuse.
1803
 *
1804
 * @param string $type        can be either encrypt or decrypt
1805
 * @param string $source_file path to source file
1806
 * @param string $target_file path to target file
1807
 * @param array  $SETTINGS    Settings
1808
 * @param string $password    A password
1809
 *
1810
 * @return string|bool
1811
 */
1812
function prepareFileWithDefuse(
1813
    string $type,
1814
    string $source_file,
1815
    string $target_file,
1816
    ?string $password = null
1817
) {
1818
    // Load AntiXSS
1819
    $antiXss = new AntiXSS();
1820
    // Protect against bad inputs
1821
    if (is_array($source_file) === true || is_array($target_file) === true) {
1822
        return 'error_cannot_be_array';
1823
    }
1824
1825
    // Sanitize
1826
    $source_file = $antiXss->xss_clean($source_file);
1827
    $target_file = $antiXss->xss_clean($target_file);
1828
    if (empty($password) === true || is_null($password) === true) {
1829
        // get KEY to define password
1830
        $ascii_key = file_get_contents(SECUREPATH.'/'.SECUREFILE);
1831
        $password = Key::loadFromAsciiSafeString($ascii_key);
1832
    }
1833
1834
    $err = '';
1835
    if ($type === 'decrypt') {
1836
        // Decrypt file
1837
        $err = defuseFileDecrypt(
1838
            $source_file,
1839
            $target_file,
1840
            $password
0 ignored issues
show
It seems like $password can also be of type Defuse\Crypto\Key; however, parameter $password of defuseFileDecrypt() does only seem to accept null|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

1840
            /** @scrutinizer ignore-type */ $password
Loading history...
1841
        );
1842
    } elseif ($type === 'encrypt') {
1843
        // Encrypt file
1844
        $err = defuseFileEncrypt(
1845
            $source_file,
1846
            $target_file,
1847
            $password
0 ignored issues
show
It seems like $password can also be of type Defuse\Crypto\Key; however, parameter $password of defuseFileEncrypt() does only seem to accept null|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

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