ItemController::getAction()   F
last analyzed

Complexity

Conditions 15
Paths 532

Size

Total Lines 71
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 15
eloc 47
nc 532
nop 1
dl 0
loc 71
rs 2.4
c 0
b 0
f 0

How to fix   Long Method    Complexity   

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:

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
                            (int) $arrQueryStringParams['anyone_can_modify'] ?? 0,
249
                            (string) $arrQueryStringParams['icon'] ?? '',
250
                            (int) $userData['id'],
251
                            (string) $userData['username'],
252
                            (string) $userData['totp'],
253
                        );
254
                        $responseData = json_encode($ret);
255
                    }
256
                
257
                } else {
258
                    // Gérer le cas où les paramètres ne sont pas un tableau
259
                    $strErrorDesc = 'Data not consistent';
260
                    $strErrorHeader = 'Expected array, received ' . gettype($arrQueryStringParams);
261
                }
262
            }
263
        } else {
264
            $strErrorDesc = 'Method not supported';
265
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
266
        }
267
268
        // send output
269
        if (empty($strErrorDesc) === true) {
270
            $this->sendOutput(
271
                $responseData,
272
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
273
            );
274
        } else {
275
            $this->sendOutput(
276
                json_encode(['error' => $strErrorDesc]),
277
                ['Content-Type: application/json', $strErrorHeader]
278
            );
279
        }
280
    }
281
    //end addAction()
282
283
284
    /**
285
     * Manage case get - get an item
286
     *
287
     * @param array $userData
288
     */
289
    public function getAction(array $userData): void
290
    {
291
        
292
        $request = symfonyRequest::createFromGlobals();
293
        $requestMethod = $request->getMethod();
294
        $strErrorDesc = '';
295
        $sqlExtra = '';
296
        $sqlLimit = 0;
297
        $responseData = '';
298
        $strErrorHeader = '';
299
        $sql_constraint = ' AND (i.id_tree IN ('.$userData['folders_list'].')';
300
        if (!empty($userData['restricted_items_list'])) {
301
            $sql_constraint .= 'OR i.id IN ('.$userData['restricted_items_list'].')';
302
        }
303
        $sql_constraint .= ')';
304
305
        // get parameters
306
        $arrQueryStringParams = $this->getQueryStringParams();
307
308
        if (strtoupper($requestMethod) === 'GET') {
309
            // SQL where clause with item id
310
            if (isset($arrQueryStringParams['id']) === true) {
311
                // build sql where clause by ID
312
                $sqlExtra = ' WHERE i.id = '.$arrQueryStringParams['id'] . $sql_constraint;
313
            } else if (isset($arrQueryStringParams['label']) === true) {
314
                // build sql where clause by LABEL
315
                $sqlExtra = ' WHERE i.label '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE "%'.$arrQueryStringParams['label'].'%"' : ' = '.$arrQueryStringParams['label']) . $sql_constraint;
316
                $sqlLimit = isset($arrQueryStringParams['limit']) === true && (int) $arrQueryStringParams['limit'] > 0 ? $arrQueryStringParams['limit'] : 50;   // let's limit to 50 by default
317
            } else if (isset($arrQueryStringParams['description']) === true) {
318
                // build sql where clause by DESCRIPTION
319
                $sqlExtra = ' WHERE i.description '.(isset($arrQueryStringParams['like']) === true && (int) $arrQueryStringParams['like'] === 1 ? ' LIKE '.$arrQueryStringParams['description'] : ' = '.$arrQueryStringParams['description']).$sql_constraint;
320
            } else {
321
                // Send error
322
                $this->sendOutput(
323
                    json_encode(['error' => 'Item id, label or description is mandatory']),
324
                    ['Content-Type: application/json', 'HTTP/1.1 401 Expected parameters not provided']
325
                );
326
            }
327
328
            // send query
329
            try {
330
                $itemModel = new ItemModel();
331
332
                // Get user's private key from database
333
                $userPrivateKey = $this->getUserPrivateKey($userData);
334
                if ($userPrivateKey === null) {
335
                    $strErrorDesc = 'Invalid session or user keys not found';
336
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
337
                } else {
338
                    $arrItems = $itemModel->getItems($sqlExtra, $sqlLimit, $userPrivateKey, $userData['id']);
339
                    $responseData = json_encode($arrItems);
340
                }
341
            } catch (Error $e) {
342
                $strErrorDesc = $e->getMessage().'. Something went wrong! Please contact support.6';
343
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
344
            }
345
        } else {
346
            $strErrorDesc = 'Method not supported';
347
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
348
        }
349
350
        // send output
351
        if (empty($strErrorDesc) === true) {
352
            $this->sendOutput(
353
                $responseData,
354
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
355
            );
356
        } else {
357
            $this->sendOutput(
358
                json_encode(['error' => $strErrorDesc]), 
359
                ['Content-Type: application/json', $strErrorHeader]
360
            );
361
        }
362
    }
