storeUsersShareKey()   C
last analyzed

Complexity

Conditions 14
Paths 48

Size

Total Lines 107
Code Lines 59

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 14
eloc 59
nc 48
nop 9
dl 0
loc 107
rs 6.2666
c 4
b 0
f 0

How to fix   Long Method    Complexity    Many Parameters   

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:

Many Parameters

Methods with many parameters are not only hard to understand, but their parameters also often become inconsistent when you need more, or different data.

There are several approaches to avoid long parameter lists:

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

1877
            /** @scrutinizer ignore-type */ $password
Loading history...
1878
        );
1879
    } elseif ($type === 'encrypt') {
1880
        // Encrypt file
1881
        $err = defuseFileEncrypt(
1882
            $source_file,
1883
            $target_file,
1884
            $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

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