Passed
Push — master ( 378303...213640 )
by Nils
06:34
created

ItemModel::checkPasswordComplexity()   B

Complexity

Conditions 7
Paths 5

Size

Total Lines 22
Code Lines 13

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 13
c 0
b 0
f 0
nc 5
nop 2
dl 0
loc 22
rs 8.8333
1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @version    API
23
 *
24
 * @file      ItemModel.php
25
 * @author    Nils Laumaillé ([email protected])
26
 * @copyright 2009-2025 Teampass.net
27
 * @license   GPL-3.0
28
 * @see       https://www.teampass.net
29
 */
30
31
use TeampassClasses\NestedTree\NestedTree;
32
use TeampassClasses\ConfigManager\ConfigManager;
33
use ZxcvbnPhp\Zxcvbn;
34
35
class ItemModel
36
{
37
38
    /**
39
     * Get the list of items to return
40
     *
41
     * @param string $sqlExtra
42
     * @param integer $limit
43
     * @param string $userPrivateKey
44
     * @param integer $userId
45
     * 
46
     * @return array
47
     */
48
    public function getItems(string $sqlExtra, int $limit, string $userPrivateKey, int $userId): array
49
    {
50
        // Get items
51
        $rows = DB::query(
52
            'SELECT i.id, label, description, i.pw, i.url, i.id_tree, i.login, i.email, i.viewed_no, i.fa_icon, i.inactif, i.perso,
53
            t.title as folder_label, io.secret as otp_secret
54
            FROM ' . prefixTable('items') . ' AS i
55
            LEFT JOIN '.prefixTable('nested_tree').' as t ON (t.id = i.id_tree) 
56
            LEFT JOIN '.prefixTable('items_otp').' as io ON (io.item_id = i.id) '.
57
            $sqlExtra . 
58
            " ORDER BY i.id ASC" .
59
            ($limit > 0 ? " LIMIT ". $limit : '')
60
        );
61
        
62
        $ret = [];
63
        foreach ($rows as $row) {
64
            $userKey = DB::queryfirstrow(
65
                'SELECT share_key
66
                FROM ' . prefixTable('sharekeys_items') . '
67
                WHERE user_id = %i AND object_id = %i',
68
                $userId,
69
                $row['id']
70
            );
71
            if (DB::count() === 0 || empty($row['pw']) === true) {
72
                // No share key found
73
                // Exit this item
74
                continue;
75
            }
76
77
            // Get password
78
            try {
79
                $pwd = base64_decode(
80
                    (string) doDataDecryption(
81
                        $row['pw'],
82
                        decryptUserObjectKey(
83
                            $userKey['share_key'],
84
                            $userPrivateKey
85
                        )
86
                    )
87
                );
88
            } catch (Exception $e) {
89
                // Password is not encrypted
90
                // deepcode ignore ServerLeak: No important data
91
                echo "ERROR";
92
            }
93
            
94
95
            // get path to item
96
            $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
97
            $arbo = $tree->getPath($row['id_tree'], false);
98
            $path = '';
99
            foreach ($arbo as $elem) {
100
                if (empty($path) === true) {
101
                    $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
102
                } else {
103
                    $path .= '/' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
104
                }
105
            }
106
107
            // Get TOTP
108
            if (empty($row['otp_secret']) === false) {
109
                $decryptedTotp = cryption(
110
                    $row['otp_secret'],
111
                    '',
112
                    'decrypt'
113
                );
114
                $row['otp_secret'] = $decryptedTotp['string'];
115
            }
116
117
            array_push(
118
                $ret,
119
                [
120
                    'id' => (int) $row['id'],
121
                    'label' => $row['label'],
122
                    'description' => $row['description'],
123
                    'pwd' => $pwd,
124
                    'url' => $row['url'],
125
                    'login' => $row['login'],
126
                    'email' => $row['email'],
127
                    'viewed_no' => (int) $row['viewed_no'],
128
                    'fa_icon' => $row['fa_icon'],
129
                    'inactif' => (int) $row['inactif'],
130
                    'perso' => (int) $row['perso'],
131
                    'id_tree' => (int) $row['id_tree'],
132
                    'folder_label' => $row['folder_label'],
133
                    'path' => empty($path) === true ? '' : $path,
134
                    'totp' => $row['otp_secret'],
135
                ]
136
            );
137
        }
138
139
        return $ret;
140
    }
141
    //end getItems() 
142
143
    /**
144
     * Main function to add a new item to the database.
145
     * It handles data preparation, validation, password checks, folder settings,
146
     * item creation, and post-insertion tasks (like logging, sharing, and tagging).
147
     */
148
    public function addItem(
149
        int $folderId,
150
        string $label,
151
        string $password,
152
        string $description,
153
        string $login,
154
        string $email,
155
        string $url,
156
        string $tags,
157
        string $anyone_can_modify,
158
        string $icon,
159
        int $userId,
160
        string $username,
161
        string $totp
162
    ) : array
163
    {
164
        try {
165
            include_once API_ROOT_PATH . '/../sources/main.functions.php';
166
167
            // Load config
168
            $configManager = new ConfigManager();
169
            $SETTINGS = $configManager->getAllSettings();
170
171
            // Step 1: Prepare data and sanitize inputs
172
            $data = $this->prepareData($folderId, $label, $password, $description, $login, $email, $url, $tags, $anyone_can_modify, $icon, $totp);
173
            $this->validateData($data); // Step 2: Validate the data
174
175
            // Step 3: Validate password rules (length, emptiness)
176
            $this->validatePassword($password, $SETTINGS);
177
178
            // Step 4: Check folder settings for permission checks
179
            $itemInfos = $this->getFolderSettings($folderId);
180
181
            // Step 5: Ensure the password meets folder complexity requirements
182
            $this->checkPasswordComplexity($password, $itemInfos);
183
184
            // Step 6: Check for duplicates in the system
185
            $this->checkForDuplicates($label, $SETTINGS, $itemInfos);
186
187
            // Step 7: Encrypt password if provided
188
            $cryptedData = $this->encryptPassword($password);
189
            $passwordKey = $cryptedData['passwordKey'];
190
            $password = $cryptedData['encrypted'];
191
192
            // Step 8: Insert the new item into the database
193
            $newID = $this->insertNewItem($data, $password, $itemInfos);
194
195
            // Step 9: Handle post-insert tasks (logging, sharing, tagging)
196
            $this->handlePostInsertTasks($newID, $itemInfos, $folderId, $passwordKey, $userId, $username, $tags, $data, $SETTINGS);
197
198
            // Success response
199
            return [
200
                'error' => false,
201
                'message' => 'Item added successfully',
202
                'newId' => $newID,
203
            ];
204
205
        } catch (Exception $e) {
206
            // Error response
207
            return [
208
                'error' => true,
209
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
210
                'error_message' => $e->getMessage(),
211
            ];
212
        }
213
    }
214
215
    /**
216
     * Prepares the data array for processing by combining all inputs.
217
     * @param int $folderId - Folder ID where the item is stored
218
     * @param string $label - Label or title of the item
219
     * @param string $password - Password associated with the item
220
     * @param string $description - Description of the item
221
     * @param string $login - Login associated with the item
222
     * @param string $email - Email linked to the item
223
     * @param string $url - URL for the item
224
     * @param string $tags - Tags for categorizing the item
225
     * @param string $anyone_can_modify - Permission to allow modifications by others
226
     * @param string $icon - Icon representing the item
227
     * @param string $totp - Token for OTP
228
     * @return array - Returns the prepared data
229
     */
230
    private function prepareData(
231
        int $folderId, string $label, string $password, string $description, string $login, 
232
        string $email, string $url, string $tags, string $anyone_can_modify, string $icon, string $totp
233
    ) : array {
234
        return [
235
            'folderId' => $folderId,
236
            'label' => $label,
237
            'password' => $password,
238
            'description' => $description,
239
            'login' => $login,
240
            'email' => $email,
241
            'tags' => $tags,
242
            'anyoneCanModify' => $anyone_can_modify,
243
            'url' => $url,
244
            'icon' => $icon,
245
            'totp' => $totp,
246
        ];
247
    }
248
249
    /**
250
     * Sanitizes and validates the input data according to specified filters.
251
     * If validation fails, throws an exception.
252
     * @param array $data - Data to be validated and sanitized
253
     * @throws Exception - If the data is invalid
254
     */
255
    private function validateData(array $data) : void
256
    {
257
        $filters = [
258
            'folderId' => 'cast:integer',
259
            'label' => 'trim|escape',
260
            'password' => 'trim|escape',
261
            'description' => 'trim|escape',
262
            'login' => 'trim|escape',
263
            'email' => 'trim|escape',
264
            'tags' => 'trim|escape',
265
            'anyoneCanModify' => 'trim|escape',
266
            'url' => 'trim|escape',
267
            'icon' => 'trim|escape',
268
            'totp' => 'trim|escape',
269
        ];
270
271
        $inputData = dataSanitizer($data, $filters);
272
        if (is_string($inputData)) {
273
            throw new Exception('Data is not valid');
274
        }
275
    }
276
277
    /**
278
     * Validates the password against length and empty password rules.
279
     * Throws an exception if validation fails.
280
     * @param string $password - The password to validate
281
     * @param array $SETTINGS - Global settings from configuration
282
     * @throws Exception - If the password is invalid
283
     */
284
    private function validatePassword(string $password, array $SETTINGS) : void
285
    {
286
        if ($this->isPasswordEmptyAllowed($password, $SETTINGS['create_item_without_password'])) {
287
            throw new Exception('Empty password is not allowed');
288
        }
289
290
        if (strlen($password) > $SETTINGS['pwd_maximum_length']) {
291
            throw new Exception('Password is too long (max allowed is ' . $SETTINGS['pwd_maximum_length'] . ' characters)');
292
        }
293
    }
294
295
    /**
296
     * Retrieves folder-specific settings, including permission to modify and create items.
297
     * @param int $folderId - The folder ID to fetch settings for
298
     * @return array - Returns an array with folder-specific permissions
299
     */
300
    private function getFolderSettings(int $folderId) : array
301
    {
302
        $dataFolderSettings = DB::queryFirstRow(
303
            'SELECT bloquer_creation, bloquer_modification, personal_folder
304
            FROM ' . prefixTable('nested_tree') . ' 
305
            WHERE id = %i',
306
            $folderId
307
        );
308
309
        return [
310
            'personal_folder' => $dataFolderSettings['personal_folder'],
311
            'no_complex_check_on_modification' => (int) $dataFolderSettings['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_modification'],
312
            'no_complex_check_on_creation' => (int) $dataFolderSettings['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_creation'],
313
        ];
314
    }
315
316
    /**
317
     * Validates that the password meets the complexity requirements of the folder.
318
     * Throws an exception if the password is too weak.
319
     * @param string $password - The password to check
320
     * @param array $itemInfos - Folder settings including password complexity requirements
321
     * @throws Exception - If the password complexity is insufficient
322
     */
323
    private function checkPasswordComplexity(string $password, array $itemInfos) : void
324
    {
325
        if ($itemInfos['folderId'] <= 0 || is_int($itemInfos['folderId']) === false || isset($itemInfos['folderId']) === false) {
326
            throw new Exception('Invalid folder ID for complexity check');
327
        }
328
        
329
        $folderComplexity = DB::queryFirstRow(
330
            'SELECT valeur
331
            FROM ' . prefixTable('misc') . '
332
            WHERE type = %s AND intitule = %i',
333
            'complex',
334
            $itemInfos['folderId']
335
        );
336
337
        $requested_folder_complexity = $folderComplexity !== null ? (int) $folderComplexity['valeur'] : 0;
338
339
        $zxcvbn = new Zxcvbn();
340
        $passwordStrength = $zxcvbn->passwordStrength($password);
341
        $passwordStrengthScore = convertPasswordStrength($passwordStrength['score']);
342
343
        if ($passwordStrengthScore < $requested_folder_complexity && (int) $itemInfos['no_complex_check_on_creation'] === 0) {
344
            throw new Exception('Password strength is too low');
345
        }
346
    }
347
348
    /**
349
     * Checks if an item with the same label already exists in the folder.
350
     * Throws an exception if duplicates are not allowed.
351
     * @param string $label - The label of the item to check for duplicates
352
     * @param array $SETTINGS - Global settings for duplicate items
353
     * @param array $itemInfos - Folder-specific settings
354
     * @throws Exception - If a duplicate item is found and not allowed
355
     */
356
    private function checkForDuplicates(string $label, array $SETTINGS, array $itemInfos) : void
357
    {
358
        DB::queryFirstRow(
359
            'SELECT * FROM ' . prefixTable('items') . '
360
            WHERE label = %s AND inactif = %i',
361
            $label,
362
            0
363
        );
364
365
        if (DB::count() > 0 && (
366
	     (isset($SETTINGS['duplicate_item']) && (int) $SETTINGS['duplicate_item'] === 0)
367
	     && (int) $itemInfos['personal_folder'] === 0)
368
        ) {
369
            throw new Exception('Similar item already exists. Duplicates are not allowed.');
370
        }
371
    }
372
373
    /**
374
     * Encrypts the password using the system's encryption function.
375
     * Returns an array containing both the encrypted password and the encryption key.
376
     * @param string $password - The password to encrypt
377
     * @return array - Returns the encrypted password and the encryption key
378
     */
379
    private function encryptPassword(string $password) : array
380
    {
381
        $cryptedStuff = doDataEncryption($password);
382
        return [
383
            'encrypted' => $cryptedStuff['encrypted'],
384
            'passwordKey' => $cryptedStuff['objectKey'],
385
        ];
386
    }
387
388
    /**
389
     * Inserts the new item into the database with all its associated data.
390
     * @param array $data - The item data to insert
391
     * @param string $password - The encrypted password
392
     * @param array $itemInfos - Folder-specific settings
393
     * @return int - Returns the ID of the newly created item
394
     */
395
    private function insertNewItem(array $data, string $password, array $itemInfos) : int
396
    {
397
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
398
399
        DB::insert(
400
            prefixTable('items'),
401
            [
402
                'label' => $data['label'],
403
                'description' => $data['description'],
404
                'pw' => $password,
405
                'pw_iv' => '',
406
                'pw_len' => strlen($data['password']),
407
                'email' => $data['email'],
408
                'url' => $data['url'],
409
                'id_tree' => $data['folderId'],
410
                'login' => $data['login'],
411
                'inactif' => 0,
412
                'restricted_to' => '',
413
                'perso' => $itemInfos['personal_folder'],
414
                'anyone_can_modify' => $data['anyoneCanModify'],
415
                'complexity_level' => $this->getPasswordComplexityLevel($password),
416
                'encryption_type' => 'teampass_aes',
417
                'fa_icon' => $data['icon'],
418
                'item_key' => uniqidReal(50),
419
                'created_at' => time(),
420
            ]
421
        );
422
423
        $newItemId = DB::insertId();
424
425
        // Handle TOTP if provided
426
        if (empty($data['totp']) === true) {
427
            $encryptedSecret = cryption(
428
                $data['totp'],
429
                '',
430
                'encrypt'
431
            );
432
433
            DB::insert(
434
                prefixTable('items_otp'),
435
                array(
436
                    'item_id' => $newItemId,
437
                    'secret' => $encryptedSecret['string'],
438
                    'phone_number' => '',
439
                    'timestamp' => time(),
440
                    'enabled' => 1,
441
                )
442
            );
443
        }
444
445
        return $newItemId;
446
    }
447
448
    /**
449
     * Determines the complexity level of a password based on its strength score.
450
     * @param string $password - The encrypted password for which complexity is being evaluated
451
     * @return int - Returns the complexity level (0 for weak, higher numbers for stronger passwords)
452
     */
453
    private function getPasswordComplexityLevel(string $password) : int
454
    {
455
        $zxcvbn = new Zxcvbn();
456
        $passwordStrength = $zxcvbn->passwordStrength($password);
457
        return convertPasswordStrength($passwordStrength['score']);
458
    }
459
460
    /**
461
     * Handles tasks that need to be performed after the item is inserted:
462
     * 1. Stores sharing keys
463
     * 2. Logs the item creation
464
     * 3. Adds a task if the folder is not personal
465
     * 4. Adds tags to the item
466
     * @param int $newID - The ID of the newly created item
467
     * @param array $itemInfos - Folder-specific settings
468
     * @param int $folderId - Folder ID of the item
469
     * @param string $passwordKey - The encryption key for the item
470
     * @param int $userId - ID of the user creating the item
471
     * @param string $username - Username of the creator
472
     * @param string $tags - Tags to be associated with the item
473
     * @param array $data - The original data used to create the item (including the label)
474
     * @param array $SETTINGS - System settings for logging and task creation
475
     */
476
    private function handlePostInsertTasks(
477
        int $newID,
478
        array $itemInfos,
479
        int $folderId,
480
        string $passwordKey,
481
        int $userId,
482
        string $username,
483
        string $tags,
484
        array $data,
485
        array $SETTINGS
486
    ) : void {
487
        // Create share keys for the creator
488
        storeUsersShareKey(
489
            'sharekeys_items',
490
            (int) $itemInfos['personal_folder'],
491
            (int) $newID,
492
            $passwordKey,
493
            true,
494
            false,
495
            [],
496
            -1,
497
            $userId
498
        );
499
500
        // Log the item creation
501
        logItems($SETTINGS, $newID, $data['label'], $userId, 'at_creation', $username);
502
503
        // Create a task if the folder is not personal
504
        if ((int) $itemInfos['personal_folder'] === 0) {
505
            storeTask('new_item', $userId, 0, $folderId, $newID, $passwordKey, [], []);
506
        }
507
508
        // Add tags to the item
509
        $this->addTags($newID, $tags);
510
    }
511
512
513
    /**
514
     * Splits the tags string into individual tags and inserts them into the database.
515
     * @param int $newID - The ID of the item to associate tags with
516
     * @param string $tags - A comma-separated string of tags
517
     */
518
    private function addTags(int $newID, string $tags) : void
519
    {
520
        $tagsArray = explode(',', $tags);
521
        foreach ($tagsArray as $tag) {
522
            if (!empty($tag)) {
523
                DB::insert(
524
                    prefixTable('tags'),
525
                    ['item_id' => $newID, 'tag' => strtolower($tag)]
526
                );
527
            }
528
        }
529
    }
530
531
532
    private function isPasswordEmptyAllowed($password, $create_item_without_password)
533
    {
534
        if (
535
            empty($password) === true
536
            && null !== $create_item_without_password
537
            && (int) $create_item_without_password !== 1
538
        ) {
539
            return true;
540
        }
541
        return false;
542
    }
543
544
    /**
545
     * Main function to update an existing item in the database.
546
     * It handles data validation, password encryption, folder permission checks,
547
     * and updates the item with the provided fields.
548
     *
549
     * @param int $itemId The ID of the item to update
550
     * @param array $params Array of parameters to update
551
     * @param array $userData User data from JWT token
552
     * @param string $userPrivateKey User's private key for encryption
553
     * @return array Returns success or error response
554
     */
555
    public function updateItem(
556
        int $itemId,
557
        array $params,
558
        array $userData,
559
        string $userPrivateKey
0 ignored issues
show
Unused Code introduced by
The parameter $userPrivateKey is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

559
        /** @scrutinizer ignore-unused */ string $userPrivateKey

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
560
    ): array
561
    {
562
        try {
563
            include_once API_ROOT_PATH . '/../sources/main.functions.php';
564
565
            // Load config
566
            $configManager = new ConfigManager();
567
            $SETTINGS = $configManager->getAllSettings();
568
569
            // Load current item data
570
            $currentItem = DB::queryFirstRow(
571
                'SELECT * FROM ' . prefixTable('items') . ' WHERE id = %i',
572
                $itemId
573
            );
574
575
            if (DB::count() === 0) {
576
                return [
577
                    'error' => true,
578
                    'error_message' => 'Item not found',
579
                    'error_header' => 'HTTP/1.1 404 Not Found',
580
                ];
581
            }
582
583
            // Prepare update data
584
            $updateData = [];
585
            $passwordKey = null;
586
            $newPassword = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $newPassword is dead and can be removed.
Loading history...
587
588
            // Handle folder_id change
589
            if (isset($params['folder_id'])) {
590
                $newFolderId = (int) $params['folder_id'];
591
592
                // Check if user has access to the new folder
593
                if (!in_array((string) $newFolderId, $userData['folders_list'], true)) {
594
                    return [
595
                        'error' => true,
596
                        'error_message' => 'Access denied to the target folder',
597
                        'error_header' => 'HTTP/1.1 403 Forbidden',
598
                    ];
599
                }
600
601
                $updateData['id_tree'] = $newFolderId;
602
            }
603
604
            // Handle label update
605
            if (isset($params['label'])) {
606
                $updateData['label'] = filter_var($params['label'], FILTER_SANITIZE_STRING);
607
            }
608
609
            // Handle description update
610
            if (isset($params['description'])) {
611
                $updateData['description'] = $params['description'];
612
            }
613
614
            // Handle login update
615
            if (isset($params['login'])) {
616
                $updateData['login'] = filter_var($params['login'], FILTER_SANITIZE_STRING);
617
            }
618
619
            // Handle email update
620
            if (isset($params['email'])) {
621
                $updateData['email'] = filter_var($params['email'], FILTER_SANITIZE_EMAIL);
622
            }
623
624
            // Handle url update
625
            if (isset($params['url'])) {
626
                $updateData['url'] = filter_var($params['url'], FILTER_SANITIZE_URL);
627
            }
628
629
            // Handle icon update
630
            if (isset($params['icon'])) {
631
                $updateData['fa_icon'] = filter_var($params['icon'], FILTER_SANITIZE_STRING);
632
            }
633
634
            // Handle anyone_can_modify update
635
            if (isset($params['anyone_can_modify'])) {
636
                $updateData['anyone_can_modify'] = (int) $params['anyone_can_modify'];
637
            }
638
639
            // Handle password update
640
            if (isset($params['password']) && !empty($params['password'])) {
641
                $newPassword = $params['password'];
642
643
                // Validate password length
644
                if (strlen($newPassword) > $SETTINGS['pwd_maximum_length']) {
645
                    return [
646
                        'error' => true,
647
                        'error_message' => 'Password is too long (max allowed is ' . $SETTINGS['pwd_maximum_length'] . ' characters)',
648
                        'error_header' => 'HTTP/1.1 400 Bad Request',
649
                    ];
650
                }
651
652
                // Get folder ID for complexity check
653
                $folderId = isset($updateData['id_tree']) ? $updateData['id_tree'] : $currentItem['id_tree'];
654
655
                // Get folder settings
656
                $itemInfos = $this->getFolderSettings((int) $folderId);
657
658
                // Check password complexity
659
                $this->checkPasswordComplexity($newPassword, array_merge($itemInfos, ['folderId' => $folderId]));
660
661
                // Encrypt password
662
                $cryptedData = $this->encryptPassword($newPassword);
663
                $passwordKey = $cryptedData['passwordKey'];
664
                $updateData['pw'] = $cryptedData['encrypted'];
665
                $updateData['pw_len'] = strlen($newPassword);
666
                $updateData['complexity_level'] = $this->getPasswordComplexityLevel($newPassword);
667
            }
668
            
669
            // Update the item
670
            if (!empty($updateData) || (isset($params['totp']) === true && empty($params['totp']) === false)) {
671
                $updateData['updated_at'] = time();
672
673
                DB::update(
674
                    prefixTable('items'),
675
                    $updateData,
676
                    'id = %i',
677
                    $itemId
678
                );
679
680
                // Handle TOTP update
681
                if (isset($params['totp']) === true && empty($params['totp']) === false) {
682
                    $encryptedSecret = cryption(
683
                        $params['totp'],
684
                        '',
685
                        'encrypt'
686
                    );
687
                    
688
                    DB::insertUpdate(
689
                        prefixTable('items_otp'),
690
                        [
691
                            'item_id' => $itemId,
692
                            'secret' => $encryptedSecret['string'],
693
                            'phone_number' => '',
694
                            'timestamp' => time(),
695
                            'enabled' => 1,
696
                        ]
697
                    );
698
                }
699
            } else if (empty($updateData)) {
700
                // Check if an entry exists for TOTP in items_otp table
701
                // IF yes delete it
702
                if (isset($params['totp']) === true && empty($params['totp']) === true) {
703
                    DB::delete(
704
                        prefixTable('items_otp'),
705
                        'item_id = %i',
706
                        $itemId
707
                    );
708
                }
709
            }
710
711
            // Handle tags update
712
            if (isset($params['tags'])) {
713
                // Delete existing tags
714
                DB::delete(
715
                    prefixTable('tags'),
716
                    'item_id = %i',
717
                    $itemId
718
                );
719
720
                // Add new tags
721
                $this->addTags($itemId, $params['tags']);
722
            }
723
724
            // If password was updated, update share keys
725
            if ($passwordKey !== null) {
726
                // Get folder ID (either new or current)
727
                $folderId = isset($updateData['id_tree']) ? $updateData['id_tree'] : $currentItem['id_tree'];
728
729
                // Get folder settings
730
                $itemInfos = $this->getFolderSettings((int) $folderId);
731
732
                // Update share keys for all users with access
733
                storeUsersShareKey(
734
                    'sharekeys_items',
735
                    (int) $itemInfos['personal_folder'],
736
                    $itemId,
737
                    $passwordKey,
738
                    false,
739
                    true,
740
                    [],
741
                    -1,
742
                    $userData['id']
743
                );
744
            }
745
746
            // Log the update
747
            $label = isset($updateData['label']) ? $updateData['label'] : $currentItem['label'];
748
            logItems($SETTINGS, $itemId, $label, $userData['id'], 'at_modification', $userData['username']);
749
750
            // Success response
751
            return [
752
                'error' => false,
753
                'message' => 'Item updated successfully',
754
                'item_id' => $itemId,
755
            ];
756
757
        } catch (Exception $e) {
758
            // Error response
759
            return [
760
                'error' => true,
761
                'error_header' => 'HTTP/1.1 500 Internal Server Error',
762
                'error_message' => $e->getMessage(),
763
            ];
764
        }
765
    }
766
767
    /**
768
     * Main function to delete an existing item in the database.
769
     *
770
     * @param int $itemId The ID of the item to delete
771
     * @param array $userData User data from JWT token
772
     * @return array Returns success or error response
773
     */
774
    public function deleteItem(
775
        int $itemId,
776
        array $userData
777
    ): array
778
    {
779
        try {
780
            include_once API_ROOT_PATH . '/../sources/main.functions.php';
781
782
            // Load config
783
            $configManager = new ConfigManager();
784
            $SETTINGS = $configManager->getAllSettings();
785
786
            // Load current item data
787
            $currentItem = DB::queryFirstRow(
788
                'SELECT * FROM ' . prefixTable('items') . ' WHERE id = %i',
789
                $itemId
790
            );
791
792
            if (DB::count() === 0) {
793
                return [
794
                    'error' => true,
795
                    'error_message' => 'Item not found',
796
                    'error_header' => 'HTTP/1.1 404 Not Found',
797
                ];
798
            }
799
800
            // delete item consists in disabling it
801
            DB::update(
802
                prefixTable('items'),
803
                array(
804
                    'inactif' => '1',
805
                    'deleted_at' => time(),
806
                ),
807
                'id = %i',
808
                $itemId
809
            );
810
811
            logItems($SETTINGS, $itemId, $currentItem['label'], $userData['id'], 'at_delete', $userData['username']);
812
813
            // Success response
814
            return [
815
                'error' => false,
816
                'message' => 'Item deleted successfully',
817
                'item_id' => $itemId,
818
            ];
819
820
        } catch (Exception $e) {
821
            // Error response
822
            return [
823
                'error' => true,
824
                'error_header' => 'HTTP/1.1 500 Internal Server Error',
825
                'error_message' => $e->getMessage(),
826
            ];
827
        }
828
    }
829
830
}
831