363
    //end getAction()
364
365
    /**
366
     * Find items by URL
367
     * Searches for items matching a specific URL
368
     *
369
     * @param array $userData User data from JWT token
370
     * @return void
371
     */
372
    public function findByUrlAction(array $userData): void
373
    {
374
        $request = symfonyRequest::createFromGlobals();
375
        $requestMethod = $request->getMethod();
376
        $strErrorDesc = $responseData = $strErrorHeader = '';
377
378
        // Get parameters
379
        $arrQueryStringParams = $this->getQueryStringParams();
380
381
        if (strtoupper($requestMethod) === 'GET') {
382
            // Check if URL parameter is provided
383
            if (isset($arrQueryStringParams['url']) === false || empty($arrQueryStringParams['url']) === true) {
384
                $this->sendOutput(
385
                    json_encode(['error' => 'URL parameter is mandatory']),
386
                    ['Content-Type: application/json', 'HTTP/1.1 400 Bad Request']
387
                );
388
                return;
389
            }
390
391
            // Prepare user's accessible folders
392
            /*if (empty($userData['folders_list']) === false) {
393
                $userData['folders_list'] = explode(',', $userData['folders_list']);
394
            } else {
395
                $userData['folders_list'] = [];
396
            }*/
397
398
            // Build SQL constraint for accessible folders
399
            $sql_constraint = ' AND (i.id_tree IN (' . $userData['folders_list'] . ')';
400
            if (!empty($userData['restricted_items_list'])) {
401
                $sql_constraint .= ' OR i.id IN (' . $userData['restricted_items_list'] . ')';
402
            }
403
            $sql_constraint .= ')';
404
405
            // Decode URL if needed
406
            $searchUrl = urldecode($arrQueryStringParams['url']);
407
408
            try {
409
                // Get user's private key from database
410
                $userPrivateKey = $this->getUserPrivateKey($userData);
411
                if ($userPrivateKey === null) {
412
                    $strErrorDesc = 'Invalid session or user keys not found';
413
                    $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
414
                } else {
415
                    // Query items with the specific URL
416
                    $rows = DB::query(
417
                        "SELECT i.id, i.label, i.login, i.url, i.id_tree, 
418
                                CASE WHEN o.enabled = 1 THEN 1 ELSE 0 END AS has_otp
419
                        FROM " . prefixTable('items') . " AS i
420
                        LEFT JOIN " . prefixTable('items_otp') . " AS o ON (o.item_id = i.id)
421
                        WHERE i.url LIKE %s" . $sql_constraint . "
422
                            AND i.deleted_at IS NULL
423
                        ORDER BY i.label ASC",
424
                        "%".$searchUrl."%"
425
                    );
426
427
                    $ret = [];
428
                    foreach ($rows as $row) {
429
                        // Get user's sharekey for this item
430
                        $shareKey = DB::queryfirstrow(
0 ignored issues
show
Unused Code introduced by
The assignment to $shareKey is dead and can be removed.
Loading history...
431
                            'SELECT share_key
432
                            FROM ' . prefixTable('sharekeys_items') . '
433
                            WHERE user_id = %i AND object_id = %i',
434
                            $userData['id'],
435
                            $row['id']
436
                        );
437
438
                        // Skip if no sharekey found (user doesn't have access)
439
                        if (DB::count() === 0) {
440
                            continue;
441
                        }
442
443
                        // Build response
444
                        array_push(
445
                            $ret,
446
                            [
447
                                'id' => (int) $row['id'],
448
                                'label' => $row['label'],
449
                                'login' => $row['login'],
450
                                'url' => $row['url'],
451
                                'folder_id' => (int) $row['id_tree'],
452
                                'has_otp' => (int) $row['has_otp'],
453
                            ]
454
                        );
455
                    }
456
457
                    if (!empty($ret)) {
458
                        $responseData = json_encode($ret);
459
                    } else {
460
                        $strErrorDesc = 'No items found with this URL';
461
                        $strErrorHeader = 'HTTP/1.1 204 No Content';
462
                    }
463
                }
464
            } catch (Error $e) {
465
                $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
466
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
467
            }
468
        } else {
469
            $strErrorDesc = 'Method not supported';
470
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
471
        }
472
473
        // Send output
474
        if (empty($strErrorDesc) === true) {
475
            $this->sendOutput(
476
                $responseData,
477
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
478
            );
479
        } else {
480
            $this->sendOutput(
481
                json_encode(['error' => $strErrorDesc]),
482
                ['Content-Type: application/json', $strErrorHeader]
483
            );
484
        }
485
    }
