ItemController::getAction()   F
last analyzed

Complexity

Conditions 15
Paths 532

Size

Total Lines 73
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

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

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