Passed
Push — master ( 82c165...9440db )
by Nils
10:41
created

ItemModel::isPasswordEmptyAllowed()   A

Complexity

Conditions 4
Paths 2

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 4
eloc 6
nc 2
nop 3
dl 0
loc 10
rs 10
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 TeampassClasses\Language\Language;
33
use ZxcvbnPhp\Zxcvbn;
34
35
require_once API_ROOT_PATH . "/Model/Database.php";
36
37
class ItemModel extends Database
38
{
39
40
41
    /**
42
     * Get the list of items to return
43
     *
44
     * @param string $sqlExtra
45
     * @param integer $limit
46
     * @param string $userPrivateKey
47
     * @param integer $userId
48
     * 
49
     * @return array
50
     */
51
    public function getItems(string $sqlExtra, int $limit, string $userPrivateKey, int $userId): array
52
    {
53
        $rows = $this->select(
54
            "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
55
            FROM ".prefixTable('items')." as i
56
            LEFT JOIN ".prefixTable('nested_tree')." as t ON (t.id = i.id_tree) ".
57
            $sqlExtra . 
58
            " ORDER BY i.id ASC" .
59
            //($limit > 0 ? " LIMIT ?". ["i", $limit] : '')
60
            ($limit > 0 ? " LIMIT ". $limit : '')
61
        );
62
        $ret = [];
63
        foreach ($rows as $row) {
64
            $userKey = $this->select(
65
                'SELECT share_key
66
                FROM ' . prefixTable('sharekeys_items') . '
67
                WHERE user_id = '.$userId.' AND object_id = '.$row['id']                
68
            );
69
            if (count($userKey) === 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[0]['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
     * Add an item
132
     *
133
     * @param integer $folderId
134
     * @param string $label
135
     * @param string $password
136
     * @param string $description
137
     * @param string $login
138
     * @param string $email
139
     * @param string $url
140
     * @param string $tags
141
     * @param string $anyone_can_modify
142
     * @param string $icon
143
     * @param integer $userId
144
     * 
145
     * 
146
     * @return boolean
147
     */
148
    public function addItem(
149
        int $folderId,
150
        string $label,
151
        string $password,
152
        string $description,
153
        string $login,
154
        string $email,
155
        string $url,
156
        string $tags,
157
        string $anyone_can_modify,
158
        string $icon,
159
        int $userId,
160
        string $username
161
    ) : array
162
    {
163
        include_once API_ROOT_PATH . '/../sources/main.functions.php';
164
        $data = [
165
            'folderId' => $folderId,
166
            'label' => $label,
167
            'password' => $password,
168
            'description' => $description,
169
            'login' => $login,
170
            'email' => $email,
171
            'tags' => $tags,
172
            'anyoneCanModify' => $anyone_can_modify,
173
            'url' => $url,
174
            'icon' => $icon,
175
        ];
176
        
177
        $filters = [
178
            'folderId' => 'cast:integer',
179
            'label' => 'trim|escape',
180
            'password' => 'trim|escape',
181
            'description' => 'trim|escape',
182
            'login' => 'trim|escape',
183
            'email' => 'trim|escape',
184
            'tags' => 'trim|escape',
185
            'anyoneCanModify' => 'trim|escape',
186
            'url' => 'trim|escape',
187
            'icon' => 'trim|escape',
188
        ];
189
        
190
        $inputData = dataSanitizer(
191
            $data,
192
            $filters
193
        );
194
        extract($inputData);
0 ignored issues
show
Bug introduced by
It seems like $inputData can also be of type string; however, parameter $array of extract() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

194
        extract(/** @scrutinizer ignore-type */ $inputData);
Loading history...
195
196
        $lang = new Language();
197
        include API_ROOT_PATH . '/../includes/config/tp.config.php';
198
199
        // is pwd empty?
200
        if ($this->isPasswordEmptyAllowed($password, $SETTINGS['create_item_without_password'], $lang)) {
201
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('error' => ...ssword is not allowed') returns the type array<string,string|true> which is incompatible with the documented return type boolean.
Loading history...
202
                'error' => true,
203
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
204
                'error_message' => 'Empty password is not allowed'
205
            ];
206
        }
207
208
        // Check length
209
        if (strlen($password) > $SETTINGS['pwd_maximum_length']) {
210
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('error' => ...gth'] . ' characters)') returns the type array<string,string|true> which is incompatible with the documented return type boolean.
Loading history...
211
                'error' => true,
212
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
213
                'error_message' => 'Password is too long (max allowed is ' . $SETTINGS['pwd_maximum_length'] . ' characters)'
214
            ];
215
        }
216
217
        // Need info in DB
218
        // About special settings
219
        $dataFolderSettings = DB::queryFirstRow(
220
            'SELECT bloquer_creation, bloquer_modification, personal_folder
221
            FROM ' . prefixTable('nested_tree') . ' 
222
            WHERE id = %i',
223
            $inputData['folderId']
224
        );
225
        $itemInfos = [];
226
        $itemInfos['personal_folder'] = $dataFolderSettings['personal_folder'];
227
        $itemInfos['no_complex_check_on_modification'] = (int) $itemInfos['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_modification'];
228
        $itemInfos['no_complex_check_on_creation'] = (int) $itemInfos['personal_folder'] === 1 ? 1 : (int) $dataFolderSettings['bloquer_creation'];
229
230
        // Get folder complexity
231
        $folderComplexity = DB::queryfirstrow(
232
            'SELECT valeur
233
            FROM ' . prefixTable('misc') . '
234
            WHERE type = %s AND intitule = %i',
235
            'complex',
236
            $inputData['folderId']
237
        );
238
        $itemInfos['requested_folder_complexity'] = $folderComplexity !== null ? (int) $folderComplexity['valeur'] : 0;
239
240
        // Check COMPLEXITY
241
        $zxcvbn = new Zxcvbn();
242
        $passwordStrength = $zxcvbn->passwordStrength($password);
243
        $folderPasswordStrength = convertPasswordStrength($itemInfos['requested_folder_complexity']);
244
        if ($passwordStrength['score'] < $folderPasswordStrength && (int) $itemInfos['no_complex_check_on_creation'] === 0) {
245
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('error' => ...d strength is too low') returns the type array<string,string|true> which is incompatible with the documented return type boolean.
Loading history...
246
                'error' => true,
247
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
248
                'error_message' => 'Password strength is too low'
249
            ];
250
        }
251
252
        // check if element doesn't already exist
253
        DB::queryfirstrow(
254
            'SELECT * FROM ' . prefixTable('items') . '
255
            WHERE label = %s AND inactif = %i',
256
            $label,
257
            0
258
        );
259
        if (
260
            DB::count() > 0
261
            && ((isset($SETTINGS['duplicate_item']) === true && (int) $SETTINGS['duplicate_item'] === 0)
262
            || (int) $itemInfos['personal_folder'] === 0)
263
        ) {
264
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('error' => ...ates are not allowed.') returns the type array<string,string|true> which is incompatible with the documented return type boolean.
Loading history...
265
                'error' => true,
266
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
267
                'error_message' => 'Similar item already exists. Duplicates are not allowed.'
268
            ];
269
        }
270
271
        // Handle case where pw is empty
272
        // if not allowed then warn user
273
        if (
274
            isset($SETTINGS['create_item_without_password']) === true && (int) $SETTINGS['create_item_without_password'] === 0
275
            && empty($password) === true
276
        ) {
277
            return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('error' => ...sword is not allowed.') returns the type array<string,string|true> which is incompatible with the documented return type boolean.
Loading history...
278
                'error' => true,
279
                'error_header' => 'HTTP/1.1 422 Unprocessable Entity',
280
                'error_message' => 'Empty password is not allowed.'
281
            ];
282
        }
283
        if (empty($password) === false) {
284
            $cryptedStuff = doDataEncryption($password);
285
            $password = $cryptedStuff['encrypted'];
286
            $passwordKey = $cryptedStuff['objectKey'];
287
        } else {
288
            $passwordKey = '';
289
        }
290
        
291
        // ADD item
292
        DB::insert(
293
            prefixTable('items'),
294
            array(
295
                'label' => $label,
296
                'description' => $description,
297
                'pw' => $password,
298
                'pw_iv' => '',
299
                'email' => $email,
300
                'url' => $url,
301
                'id_tree' => $folderId,
302
                'login' => $login,
303
                'inactif' => 0,
304
                'restricted_to' => '',
305
                'perso' => $itemInfos['personal_folder'],
306
                'anyone_can_modify' => $anyoneCanModify,
307
                'complexity_level' => $passwordStrength['score'],
308
                'encryption_type' => 'teampass_aes',
309
                'fa_icon' => $icon,
310
                'item_key' => uniqidReal(50),
311
                'created_at' => time(),
312
            )
313
        );
314
        $newID = DB::insertId();
315
316
        // Create sharekeys for the user itself
317
        storeUsersShareKey(
318
            prefixTable('sharekeys_items'),
319
            (int) $itemInfos['personal_folder'],
320
            (int) $folderId,
321
            (int) $newID,
322
            $passwordKey,
323
            true,   // only for the item creator
324
            false,  // no delete all
325
            [],
326
            -1,
327
            $userId
328
        );
329
330
        // log
331
        logItems(
332
            $SETTINGS,
333
            (int) $newID,
334
            $label,
335
            $userId,
336
            'at_creation',
337
            $username
338
        );
339
340
        // Create new task for the new item
341
        // If it is not a personnal one
342
        if ((int) $itemInfos['personal_folder'] === 0) {
343
            storeTask(
344
                'new_item',
345
                $userId,
346
                0,
347
                (int) $folderId,
348
                (int) $newID,
349
                $passwordKey,
350
                [],
351
                [],
352
            );
353
        }
354
355
        // Add tags
356
        $tags = explode(',', $tags);
357
        foreach ($tags as $tag) {
358
            if (empty($tag) === false) {
359
                DB::insert(
360
                    prefixTable('tags'),
361
                    array(
362
                        'item_id' => $newID,
363
                        'tag' => strtolower($tag),
364
                    )
365
                );
366
            }
367
        }
368
369
        return [
0 ignored issues
show
Bug Best Practice introduced by
The expression return array('error' => ...ly', 'newId' => $newID) returns the type array<string,false|mixed|string> which is incompatible with the documented return type boolean.
Loading history...
370
            'error' => false,
371
            'message' => 'Item added successfully',
372
            'newId' => $newID,
373
        ];
374
    }
375
376
    private function isPasswordEmptyAllowed($password, $create_item_without_password, $lang)
0 ignored issues
show
Unused Code introduced by
The parameter $lang 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

376
    private function isPasswordEmptyAllowed($password, $create_item_without_password, /** @scrutinizer ignore-unused */ $lang)

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...
377
    {
378
        if (
379
            empty($password) === true
380
            && null !== $create_item_without_password
381
            && (int) $create_item_without_password !== 1
382
        ) {
383
            return true;
384
        }
385
        return false;
386
    }
387
}