486
    //end findByUrlAction()
487
488
489
    /**
490
     * Get OTP/TOTP code for an item
491
     * Retrieves the current 6-digit TOTP code for an item with OTP enabled
492
     *
493
     * @param array $userData User data from JWT token
494
     * @return void
495
     */
496
    public function getOtpAction(array $userData): void
497
    {
498
        $request = symfonyRequest::createFromGlobals();
499
        $requestMethod = $request->getMethod();
500
        $strErrorDesc = '';
501
        $responseData = '';
502
        $strErrorHeader = '';
503
504
        // get parameters
505
        $arrQueryStringParams = $this->getQueryStringParams();
506
507
        if (strtoupper($requestMethod) === 'GET') {
508
            // Check if item ID is provided
509
            if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
510
                $this->sendOutput(
511
                    json_encode(['error' => 'Item id is mandatory']),
512
                    ['Content-Type: application/json', 'HTTP/1.1 400 Bad Request']
513
                );
514
                return;
515
            }
516
517
            $itemId = (int) $arrQueryStringParams['id'];
518
519
            try {
520
                // Load config
521
                loadClasses('DB');
522
523
                // Load item basic info to check folder access
524
                $itemInfo = DB::queryFirstRow(
525
                    'SELECT id_tree FROM ' . prefixTable('items') . ' WHERE id = %i',
526
                    $itemId
527
                );
528
529
                if (DB::count() === 0) {
530
                    $strErrorDesc = 'Item not found';
531
                    $strErrorHeader = 'HTTP/1.1 404 Not Found';
532
                } else {
533
                    // Check if user has access to the folder
534
                    $userFolders = !empty($userData['folders_list']) ? explode(',', $userData['folders_list']) : [];
535
                    $hasAccess = in_array((string) $itemInfo['id_tree'], $userFolders, true);
536
537
                    // Also check restricted items if applicable
538
                    if (!$hasAccess && !empty($userData['restricted_items_list'])) {
539
                        $restrictedItems = explode(',', $userData['restricted_items_list']);
540
                        $hasAccess = in_array((string) $itemId, $restrictedItems, true);
541
                    }
542
543
                    if (!$hasAccess) {
544
                        $strErrorDesc = 'Access denied to this item';
545
                        $strErrorHeader = 'HTTP/1.1 403 Forbidden';
546
                    } else {
547
                        // Load OTP data
548
                        $otpData = DB::queryFirstRow(
549
                            'SELECT secret, enabled FROM ' . prefixTable('items_otp') . ' WHERE item_id = %i',
550
                            $itemId
551
                        );
552
553
                        if (DB::count() === 0) {
554
                            $strErrorDesc = 'OTP not configured for this item';
555
                            $strErrorHeader = 'HTTP/1.1 404 Not Found';
556
                        } elseif ((int) $otpData['enabled'] !== 1) {
557
                            $strErrorDesc = 'OTP is not enabled for this item';
558
                            $strErrorHeader = 'HTTP/1.1 403 Forbidden';
559
                        } else {
560
                            // Decrypt the secret
561
                            $decryptedSecret = cryption(
562
                                $otpData['secret'],
563
                                '',
564
                                'decrypt'
565
                            );
566
567
                            if (isset($decryptedSecret['string']) && !empty($decryptedSecret['string'])) {
568
                                // Generate OTP code using OTPHP library
569
                                try {
570
                                    $otp = \OTPHP\TOTP::createFromSecret($decryptedSecret['string']);
571
                                    $otpCode = $otp->now();
572
                                    $otpExpiresIn = $otp->expiresIn();
573
574
                                    $responseData = json_encode([
575
                                        'otp_code' => $otpCode,
576
                                        'expires_in' => $otpExpiresIn,
577
                                        'item_id' => $itemId
578
                                    ]);
579
                                } catch (\RuntimeException $e) {
580
                                    $strErrorDesc = 'Failed to generate OTP code: ' . $e->getMessage();
581
                                    $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
582
                                }
583
                            } else {
584
                                $strErrorDesc = 'Failed to decrypt OTP secret';
585
                                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
586
                            }
587
                        }
588
                    }
589
                }
590
            } catch (\Error $e) {
591
                $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
592
                $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
593
            }
594
        } else {
595
            $strErrorDesc = 'Method not supported';
596
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
597
        }
