userHasAccessToBackupFile()   A
last analyzed

Complexity

Conditions 6
Paths 4

Size

Total Lines 36
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

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

1847
            /** @scrutinizer ignore-type */ $password
Loading history...
1848
        );
1849
    } elseif ($type === 'encrypt') {
1850
        // Encrypt file
1851
        $err = defuseFileEncrypt(
1852
            $source_file,
1853
            $target_file,
1854
            $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

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