ItemModel::handlePostInsertTasks()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 34
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 12
c 0
b 0
f 0
nc 2
nop 9
dl 0
loc 34
rs 9.8666

How to fix   Many Parameters   

Many Parameters

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

There are several approaches to avoid long parameter lists:

1
<?php
2
/**
3
 * 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, i.favicon_url
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
                    'favicon_url' => $row['favicon_url'],
136
                ]
137
            );
138
        }
139
140
        return $ret;
141
    }
142
    //end getItems() 
143
144
    /**
145
     * Main function to add a new item to the database.
146
     * It handles data preparation, validation, password checks, folder settings,
147
     * item creation, and post-insertion tasks (like logging, sharing, and tagging).
148
     */
149
    public function addItem(
150
        array $arrItemParams
151
    ) : array
152
    {
153
        try {
154
            include_once API_ROOT_PATH . '/../sources/main.functions.php';
155
156
            // Extract parameters
157
            $folderId = (int) $arrItemParams['folder_id'];
158
            $label = (string) $arrItemParams['label'];
159
            $password = (string) $arrItemParams['password'];
160
            $tags = (string) $arrItemParams['tags'] ?? '';
161
            $userId = (int) $arrItemParams['id'];
162
            $username = (string) $arrItemParams['username'];
163
164
            // Load config
165
            $configManager = new ConfigManager();
166
            $SETTINGS = $configManager->getAllSettings();
167
168
            // Step 1: Prepare data and sanitize inputs
169
            $data = $this->prepareData($arrItemParams);
170
            $this->validateData($data); // Step 2: Validate the data
171
172
            // Step 3: Validate password rules (length, emptiness)
173
            $this->validatePassword($password, $SETTINGS);
174
175
            // Step 4: Check folder settings for permission checks
176
            $itemInfos = $this->getFolderSettings($folderId);
177
178
            // Step 5: Ensure the password meets folder complexity requirements
179
            $this->checkPasswordComplexity($password, array_merge($itemInfos, ['folderId' => $folderId]));
180
181
            // Step 6: Check for duplicates in the system
182
            $this->checkForDuplicates($label, $SETTINGS, $itemInfos);
183
184
            // Step 7: Encrypt password if provided
185
            $cryptedData = $this->encryptPassword($password);
186
            $passwordKey = $cryptedData['passwordKey'];
187
            $password = $cryptedData['encrypted'];
188
189
            // Generate favicon URL if URL is provided and favicon_url is empty
190
            if (empty($data['url']) === false) {
191
                $data['favicon_url'] = $this->getFaviconUrl($data['url']);
192
            }
193
194
            // Step 8: Insert the new item into the database
195
            $newID = $this->insertNewItem($data, $password, $itemInfos);
196
197
            // Step 9: Handle post-insert tasks (logging, sharing, tagging)
198
            $this->handlePostInsertTasks($newID, $itemInfos, $folderId, $passwordKey, $userId, $username, $tags, $data, $SETTINGS);
199
200
            // Success response
201
            return [
202
                'error' => false,
203
                'message' => 'Item added successfully',
204
                'newId' => $newID,
205
            ];
206
207
        } catch (Exception $e) {
208
            // Error response
209
            return [
210
                'error' => true,
211
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
212
                'error_message' => $e->getMessage(),
213
            ];
214
        }
215
    }
216
217
    /**
218
     * Generate favicon URL using Google service
219
     * 
220
     * @param string $url Website URL
221
     * @return string|null Favicon URL
222
     */
223
    private function getFaviconUrl(string $url): ?string
224
    {
225
        try {
226
            $parsedUrl = parse_url($url);
227
            if (!isset($parsedUrl['host'])) {
228
                return null;
229
            }
230
            
231
            $domain = $parsedUrl['host'];
232
            
233
            // Quick DNS validation (very fast, ~50-100ms)
234
            if (!$this->isValidDomain($domain)) {
235
                return null;
236
            }
237
            
238
            // Google's service handles the rest gracefully
239
            return 'https://www.google.com/s2/favicons?domain=' . $domain . '&sz=32';
240
            
241
        } catch (Exception $e) {
242
            // Silent fail
243
            error_log('Favicon URL generation failed: ' . $e->getMessage());
244
            return null;
245
        }
246
    }
