ItemModel::insertNewItem()   A
last analyzed

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