Passed
Push — master ( bcd7e5...227aa3 )
by Nils
06:18
created

ItemModel::insertNewItem()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 27
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 21
nc 1
nop 6
dl 0
loc 27
rs 9.584
c 0
b 0
f 0
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-2024 Teampass.net
27
 * @license   GPL-3.0
28
 * @see       https://www.teampass.net
29
 */
30
31
use TeampassClasses\NestedTree\NestedTree;
32
use ZxcvbnPhp\Zxcvbn;
33
34
require_once API_ROOT_PATH . "/Model/Database.php";
35
36
class ItemModel extends Database
37
{
38
39
40
    /**
41
     * Get the list of items to return
42
     *
43
     * @param string $sqlExtra
44
     * @param integer $limit
45
     * @param string $userPrivateKey
46
     * @param integer $userId
47
     * 
48
     * @return array
49
     */
50
    public function getItems(string $sqlExtra, int $limit, string $userPrivateKey, int $userId): array
51
    {
52
        $rows = $this->select(
53
            "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, t.title as folder_label
54
            FROM ".prefixTable('items')." as i
55
            LEFT JOIN ".prefixTable('nested_tree')." as t ON (t.id = i.id_tree) ".
56
            $sqlExtra . 
57
            " ORDER BY i.id ASC" .
58
            //($limit > 0 ? " LIMIT ?". ["i", $limit] : '')
59
            ($limit > 0 ? " LIMIT ". $limit : '')
60
        );
61
        $ret = [];
62
        foreach ($rows as $row) {
63
            $userKey = $this->select(
64
                'SELECT share_key
65
                FROM ' . prefixTable('sharekeys_items') . '
66
                WHERE user_id = '.$userId.' AND object_id = '.$row['id']                
67
            );
68
            if (count($userKey) === 0 || empty($row['pw']) === true) {
69
                // No share key found
70
                // Exit this item
71
                continue;
72
            }
73
74
            // Get password
75
            try {
76
                $pwd = base64_decode(
77
                    (string) doDataDecryption(
78
                        $row['pw'],
79
                        decryptUserObjectKey(
80
                            $userKey[0]['share_key'],
81
                            $userPrivateKey
82
                        )
83
                    )
84
                );
85
            } catch (Exception $e) {
86
                // Password is not encrypted
87
                // deepcode ignore ServerLeak: No important data
88
                echo "ERROR";
89
            }
90
            
91
92
            // get path to item
93
            $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
94
            $arbo = $tree->getPath($row['id_tree'], false);
95
            $path = '';
96
            foreach ($arbo as $elem) {
97
                if (empty($path) === true) {
98
                    $path = htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
99
                } else {
100
                    $path .= '/' . htmlspecialchars(stripslashes(htmlspecialchars_decode($elem->title, ENT_QUOTES)), ENT_QUOTES);
101
                }
102
            }
103
104
            array_push(
105
                $ret,
106
                [
107
                    'id' => (int) $row['id'],
108
                    'label' => $row['label'],
109
                    'description' => $row['description'],
110
                    'pwd' => $pwd,
111
                    'url' => $row['url'],
112
                    'login' => $row['login'],
113
                    'email' => $row['email'],
114
                    'viewed_no' => (int) $row['viewed_no'],
115
                    'fa_icon' => $row['fa_icon'],
116
                    'inactif' => (int) $row['inactif'],
117
                    'perso' => (int) $row['perso'],
118
                    'id_tree' => (int) $row['id_tree'],
119
                    'folder_label' => $row['folder_label'],
120
                    'path' => empty($path) === true ? '' : $path,
121
                ]
122
            );
123
        }
124
125
        return $ret;
126
    }
127
    //end getItems() 
128
129
    /**
130
     * Main function to add a new item to the database.
131
     * It handles data preparation, validation, password checks, folder settings,
132
     * item creation, and post-insertion tasks (like logging, sharing, and tagging).
133
     */
134
    public function addItem(
135
        int $folderId,
136
        string $label,
137
        string $password,
138
        string $description,
139
        string $login,
140
        string $email,
141
        string $url,
142
        string $tags,
143
        string $anyone_can_modify,
144
        string $icon,
145
        int $userId,
146
        string $username
147
    ) : array
148
    {
149
        try {
150
            include_once API_ROOT_PATH . '/../sources/main.functions.php';
151
            include API_ROOT_PATH . '/../includes/config/tp.config.php';
152
153
            // Step 1: Prepare data and sanitize inputs
154
            $data = $this->prepareData($folderId, $label, $password, $description, $login, $email, $url, $tags, $anyone_can_modify, $icon);
155
            $this->validateData($data); // Step 2: Validate the data
156
157
            // Step 3: Validate password rules (length, emptiness)
158
            $this->validatePassword($password, $SETTINGS);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $SETTINGS seems to be never defined.
Loading history...
159
160
            // Step 4: Check folder settings for permission checks
161
            $itemInfos = $this->getFolderSettings($folderId);
162
163
            // Step 5: Ensure the password meets folder complexity requirements
164
            $this->checkPasswordComplexity($password, $itemInfos);
165
166
            // Step 6: Check for duplicates in the system
167
            $this->checkForDuplicates($label, $SETTINGS, $itemInfos);
168
169
            // Step 7: Encrypt password if provided
170
            $cryptedData = $this->encryptPassword($password);
171
            $passwordKey = $cryptedData['passwordKey'];
172
            $password = $cryptedData['encrypted'];
173
174
            // Step 8: Insert the new item into the database
175
            $newID = $this->insertNewItem($data, $password, $passwordKey, $userId, $itemInfos, $SETTINGS);
176
177
            // Step 9: Handle post-insert tasks (logging, sharing, tagging)
178
            $this->handlePostInsertTasks($newID, $itemInfos, $folderId, $passwordKey, $userId, $username, $tags, $data, $SETTINGS);
179
180
            // Success response
181
            return [
182
                'error' => false,
183
                'message' => 'Item added successfully',
184
                'newId' => $newID,
185
            ];
186
187
        } catch (Exception $e) {
188
            // Error response
189
            return [
190
                'error' => true,
191
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
192
                'error_message' => $e->getMessage(),
193
            ];
194
        }
195
    }
196
197
    /**
198
     * Prepares the data array for processing by combining all inputs.
199
     * @param int $folderId - Folder ID where the item is stored
200
     * @param string $label - Label or title of the item
201
     * @param string $password - Password associated with the item
202
     * @param string $description - Description of the item
203
     * @param string $login - Login associated with the item
204
     * @param string $email - Email linked to the item
205
     * @param string $url - URL for the item
206
     * @param string $tags - Tags for categorizing the item
207
     * @param string $anyone_can_modify - Permission to allow modifications by others
208
     * @param string $icon - Icon representing the item
209
     * @return array - Returns the prepared data
210
     */
211
    private function prepareData(
212
        int $folderId, string $label, string $password, string $description, string $login, 
213
        string $email, string $url, string $tags, string $anyone_can_modify, string $icon
214
    ) : array {
215
        return [
216
            'folderId' => $folderId,
217
            'label' => $label,
218
            'password' => $password,
219
            'description' => $description,
220
            'login' => $login,
221
            'email' => $email,
222
            'tags' => $tags,
223
            'anyoneCanModify' => $anyone_can_modify,
224
            'url' => $url,
225
            'icon' => $icon,
226
        ];
227
    }
228
229
    /**
230
     * Sanitizes and validates the input data according to specified filters.
231
     * If validation fails, throws an exception.
232
     * @param array $data - Data to be validated and sanitized
233
     * @throws Exception - If the data is invalid
234
     */
235
    private function validateData(array $data) : void
236
    {
237
        $filters = [
238
            'folderId' => 'cast:integer',
239
            'label' => 'trim|escape',
240
            'password' => 'trim|escape',
241
            'description' => 'trim|escape',
242
            'login' => 'trim|escape',
243
            'email' => 'trim|escape',
244
            'tags' => 'trim|escape',
245
            'anyoneCanModify' => 'trim|escape',
246
            'url' => 'trim|escape',
247
            'icon' => 'trim|escape',
248
        ];
249
250
        $inputData = dataSanitizer($data, $filters);
251
        if (is_string($inputData)) {
252
            throw new Exception('Data is not valid');
253
        }
254
    }
255
256
    /**
257
     * Validates the password against length and empty password rules.
258
     * Throws an exception if validation fails.
259
     * @param string $password - The password to validate
260
     * @param array $SETTINGS - Global settings from configuration
261
     * @throws Exception - If the password is invalid
262
     */
263
    private function validatePassword(string $password, array $SETTINGS) : void
264
    {
265
        if ($this->isPasswordEmptyAllowed($password, $SETTINGS['create_item_without_password'])) {
266
            throw new Exception('Empty password is not allowed');
267
        }
268
269
        if (strlen($password) > $SETTINGS['pwd_maximum_length']) {
270
            throw new Exception('Password is too long (max allowed is ' . $SETTINGS['pwd_maximum_length'] . ' characters)');
271
        }
272
    }
273
274
    /**
275
     * Retrieves folder-specific settings, including permission to modify and create items.
276
     * @param int $folderId - The folder ID to fetch settings for
277
     * @return array - Returns an array with folder-specific permissions
278
     */
279
    private function getFolderSettings(int $folderId) : array
280
    {
281
        $dataFolderSettings = DB::queryFirstRow(
282
            'SELECT bloquer_creation, bloquer_modification, personal_folder
283
            FROM ' . prefixTable('nested_tree') . ' 
284
            WHERE id = %i',
285
            $folderId
286
        );
287
288
        return [
289
            'personal_folder' => $dataFolderSettings['personal_folder'],
290
            'no_complex_check_on_modification' => (int) $dataFolderSettings['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_modification'],
291
            'no_complex_check_on_creation' => (int) $dataFolderSettings['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_creation'],
292
        ];
293
    }
294
295
    /**
296
     * Validates that the password meets the complexity requirements of the folder.
297
     * Throws an exception if the password is too weak.
298
     * @param string $password - The password to check
299
     * @param array $itemInfos - Folder settings including password complexity requirements
300
     * @throws Exception - If the password complexity is insufficient
301
     */
302
    private function checkPasswordComplexity(string $password, array $itemInfos) : void
303
    {
304
        $folderComplexity = DB::queryFirstRow(
305
            'SELECT valeur
306
            FROM ' . prefixTable('misc') . '
307
            WHERE type = %s AND intitule = %i',
308
            'complex',
309
            $itemInfos['folderId']
310
        );
311
312
        $requested_folder_complexity = $folderComplexity !== null ? (int) $folderComplexity['valeur'] : 0;
313
314
        $zxcvbn = new Zxcvbn();
315
        $passwordStrength = $zxcvbn->passwordStrength($password);
316
        $passwordStrengthScore = convertPasswordStrength($passwordStrength['score']);
317
318
        if ($passwordStrengthScore < $requested_folder_complexity && (int) $itemInfos['no_complex_check_on_creation'] === 0) {
319
            throw new Exception('Password strength is too low');
320
        }
321
    }
322
323
    /**
324
     * Checks if an item with the same label already exists in the folder.
325
     * Throws an exception if duplicates are not allowed.
326
     * @param string $label - The label of the item to check for duplicates
327
     * @param array $SETTINGS - Global settings for duplicate items
328
     * @param array $itemInfos - Folder-specific settings
329
     * @throws Exception - If a duplicate item is found and not allowed
330
     */
331
    private function checkForDuplicates(string $label, array $SETTINGS, array $itemInfos) : void
332
    {
333
        $existingItem = DB::queryFirstRow(
0 ignored issues
show
Unused Code introduced by
The assignment to $existingItem is dead and can be removed.
Loading history...
334
            'SELECT * FROM ' . prefixTable('items') . '
335
            WHERE label = %s AND inactif = %i',
336
            $label,
337
            0
338
        );
339
340
        if (DB::count() > 0 && (
341
            (isset($SETTINGS['duplicate_item']) && (int) $SETTINGS['duplicate_item'] === 0)
342
            || (int) $itemInfos['personal_folder'] === 0)
343
        ) {
344
            throw new Exception('Similar item already exists. Duplicates are not allowed.');
345
        }
346
    }
347
348
    /**
349
     * Encrypts the password using the system's encryption function.
350
     * Returns an array containing both the encrypted password and the encryption key.
351
     * @param string $password - The password to encrypt
352
     * @return array - Returns the encrypted password and the encryption key
353
     */
354
    private function encryptPassword(string $password) : array
355
    {
356
        $cryptedStuff = doDataEncryption($password);
357
        return [
358
            'encrypted' => $cryptedStuff['encrypted'],
359
            'passwordKey' => $cryptedStuff['objectKey'],
360
        ];
361
    }
362
363
    /**
364
     * Inserts the new item into the database with all its associated data.
365
     * @param array $data - The item data to insert
366
     * @param string $password - The encrypted password
367
     * @param string $passwordKey - The encryption key for the password
368
     * @param int $userId - The ID of the user creating the item
369
     * @param array $itemInfos - Folder-specific settings
370
     * @param array $SETTINGS - Global settings
371
     * @return int - Returns the ID of the newly created item
372
     */
373
    private function insertNewItem(array $data, string $password, string $passwordKey, int $userId, array $itemInfos, array $SETTINGS) : int
0 ignored issues
show
Unused Code introduced by
The parameter $passwordKey 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

373
    private function insertNewItem(array $data, string $password, /** @scrutinizer ignore-unused */ string $passwordKey, int $userId, array $itemInfos, array $SETTINGS) : int

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...
Unused Code introduced by
The parameter $SETTINGS 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

373
    private function insertNewItem(array $data, string $password, string $passwordKey, int $userId, array $itemInfos, /** @scrutinizer ignore-unused */ array $SETTINGS) : int

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...
Unused Code introduced by
The parameter $userId 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

373
    private function insertNewItem(array $data, string $password, string $passwordKey, /** @scrutinizer ignore-unused */ int $userId, array $itemInfos, array $SETTINGS) : int

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...
374
    {
375
        DB::insert(
376
            prefixTable('items'),
377
            [
378
                'label' => $data['label'],
379
                'description' => $data['description'],
380
                'pw' => $password,
381
                'pw_iv' => '',
382
                'pw_len' => strlen($data['password']),
383
                'email' => $data['email'],
384
                'url' => $data['url'],
385
                'id_tree' => $data['folderId'],
386
                'login' => $data['login'],
387
                'inactif' => 0,
388
                'restricted_to' => '',
389
                'perso' => $itemInfos['personal_folder'],
390
                'anyone_can_modify' => $data['anyoneCanModify'],
391
                'complexity_level' => $this->getPasswordComplexityLevel($password),
392
                'encryption_type' => 'teampass_aes',
393
                'fa_icon' => $data['icon'],
394
                'item_key' => uniqidReal(50),
395
                'created_at' => time(),
396
            ]
397
        );
398
399
        return DB::insertId();
400
    }
401
402
    /**
403
     * Determines the complexity level of a password based on its strength score.
404
     * @param string $password - The encrypted password for which complexity is being evaluated
405
     * @return int - Returns the complexity level (0 for weak, higher numbers for stronger passwords)
406
     */
407
    private function getPasswordComplexityLevel(string $password) : int
408
    {
409
        $zxcvbn = new Zxcvbn();
410
        $passwordStrength = $zxcvbn->passwordStrength($password);
411
        return convertPasswordStrength($passwordStrength['score']);
412
    }
413
414
    /**
415
     * Handles tasks that need to be performed after the item is inserted:
416
     * 1. Stores sharing keys
417
     * 2. Logs the item creation
418
     * 3. Adds a task if the folder is not personal
419
     * 4. Adds tags to the item
420
     * @param int $newID - The ID of the newly created item
421
     * @param array $itemInfos - Folder-specific settings
422
     * @param int $folderId - Folder ID of the item
423
     * @param string $passwordKey - The encryption key for the item
424
     * @param int $userId - ID of the user creating the item
425
     * @param string $username - Username of the creator
426
     * @param string $tags - Tags to be associated with the item
427
     * @param array $data - The original data used to create the item (including the label)
428
     * @param array $SETTINGS - System settings for logging and task creation
429
     */
430
    private function handlePostInsertTasks(
431
        int $newID,
432
        array $itemInfos,
433
        int $folderId,
434
        string $passwordKey,
435
        int $userId,
436
        string $username,
437
        string $tags,
438
        array $data,
439
        array $SETTINGS
440
    ) : void {
441
        // Create share keys for the creator
442
        storeUsersShareKey(
443
            prefixTable('sharekeys_items'),
444
            (int) $itemInfos['personal_folder'],
445
            (int) $folderId,
446
            (int) $newID,
447
            $passwordKey,
448
            true,
449
            false,
450
            [],
451
            -1,
452
            $userId
453
        );
454
455
        // Log the item creation
456
        logItems($SETTINGS, $newID, $data['label'], $userId, 'at_creation', $username);
457
458
        // Create a task if the folder is not personal
459
        if ((int) $itemInfos['personal_folder'] === 0) {
460
            storeTask('new_item', $userId, 0, $folderId, $newID, $passwordKey, [], []);
461
        }
462
463
        // Add tags to the item
464
        $this->addTags($newID, $tags);
465
    }
466
467
468
    /**
469
     * Splits the tags string into individual tags and inserts them into the database.
470
     * @param int $newID - The ID of the item to associate tags with
471
     * @param string $tags - A comma-separated string of tags
472
     */
473
    private function addTags(int $newID, string $tags) : void
474
    {
475
        $tagsArray = explode(',', $tags);
476
        foreach ($tagsArray as $tag) {
477
            if (!empty($tag)) {
478
                DB::insert(
479
                    prefixTable('tags'),
480
                    ['item_id' => $newID, 'tag' => strtolower($tag)]
481
                );
482
            }
483
        }
484
    }
485
486
487
    private function isPasswordEmptyAllowed($password, $create_item_without_password)
488
    {
489
        if (
490
            empty($password) === true
491
            && null !== $create_item_without_password
492
            && (int) $create_item_without_password !== 1
493
        ) {
494
            return true;
495
        }
496
        return false;
497
    }
498
}