247
248
    /**
249
     * Validate domain using DNS lookup only
250
     * Very fast check (~50ms) without HTTP overhead
251
     * 
252
     * @param string $domain Domain to validate
253
     * @return bool True if domain has valid DNS records
254
     */
255
    private function isValidDomain(string $domain): bool
256
    {
257
        // Check for A or AAAA DNS records
258
        return checkdnsrr($domain, 'A') || checkdnsrr($domain, 'AAAA');
259
    }
260
261
    /**
262
     * Prepares the data array for processing by combining all inputs.
263
     * @param array $arrItemParams - Array of item parameters
264
     * @return array - Returns the prepared data
265
     */
266
    private function prepareData(
267
        array $arrItemParams
268
    ) : array {
269
        return [
270
            'folderId' => (int) $arrItemParams['folder_id'],
271
            'label' => (string) $arrItemParams['label'],
272
            'password' => (string) $arrItemParams['password'],
273
            'description' => (string) $arrItemParams['description'] ?? '',
274
            'login' => (string) $arrItemParams['login'] ?? '',
275
            'email' => (string) $arrItemParams['email'] ?? '',
276
            'tags' => (string) $arrItemParams['tags'] ?? '',
277
            'anyoneCanModify' => (int) $arrItemParams['anyone_can_modify'] ?? '0',
278
            'url' => (string) $arrItemParams['url'] ?? '',
279
            'icon' => (string) $arrItemParams['icon'] ?? '',
280
            'totp' => (string) $arrItemParams['totp'] ?? '',
281
            'favicon_url' => '',
282
        ];
283
    }
284
285
    /**
286
     * Sanitizes and validates the input data according to specified filters.
287
     * If validation fails, throws an exception.
288
     * @param array $data - Data to be validated and sanitized
289
     * @throws Exception - If the data is invalid
290
     */
291
    private function validateData(array $data) : void
292
    {
293
        $filters = [
294
            'folderId' => 'cast:integer',
295
            'label' => 'trim|escape',
296
            'password' => 'trim|escape',
297
            'description' => 'trim|escape',
298
            'login' => 'trim|escape',
299
            'email' => 'trim|escape',
300
            'tags' => 'trim|escape',
301
            'anyoneCanModify' => 'trim|escape',
302
            'url' => 'trim|escape',
303
            'icon' => 'trim|escape',
304
            'totp' => 'trim|escape',
305
        ];
306
307
        $inputData = dataSanitizer($data, $filters);
308
        if (is_string($inputData)) {
309
            throw new Exception('Data is not valid');
310
        }
311
    }
312
313
    /**
314
     * Validates the password against length and empty password rules.
315
     * Throws an exception if validation fails.
316
     * @param string $password - The password to validate
317
     * @param array $SETTINGS - Global settings from configuration
318
     * @throws Exception - If the password is invalid
319
     */
320
    private function validatePassword(string $password, array $SETTINGS) : void
321
    {
322
        if ($this->isPasswordEmptyAllowed($password, $SETTINGS['create_item_without_password'])) {
323
            throw new Exception('Empty password is not allowed');
324
        }
325
326
        if (strlen($password) > $SETTINGS['pwd_maximum_length']) {
327
            throw new Exception('Password is too long (max allowed is ' . $SETTINGS['pwd_maximum_length'] . ' characters)');
328
        }
329
    }
330
331
    /**
332
     * Retrieves folder-specific settings, including permission to modify and create items.
333
     * @param int $folderId - The folder ID to fetch settings for
334
     * @return array - Returns an array with folder-specific permissions
335
     */
336
    private function getFolderSettings(int $folderId) : array
337
    {
338
        $dataFolderSettings = DB::queryFirstRow(
339
            'SELECT bloquer_creation, bloquer_modification, personal_folder
340
            FROM ' . prefixTable('nested_tree') . ' 
341
            WHERE id = %i',
342
            $folderId
343
        );
344
345
        return [
346
            'personal_folder' => $dataFolderSettings['personal_folder'],
347
            'no_complex_check_on_modification' => (int) $dataFolderSettings['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_modification'],
348
            'no_complex_check_on_creation' => (int) $dataFolderSettings['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_creation'],
349
        ];
350
    }
351
352
    /**
353
     * Validates that the password meets the complexity requirements of the folder.
354
     * Throws an exception if the password is too weak.
355
     * @param string $password - The password to check
356
     * @param array $itemInfos - Folder settings including password complexity requirements
357
     * @throws Exception - If the password complexity is insufficient
358
     */
