Passed
Push — master ( 3254be...12c274 )
by Nils
06:09
created

ItemModel::handlePostInsertTasks()   A

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

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