598
599
        // send output
600
        if (empty($strErrorDesc) === true) {
601
            $this->sendOutput(
602
                $responseData,
603
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
604
            );
605
        } else {
606
            $this->sendOutput(
607
                json_encode(['error' => $strErrorDesc]),
608
                ['Content-Type: application/json', $strErrorHeader]
609
            );
610
        }
611
    }
612
    //end getOtpAction()
613
614
615
    /**
616
     * Update an existing item
617
     * Updates an item based upon provided parameters and item ID
618
     *
619
     * @param array $userData User data from JWT token
620
     * @return void
621
     */
622
    public function updateAction(array $userData): void
623
    {
624
        $request = symfonyRequest::createFromGlobals();
625
        $requestMethod = $request->getMethod();
626
        $strErrorDesc = $strErrorHeader = $responseData = '';
627
628
        if (strtoupper($requestMethod) === 'PUT' || strtoupper($requestMethod) === 'POST') {
629
            // Check if user is allowed to update items
630
            if ((int) $userData['allowed_to_update'] !== 1) {
631
                $strErrorDesc = 'User is not allowed to update items';
632
                $strErrorHeader = 'HTTP/1.1 403 Forbidden';
633
            } else {
634
                // Prepare user's accessible folders
635
                if (empty($userData['folders_list']) === false) {
636
                    $userData['folders_list'] = explode(',', $userData['folders_list']);
637
                } else {
638
                    $userData['folders_list'] = [];
639
                }
640
641
                // Get parameters
642
                $arrQueryStringParams = $this->getQueryStringParams();
643
644
                // Check that the parameters are indeed an array before using them
645
                if (is_array($arrQueryStringParams)) {
646
                    // Check if item ID is provided
647
                    if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
648
                        $strErrorDesc = 'Item ID is mandatory';
649
                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
650
                    } else {
651
                        $itemId = (int) $arrQueryStringParams['id'];
652
653
                        try {
654
                            // Load item info to check access rights
655
                            $itemInfo = DB::queryFirstRow(
656
                                'SELECT id, id_tree, label FROM ' . prefixTable('items') . ' WHERE id = %i',
657
                                $itemId
658
                            );
659
660
                            if (DB::count() === 0) {
661
                                $strErrorDesc = 'Item not found';
662
                                $strErrorHeader = 'HTTP/1.1 404 Not Found';
663
                            } else {
664
                                // Check if user has access to the folder
665
                                $hasAccess = in_array((string) $itemInfo['id_tree'], $userData['folders_list'], true);
666
667
                                // Also check restricted items if applicable
668
                                if (!$hasAccess && !empty($userData['restricted_items_list'])) {
669
                                    $restrictedItems = explode(',', $userData['restricted_items_list']);
670
                                    $hasAccess = in_array((string) $itemId, $restrictedItems, true);
671
                                }
672
673
                                if (!$hasAccess) {
674
                                    $strErrorDesc = 'Access denied to this item';
675
                                    $strErrorHeader = 'HTTP/1.1 403 Forbidden';
676
                                } else {
677
                                    // Validate at least one field to update is provided
678
                                    $updateableFields = ['label', 'password', 'description', 'login', 'email', 'url', 'tags', 'anyone_can_modify', 'icon', 'folder_id', 'totp'];
679
                                    $hasUpdateField = false;
680
                                    foreach ($updateableFields as $field) {
681
                                        if (isset($arrQueryStringParams[$field])) {
682
                                            $hasUpdateField = true;
683
                                            break;
684
                                        }
685
                                    }
686
687
                                    if (!$hasUpdateField) {
688
                                        $strErrorDesc = 'At least one field to update must be provided (label, password, description, login, email, url, tags, anyone_can_modify, icon, folder_id, totp)';
689
                                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
690
                                    } else {
691
                                        // Get user's private key for password encryption/decryption
692
                                        $userPrivateKey = $this->getUserPrivateKey($userData);
693
                                        if ($userPrivateKey === null) {
694
                                            $strErrorDesc = 'Invalid session or user keys not found';
695
                                            $strErrorHeader = 'HTTP/1.1 401 Unauthorized';
696
                                        } else {
697
                                            // Update the item
698
                                            $itemModel = new ItemModel();
699
                                            $ret = $itemModel->updateItem(
700
                                                $itemId,
701
                                                $arrQueryStringParams,
0 ignored issues
show
Bug introduced by
It seems like $arrQueryStringParams can also be of type string; however, parameter $params of ItemModel::updateItem() 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

701
                                                /** @scrutinizer ignore-type */ $arrQueryStringParams,
Loading history...
702
                                                $userData,
703
                                                $userPrivateKey
704
                                            );
705
706
                                            if ($ret['error'] === true) {
707
                                                $strErrorDesc = $ret['error_message'];
708
                                                $strErrorHeader = $ret['error_header'];
709
                                            } else {
710
                                                $responseData = json_encode($ret);
711
                                            }
712
                                        }
713
                                    }
714
                                }
715
                            }
716
                        } catch (Error $e) {
717
                            $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
718
                            $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
719
                        }
720
                    }
721
                } else {
722
                    $strErrorDesc = 'Data not consistent';
723
                    $strErrorHeader = 'HTTP/1.1 400 Bad Request - Expected array, received ' . gettype($arrQueryStringParams);
724
                }
725
            }
726
        } else {
727
            $strErrorDesc = 'Method not supported';
728
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
729
        }