359
    private function checkPasswordComplexity(string $password, array $itemInfos) : void
360
    {
361
        if ($itemInfos['folderId'] <= 0 || is_int($itemInfos['folderId']) === false || isset($itemInfos['folderId']) === false) {
362
            throw new Exception('Invalid folder ID for complexity check');
363
        }
364
        
365
        $folderComplexity = DB::queryFirstRow(
366
            'SELECT valeur
367
            FROM ' . prefixTable('misc') . '
368
            WHERE type = %s AND intitule = %i',
369
            'complex',
370
            $itemInfos['folderId']
371
        );
372
373
        $requested_folder_complexity = $folderComplexity !== null ? (int) $folderComplexity['valeur'] : 0;
374
375
        $zxcvbn = new Zxcvbn();
376
        $passwordStrength = $zxcvbn->passwordStrength($password);
377
        $passwordStrengthScore = convertPasswordStrength($passwordStrength['score']);
378
379
        if ($passwordStrengthScore < $requested_folder_complexity && (int) $itemInfos['no_complex_check_on_creation'] === 0) {
380
            throw new Exception('Password strength is too low');
381
        }
382
    }
383
384
    /**
385
     * Checks if an item with the same label already exists in the folder.
386
     * Throws an exception if duplicates are not allowed.
387
     * @param string $label - The label of the item to check for duplicates
388
     * @param array $SETTINGS - Global settings for duplicate items
389
     * @param array $itemInfos - Folder-specific settings
390
     * @throws Exception - If a duplicate item is found and not allowed
391
     */
392
    private function checkForDuplicates(string $label, array $SETTINGS, array $itemInfos) : void
393
    {
394
        DB::queryFirstRow(
395
            'SELECT * FROM ' . prefixTable('items') . '
396
            WHERE label = %s AND inactif = %i',
397
            $label,
398
            0
399
        );
400
401
        if (DB::count() > 0 && (
402
	     (isset($SETTINGS['duplicate_item']) && (int) $SETTINGS['duplicate_item'] === 0)
403
	     && (int) $itemInfos['personal_folder'] === 0)
404
        ) {
405
            throw new Exception('Similar item already exists. Duplicates are not allowed.');
406
        }
407
    }
408
409
    /**
410
     * Encrypts the password using the system's encryption function.
411
     * Returns an array containing both the encrypted password and the encryption key.
412
     * @param string $password - The password to encrypt
413
     * @return array - Returns the encrypted password and the encryption key
414
     */
415
    private function encryptPassword(string $password) : array
416
    {
417
        $cryptedStuff = doDataEncryption($password);
418
        return [
419
            'encrypted' => $cryptedStuff['encrypted'],
420
            'passwordKey' => $cryptedStuff['objectKey'],
421
        ];
422
    }
423
424
    /**
425
     * Inserts the new item into the database with all its associated data.
426
     * @param array $data - The item data to insert
427
     * @param string $password - The encrypted password
428
     * @param array $itemInfos - Folder-specific settings
429
     * @return int - Returns the ID of the newly created item
430
     */
431
    private function insertNewItem(array $data, string $password, array $itemInfos) : int
432
    {
433
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
434
435
        DB::insert(
436
            prefixTable('items'),
437
            [
438
                'label' => $data['label'],
439
                'description' => $data['description'],
440
                'pw' => $password,
441
                'pw_iv' => '',
442
                'pw_len' => strlen($data['password']),
443
                'email' => $data['email'],
444
                'url' => $data['url'],
445
                'id_tree' => $data['folderId'],
446
                'login' => $data['login'],
447
                'inactif' => 0,
448
                'restricted_to' => '',
449
                'perso' => $itemInfos['personal_folder'],
450
                'anyone_can_modify' => $data['anyoneCanModify'],
451
                'complexity_level' => $this->getPasswordComplexityLevel($password),
452
                'encryption_type' => 'teampass_aes',
453
                'fa_icon' => $data['icon'],
454
                'item_key' => uniqidReal(50),
455
                'created_at' => time(),
456
                'favicon_url' => $data['favicon_url'],
457
            ]
458
        );
459
460
        $newItemId = DB::insertId();
461
462
        // Handle TOTP if provided
463
        if (empty($data['totp']) === true) {
464
            $encryptedSecret = cryption(
465
                $data['totp'],
466
                '',
467
                'encrypt'
468
            );
469
470
            DB::insert(
471
                prefixTable('items_otp'),
472
                array(
473
                    'item_id' => $newItemId,
474
                    'secret' => $encryptedSecret['string'],
475
                    'phone_number' => '',
476
                    'timestamp' => time(),
477
                    'enabled' => 1,
478
                )
479
            );
480
        }
481
482
        return $newItemId;
483
    }
