ItemModel::getItems()   C
last analyzed

Complexity

Conditions 10
Paths 14

Size

Total Lines 97
Code Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 10
eloc 64
nc 14
nop 4
dl 0
loc 97
rs 6.9187
c 1
b 0
f 0

How to fix   Long Method    Complexity   

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-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