ItemModel::addItem()   A
last analyzed

Complexity

Conditions 3
Paths 26

Size

Total Lines 64
Code Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 32
nc 26
nop 1
dl 0
loc 64
rs 9.408
c 0
b 0
f 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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