Issues (22)

Security Analysis    no vulnerabilities found

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

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

sources/main.functions.php (7 issues)

Code
1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      main.functions.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use LdapRecord\Connection;
33
use Elegant\Sanitizer\Sanitizer;
34
use voku\helper\AntiXSS;
35
use Hackzilla\PasswordGenerator\Generator\ComputerPasswordGenerator;
36
use Hackzilla\PasswordGenerator\RandomGenerator\Php7RandomGenerator;
37
use TeampassClasses\SessionManager\SessionManager;
38
use Symfony\Component\HttpFoundation\Request as SymfonyRequest;
39
use TeampassClasses\Language\Language;
40
use TeampassClasses\NestedTree\NestedTree;
41
use Defuse\Crypto\Key;
42
use Defuse\Crypto\Crypto;
43
use Defuse\Crypto\KeyProtectedByPassword;
44
use Defuse\Crypto\File as CryptoFile;
45
use Defuse\Crypto\Exception as CryptoException;
46
use TeampassClasses\PasswordManager\PasswordManager;
47
use Symfony\Component\Process\PhpExecutableFinder;
48
use TeampassClasses\Encryption\Encryption;
49
use TeampassClasses\ConfigManager\ConfigManager;
50
use TeampassClasses\EmailService\EmailService;
51
use TeampassClasses\EmailService\EmailSettings;
52
53
header('Content-type: text/html; charset=utf-8');
54
header('Cache-Control: no-cache, must-revalidate');
55
56
loadClasses('DB');
57
$session = SessionManager::getSession();
58
59
// Load config if $SETTINGS not defined
60
$configManager = new ConfigManager();
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
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
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
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
The condition $userId === SSH_USER_ID is always false.
Loading history...
2759
        || $userId === API_USER_ID
0 ignored issues
show
The condition $userId === API_USER_ID is always false.
Loading history...
2760
        || $userId === TP_USER_ID
0 ignored issues
show
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
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
}