Passed
Pull Request — master (#4686)
by Nils
06:48
created

safeString()   B

Complexity

Conditions 10
Paths 10

Size

Total Lines 36
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 1 Features 0
Metric Value
cc 10
eloc 15
c 1
b 1
f 0
nc 10
nop 2
dl 0
loc 36
rs 7.6666

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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

1893
            /** @scrutinizer ignore-type */ $password
Loading history...
1894
        );
1895
    } elseif ($type === 'encrypt') {
1896
        // Encrypt file
1897
        $err = defuseFileEncrypt(
1898
            $source_file,
1899
            $target_file,
1900
            $password
0 ignored issues
show
Bug introduced by
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

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