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

ItemModel::addItem()   A

Complexity

Conditions 2
Paths 14

Size

Total Lines 59
Code Lines 23

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 23
nc 14
nop 12
dl 0
loc 59
rs 9.552
c 0
b 0
f 0

How to fix   Long Method    Many Parameters   

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:

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