730
731
        // send output
732
        if (empty($strErrorDesc) === true) {
733
            $this->sendOutput(
734
                $responseData,
735
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
736
            );
737
        } else {
738
            $this->sendOutput(
739
                json_encode(['error' => $strErrorDesc]),
740
                ['Content-Type: application/json', $strErrorHeader]
741
            );
742
        }
743
    }
744
    //end updateAction()
745
746
747
    /**
748
     * Delete an existing item
749
     * Updates an item based upon provided parameters and item ID
750
     *
751
     * @param array $userData User data from JWT token
752
     * @return void
753
     */
754
    public function deleteAction(array $userData): void
755
    {
756
        $request = symfonyRequest::createFromGlobals();
757
        $requestMethod = $request->getMethod();
758
        $strErrorDesc = $strErrorHeader = $responseData = '';
759
760
        if (strtoupper($requestMethod) === 'DELETE') {
761
            // Check if user is allowed to delete items
762
            if ((int) $userData['allowed_to_delete'] !== 1) {
763
                $strErrorDesc = 'User is not allowed to delete items';
764
                $strErrorHeader = 'HTTP/1.1 403 Forbidden';
765
            } else {
766
                // Get parameters
767
                $arrQueryStringParams = $this->getQueryStringParams();
768
                
769
                // Check that the parameters are indeed an array before using them
770
                if (is_array($arrQueryStringParams)) {
771
                    // Check if item ID is provided
772
                    if (!isset($arrQueryStringParams['id']) || empty($arrQueryStringParams['id'])) {
773
                        $strErrorDesc = 'Item ID is mandatory';
774
                        $strErrorHeader = 'HTTP/1.1 400 Bad Request';
775
                    } else {
776
                        $itemId = (int) $arrQueryStringParams['id'];
777
                        
778
                        try {
779
                            // Load item info to check access rights
780
                            $itemInfo = DB::queryFirstRow(
781
                                'SELECT id, id_tree, label FROM ' . prefixTable('items') . ' WHERE id = %i',
782
                                $itemId
783
                            );
784
785
                            if (DB::count() === 0) {
786
                                $strErrorDesc = 'Item not found';
787
                                $strErrorHeader = 'HTTP/1.1 404 Not Found';
788
                            } else {
789
                                // Check if user has access to the folder
790
                                $userFolders = explode(',', $userData['folders_list']) ?? [];
791
                                $hasAccess = in_array((string) $itemInfo['id_tree'], $userFolders, true);
792
793
                                // Also check restricted items if applicable
794
                                if (!$hasAccess && !empty($userData['restricted_items_list'])) {
795
                                    $restrictedItems = explode(',', $userData['restricted_items_list']) ?? [];
796
                                    $hasAccess = in_array((string) $itemId, $restrictedItems, true);
797
                                }
798
799
                                if (!$hasAccess) {
800
                                    $strErrorDesc = 'Access denied to this item';
801
                                    $strErrorHeader = 'HTTP/1.1 403 Forbidden';
802
                                } else {
803
                                    // delete the item
804
                                    $itemModel = new ItemModel();
805
                                    $ret = $itemModel->deleteItem(
806
                                        $itemId,
807
                                        $userData
808
                                    );
809
810
                                    if ($ret['error'] === true) {
811
                                        $strErrorDesc = $ret['error_message'];
812
                                        $strErrorHeader = $ret['error_header'];
813
                                    } else {
814
                                        $responseData = json_encode($ret);
815
                                    }
816
                                }
817
                            }
818
                        } catch (Error $e) {
819
                            $strErrorDesc = $e->getMessage() . '. Something went wrong! Please contact support.';
820
                            $strErrorHeader = 'HTTP/1.1 500 Internal Server Error';
821
                        }
822
                    }
823
                } else {
824
                    $strErrorDesc = 'Data not consistent';
825
                    $strErrorHeader = 'HTTP/1.1 400 Bad Request - Expected array, received ' . gettype($arrQueryStringParams);
826
                }
827
            }
828
        } else {
829
            $strErrorDesc = 'Method not supported';
830
            $strErrorHeader = 'HTTP/1.1 422 Unprocessable Entity';
831
        }
832
833
        // send output
834
        if (empty($strErrorDesc) === true) {
835
            $this->sendOutput(
836
                $responseData,
837
                ['Content-Type: application/json', 'HTTP/1.1 200 OK']
838
            );
839
        } else {
840
            $this->sendOutput(
841
                json_encode(['error' => $strErrorDesc]),
842
                ['Content-Type: application/json', $strErrorHeader]
843
            );
844
        }
845
    }
846
    //end deleteAction()
847
848
849
}
850