Passed
Push — master ( 2b951a...bd06e7 )
by Nils
06:45
created

ItemController::createAction()   B

Complexity

Conditions 7
Paths 16

Size

Total Lines 72
Code Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 7
eloc 49
c 0
b 0
f 0
nc 16
nop 1
dl 0
loc 72
rs 8.1793

How to fix   Long Method   

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

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