ItemController   B
last analyzed

Complexity

Total Complexity 46

Size/Duplication

Total Lines 329
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 183
dl 0
loc 329
rs 8.72
c 0
b 0
f 0
wmc 46

5 Methods

Rating   Name   Duplication   Size   Complexity  
C inFoldersAction() 0 88 10
C getAction() 0 69 13
A getUserPrivateKey() 0 21 4
B createAction() 0 71 7
C checkNewItemData() 0 35 12

How to fix   Complexity   

Complex Class

Complex classes like ItemController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use ItemController, and based on these observations, apply Extract Interface, too.

1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This library is distributed in the hope that it will be useful,
6
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
7
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
8
 * ---
9
 *
10
 * @project   Teampass
11
 * @version    API
12
 *
13
 * @file      ItemControler.php
14
 * ---
15
 *
16
 * @author    Nils Laumaillé ([email protected])
17
 *
18
 * @copyright 2009-2025 Teampass.net
19
 *
20
 * @license   https://spdx.org/licenses/GPL-3.0-only.html#licenseText GPL-3.0
21
 * ---
22
 *
23
 * @see       https://www.teampass.net
24
 */
25
26
use Symfony\Component\HttpFoundation\Request AS symfonyRequest;
27
28
class ItemController extends BaseController
29
{
30
31
    /**
32
     * Get user's private key from database
33
     *
34
     * Retrieves the user's private key by decrypting it from the database.
35
     * The private key is stored ENCRYPTED in the database and is decrypted
36
     * using the session_key from the JWT token.
37
     *
38
     * @param array $userData User data from JWT token (must include id, key_tempo, and session_key)
39
     * @return string|null Decrypted private key or null if not found/invalid session
40
     */
41
    private function getUserPrivateKey(array $userData): ?string
42
    {
43
        include_once API_ROOT_PATH . '/inc/jwt_utils.php';
44
45
        // Verify session_key exists in JWT payload
46
        if (!isset($userData['session_key']) || empty($userData['session_key'])) {
47
            error_log('getUserPrivateKey: Missing session_key in JWT token for user ID ' . $userData['id']);
48
            return null;
49
        }
50
51
        $userKeys = get_user_keys(
52
            (int) $userData['id'],
53
            (string) $userData['key_tempo'],
54
            (string) $userData['session_key'] // Session key from JWT for decryption
55
        );
56
57
        if ($userKeys === null) {
58
            return null;
59
        }
60
61
        return $userKeys['private_key'];
62
    }
63
64
65
    /**
66
     * Manage case inFolder - get items inside an array of folders
67
     *
68
     * @param array $userData
69
     */
70
    public function inFoldersAction(array $userData): void
71
    {
72
        $request = symfonyRequest::createFromGlobals();
73
        $requestMethod = $request->getMethod();
74
        $strErrorDesc = $responseData = $strErrorHeader = '';
75
76
        // get parameters
77
        $arrQueryStringParams = $this->getQueryStringParams();
78
79
        if (strtoupper($requestMethod) === 'GET') {
80
            // define WHERE clause
81
            $sqlExtra = '';
82
            if (empty($userData['folders_list']) === false) {
83
                $userData['folders_list'] = explode(',', $userData['folders_list']);
84
            } else {
85
                $userData['folders_list'] = [];
86
            }
87
88
            // SQL where clause with folders list
89
            if (isset($arrQueryStringParams['folders']) === true) {
90
                // convert the folders to an array
91
                $arrQueryStringParams['folders'] = explode(',', str_replace( array('[',']') , ''  , $arrQueryStringParams['folders']));
92
93
                // ensure to only use the intersection
94
                $foldersList = implode(',', array_intersect($arrQueryStringParams['folders'], $userData['folders_list']));
95
96
                // build sql where clause
97
                if (!empty($foldersList)) {
98
                    // build sql where clause
99
                    $sqlExtra = ' WHERE id_tree IN ('.$foldersList.')';
100
                } else {
101
                    // Send error
102
                    $this->sendOutput(
103
                        json_encode(['error' => 'Folders are mandatory']),
104
                        ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
105
                    );
106
                }
107
            } else {
108
                // Send error
109
                $this->sendOutput(
110
                    json_encode(['error' => 'Folders are mandatory']),
111
                    ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
112
                );
113
            }
114
115
            // SQL LIMIT
116
            $intLimit = 0;
117
            if (isset($arrQueryStringParams['limit']) === true) {
118
                $intLimit = $arrQueryStringParams['limit'];
119
            }
120
121
            // send query
122
            try {
123
                $itemModel = new ItemModel();
124
125
                // Get user's private key from database
126
                $userPrivateKey = $this->getUserPrivateKey($userData);
127
                if ($userPrivateKey === null) {
128
                    $strErrorDesc = 'Invalid session or user keys not found';
129
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
130
                } else {
131
                    $arrItems = $itemModel->getItems($sqlExtra, $intLimit, $userPrivateKey, $userData['id']);
132
                    if (!empty($arrItems)) {
133
                        $responseData = json_encode($arrItems);
134
                    } else {
135
                        $strErrorDesc = 'No content for this label';
136
                        $strErrorHeader = 'HTTP/1.1 204 No Content';
137
                    }
138
                }
139
            } catch (Error $e) {
140
                $strErrorDesc = $e->getMessage().'. Something went wrong! Please contact support.4';
141
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
142
            }
143
        } else {
144
            $strErrorDesc = 'Method not supported';
145
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
146
        }
147
148
        // send output
149
        if (empty($strErrorDesc) === true) {
150
            $this->sendOutput(
151
                $responseData,
152
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
153
            );
154
        } else {
155
            $this->sendOutput(
156
                json_encode(['error' => $strErrorDesc]), 
157
                ['Content-Type: application/json', $strErrorHeader]
158
            );
159
        }
160
    }
161
    //end InFoldersAction()
162
163
    private function checkNewItemData(array $arrQueryStringParams, array $userData): array
164
    {
165
        if (isset($arrQueryStringParams['label']) === true
166
            && isset($arrQueryStringParams['folder_id']) === true
167
            && isset($arrQueryStringParams['password']) === true
168
            && isset($arrQueryStringParams['login']) === true
169
            && isset($arrQueryStringParams['email']) === true
170
            && isset($arrQueryStringParams['url']) === true
171
            && isset($arrQueryStringParams['tags']) === true
172
            && isset($arrQueryStringParams['anyone_can_modify']) === true
173
        ) {
174
            //
175
            if (in_array($arrQueryStringParams['folder_id'], $userData['folders_list']) === false && $userData['user_can_create_root_folder'] === 0) {
176
                return [
177
                    'error' => true,
178
                    'strErrorDesc' => 'User is not allowed in this folder',
179
                    'strErrorHeader' => 'HTTP/1.1 401 Unauthorized',
180
                ];
181
            } else if (empty($arrQueryStringParams['label']) === true) {
182
                return [
183
                    'error' => true,
184
                    'strErrorDesc' => 'Label is mandatory',
185
                    'strErrorHeader' => 'HTTP/1.1 401 Expected parameters not provided',
186
                ];
187
            } else {
188
                return [
189
                    'error' => false,
190
                ];
191
            }
192
        }
193
194
        return [
195
            'error' => true,
196
            'strErrorDesc' => 'All fields have to be provided even if empty (refer to documentation).',
197
            'strErrorHeader' => 'HTTP/1.1 401 Expected parameters not provided',
198
        ];
199
    }
200
201
    /**
202
     * Manage case Add
203
     *
204
     * @param array $userData
205
     */
206
    public function createAction(array $userData)
207
    {
208
        $request = symfonyRequest::createFromGlobals();
209
        $requestMethod = $request->getMethod();
210
        $strErrorDesc = $strErrorHeader = $responseData = '';
211
212
        if (strtoupper($requestMethod) === 'POST') {
213
            // Is user allowed to create a folder
214
            // We check if allowed_to_create
215
            if ((int) $userData['allowed_to_create'] !== 1) {
216
                $strErrorDesc = 'User is not allowed to create an item';
217
                $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
218
            } else {
219
                if (empty($userData['folders_list']) === false) {
220
                    $userData['folders_list'] = explode(',', $userData['folders_list']);
221
                } else {
222
                    $userData['folders_list'] = [];
223
                }
224
225
                // get parameters
226
                $arrQueryStringParams = $this->getQueryStringParams();
227
228
                // Check that the parameters are indeed an array before using them
229
                if (is_array($arrQueryStringParams)) {
230
                    // check parameters
231
                    $arrCheck = $this->checkNewItemData($arrQueryStringParams, $userData);
232
233
                    if ($arrCheck['error'] === true) {
234
                        $strErrorDesc = $arrCheck['strErrorDesc'];
235
                        $strErrorHeader = $arrCheck['strErrorHeader'];
236
                    } else {
237
                        // launch
238
                        $itemModel = new ItemModel();
239
                        $ret = $itemModel->addItem(
240
                            (int) $arrQueryStringParams['folder_id'],
241
                            (string) $arrQueryStringParams['label'],
242
                            (string) $arrQueryStringParams['password'],
243
                            (string) $arrQueryStringParams['description'],
244
                            (string) $arrQueryStringParams['login'],
245
                            (string) $arrQueryStringParams['email'],
246
                            (string) $arrQueryStringParams['url'],
247
                            (string) $arrQueryStringParams['tags'],
248
                            (string) $arrQueryStringParams['anyone_can_modify'],
249
                            (string) $arrQueryStringParams['icon'],
250
                            (int) $userData['id'],
251
                            (string) $userData['username'],
252
                        );
253
                        $responseData = json_encode($ret);
254
                    }
255
                
256
                } else {
257
                    // Gérer le cas où les paramètres ne sont pas un tableau
258
                    $strErrorDesc = 'Data not consistent';
259
                    $strErrorHeader = 'Expected array, received ' . gettype($arrQueryStringParams);
260
                }
261
            }
262
        } else {
263
            $strErrorDesc = 'Method not supported';
264
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
265
        }
266
267
        // send output
268
        if (empty($strErrorDesc) === true) {
269
            $this->sendOutput(
270
                $responseData,
271
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
272
            );
273
        } else {
274
            $this->sendOutput(
275
                json_encode(['error' => $strErrorDesc]),
276
                ['Content-Type: application/json', $strErrorHeader]
277
            );
278
        }
279
    }
280
    //end addAction()
281
282
283
    /**
284
     * Manage case get - get an item
285
     *
286
     * @param array $userData
287
     */
288
    public function getAction(array $userData): void
289
    {
290
        
291
        $request = symfonyRequest::createFromGlobals();
292
        $requestMethod = $request->getMethod();
293
        $strErrorDesc = '';
294
        $sqlExtra = '';
295
        $responseData = '';
296
        $strErrorHeader = '';
297
        $sql_constraint = ' AND (i.id_tree IN ('.$userData['folders_list'].')';
298
        if (!empty($userData['restricted_items_list'])) {
299
            $sql_constraint .= 'OR i.id IN ('.$userData['restricted_items_list'].')';
300
        }
301
        $sql_constraint .= ')';
302
303
        // get parameters
304
        $arrQueryStringParams = $this->getQueryStringParams();
305
306
        if (strtoupper($requestMethod) === 'GET') {
307
            // SQL where clause with item id
308
            if (isset($arrQueryStringParams['id']) === true) {
309
                // build sql where clause by ID
310
                $sqlExtra = ' WHERE i.id = '.$arrQueryStringParams['id'] . $sql_constraint;
311
            } else if (isset($arrQueryStringParams['label']) === true) {
312
                // build sql where clause by LABEL
313
                $sqlExtra = ' WHERE i.label '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE '.$arrQueryStringParams['label'] : ' = '.$arrQueryStringParams['label']) . $sql_constraint;
314
            } else if (isset($arrQueryStringParams['description']) === true) {
315
                // build sql where clause by LABEL
316
                $sqlExtra = ' WHERE i.description '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE '.$arrQueryStringParams['description'] : ' = '.$arrQueryStringParams['description']).$sql_constraint;
317
            } else {
318
                // Send error
319
                $this->sendOutput(
320
                    json_encode(['error' => 'Item id, label or description is mandatory']),
321
                    ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
322
                );
323
            }
324
325
            // send query
326
            try {
327
                $itemModel = new ItemModel();
328
329
                // Get user's private key from database
330
                $userPrivateKey = $this->getUserPrivateKey($userData);
331
                if ($userPrivateKey === null) {
332
                    $strErrorDesc = 'Invalid session or user keys not found';
333
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
334
                } else {
335
                    $arrItems = $itemModel->getItems($sqlExtra, 0, $userPrivateKey, $userData['id']);
336
                    $responseData = json_encode($arrItems);
337
                }
338
            } catch (Error $e) {
339
                $strErrorDesc = $e->getMessage().'. Something went wrong! Please contact support.6';
340
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
341
            }
342
        } else {
343
            $strErrorDesc = 'Method not supported';
344
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
345
        }
346
347
        // send output
348
        if (empty($strErrorDesc) === true) {
349
            $this->sendOutput(
350
                $responseData,
351
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
352
            );
353
        } else {
354
            $this->sendOutput(
355
                json_encode(['error' => $strErrorDesc]), 
356
                ['Content-Type: application/json', $strErrorHeader]
357
            );
358
        }
359
    }
360
    //end getAction() 
361
}
362