ItemModel::prepareData()   A
last analyzed

Complexity

Conditions 1
Paths 1

Size

Total Lines 15
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 11
nc 1
nop 10
dl 0
loc 15
rs 9.9
c 0
b 0
f 0

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