484
485
    /**
486
     * Determines the complexity level of a password based on its strength score.
487
     * @param string $password - The encrypted password for which complexity is being evaluated
488
     * @return int - Returns the complexity level (0 for weak, higher numbers for stronger passwords)
489
     */
490
    private function getPasswordComplexityLevel(string $password) : int
491
    {
492
        $zxcvbn = new Zxcvbn();
493
        $passwordStrength = $zxcvbn->passwordStrength($password);
494
        return convertPasswordStrength($passwordStrength['score']);
495
    }
496
497
    /**
498
     * Handles tasks that need to be performed after the item is inserted:
499
     * 1. Stores sharing keys
500
     * 2. Logs the item creation
501
     * 3. Adds a task if the folder is not personal
502
     * 4. Adds tags to the item
503
     * @param int $newID - The ID of the newly created item
504
     * @param array $itemInfos - Folder-specific settings
505
     * @param int $folderId - Folder ID of the item
506
     * @param string $passwordKey - The encryption key for the item
507
     * @param int $userId - ID of the user creating the item
508
     * @param string $username - Username of the creator
509
     * @param string $tags - Tags to be associated with the item
510
     * @param array $data - The original data used to create the item (including the label)
511
     * @param array $SETTINGS - System settings for logging and task creation
512
     */
513
    private function handlePostInsertTasks(
514
        int $newID,
515
        array $itemInfos,
516
        int $folderId,
517
        string $passwordKey,
518
        int $userId,
519
        string $username,
520
        string $tags,
521
        array $data,
522
        array $SETTINGS
523
    ) : void {
524
        // Create share keys for the creator
525
        storeUsersShareKey(
526
            'sharekeys_items',
527
            (int) $itemInfos['personal_folder'],
528
            (int) $newID,
529
            $passwordKey,
530
            true,
531
            false,
532
            [],
533
            -1,
534
            $userId
535
        );
536
537
        // Log the item creation
538
        logItems($SETTINGS, $newID, $data['label'], $userId, 'at_creation', $username);
539
540
        // Create a task if the folder is not personal
541
        if ((int) $itemInfos['personal_folder'] === 0) {
542
            storeTask('new_item', $userId, 0, $folderId, $newID, $passwordKey, [], []);
543
        }
544
545
        // Add tags to the item
546
        $this->addTags($newID, $tags);
547
    }
548
549
550
    /**
551
     * Splits the tags string into individual tags and inserts them into the database.
552
     * @param int $newID - The ID of the item to associate tags with
553
     * @param string $tags - A comma-separated string of tags
554
     */
555
    private function addTags(int $newID, string $tags) : void
556
    {
557
        $tagsArray = explode(',', $tags);
558
        foreach ($tagsArray as $tag) {
559
            if (!empty($tag)) {
560
                DB::insert(
561
                    prefixTable('tags'),
562
                    ['item_id' => $newID, 'tag' => strtolower($tag)]
563
                );
564
            }
565
        }
566
    }
567
568
569
    private function isPasswordEmptyAllowed($password, $create_item_without_password)
570
    {
571
        if (
572
            empty($password) === true
573
            && null !== $create_item_without_password
574
            && (int) $create_item_without_password !== 1
575
        ) {
576
            return true;
577
        }
578
        return false;
579
    }
580
581
    /**
582
     * Main function to update an existing item in the database.
583
     * It handles data validation, password encryption, folder permission checks,
584
     * and updates the item with the provided fields.
585
     *
586
     * @param int $itemId The ID of the item to update
587
     * @param array $params Array of parameters to update
588
     * @param array $userData User data from JWT token
589
     * @param string $userPrivateKey User's private key for encryption
590
     * @return array Returns success or error response
591
     */
592
    public function updateItem(
593
        int $itemId,
594
        array $params,
595
        array $userData,
596
        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

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