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

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