FolderManager   F
last analyzed

Complexity

Total Complexity 71

Size/Duplication

Total Lines 472
Duplicated Lines 0 %

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 211
dl 0
loc 472
c 3
b 0
f 0
rs 2.7199
wmc 71

20 Methods

Rating   Name   Duplication   Size   Complexity  
A loadSettings() 0 5 1
A __construct() 0 4 1
A canCreateFolder() 0 7 6
A checkComplexityLevel() 0 24 5
A checkDuplicateFolderAllowed() 0 19 4
A isParentFolderAllowed() 0 11 5
A addComplexity() 0 7 1
A setFolderCategories() 0 3 1
A isTitleNumeric() 0 3 1
B createNewFolder() 0 32 7
A getParentFolderData() 0 23 3
A updateTimestamp() 0 6 1
B manageFolderPermissions() 0 18 7
A errorResponse() 0 6 1
A updateUserFolderCache() 0 52 4
A copyCustomFieldsCategories() 0 7 2
C createFolder() 0 38 12
A insertFolder() 0 15 3
A refreshCacheForUsersWithSimilarRoles() 0 34 3
A rebuildFolderTree() 0 11 3

How to fix   Complexity   

Complex Class

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

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

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

1
<?php
2
3
declare(strict_types=1);
4
5
/**
6
 * Teampass - a collaborative passwords manager.
7
 * ---
8
 * This file is part of the TeamPass project.
9
 * 
10
 * TeamPass is free software: you can redistribute it and/or modify it
11
 * under the terms of the GNU General Public License as published by
12
 * the Free Software Foundation, version 3 of the License.
13
 * 
14
 * TeamPass is distributed in the hope that it will be useful,
15
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
16
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17
 * GNU General Public License for more details.
18
 * 
19
 * You should have received a copy of the GNU General Public License
20
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
21
 * 
22
 * Certain components of this file may be under different licenses. For
23
 * details, see the `licenses` directory or individual file headers.
24
 * ---
25
 * @file      folders.class.php
26
 * @author    Nils Laumaillé ([email protected])
27
 * @copyright 2009-2025 Teampass.net
28
 * @license   GPL-3.0
29
 * @see       https://www.teampass.net
30
 */
31
32
use TeampassClasses\NestedTree\NestedTree;
33
use TeampassClasses\SessionManager\SessionManager;
34
use TeampassClasses\ConfigManager\ConfigManager;
35
36
class FolderManager
37
{
38
    private $lang;
39
    private $settings;
40
41
    /**
42
     * Constructor
43
     */
44
    public function __construct($lang)
45
    {
46
        $this->lang = $lang;
47
        $this->loadSettings();
48
    }
49
50
    /**
51
     * Load settings
52
     */
53
    private function loadSettings()
54
    {
55
        // Load config if $SETTINGS not defined
56
        $configManager = new ConfigManager();
57
        $this->settings = $configManager->getAllSettings();
58
    }
59
60
    /**
61
     * Create a new folder
62
     *
63
     * @param array $params
64
     * @return array
65
     */
66
    public function createNewFolder(array $params, array $options = []): array
67
    {
68
            // Decompose parameters
69
            $title = $params['title'] ?? '';
70
            $parent_id = $params['parent_id'] ?? 0;
71
            $personal_folder = $params['personal_folder'] ?? 0;
72
            $complexity = $params['complexity'] ?? 0;
73
            $user_is_admin = $params['user_is_admin'] ?? 0;
74
            $user_accessible_folders = $params['user_accessible_folders'] ?? [];
75
            $user_can_create_root_folder = $params['user_can_create_root_folder'] ?? 0;
76
77
78
        if ($this->isTitleNumeric($title)) {
79
            return $this->errorResponse($this->lang->get('error_only_numbers_in_folder_name'));
80
        }
81
82
        if (!$this->isParentFolderAllowed($parent_id, $user_accessible_folders, $user_is_admin, $user_can_create_root_folder)) {
0 ignored issues
show
Bug introduced by
It seems like $user_is_admin can also be of type integer; however, parameter $user_is_admin of FolderManager::isParentFolderAllowed() does only seem to accept boolean, 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

82
        if (!$this->isParentFolderAllowed($parent_id, $user_accessible_folders, /** @scrutinizer ignore-type */ $user_is_admin, $user_can_create_root_folder)) {
Loading history...
Bug introduced by
It seems like $user_can_create_root_folder can also be of type integer; however, parameter $user_can_create_root_folder of FolderManager::isParentFolderAllowed() does only seem to accept boolean, 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

82
        if (!$this->isParentFolderAllowed($parent_id, $user_accessible_folders, $user_is_admin, /** @scrutinizer ignore-type */ $user_can_create_root_folder)) {
Loading history...
83
            return $this->errorResponse($this->lang->get('error_folder_not_allowed_for_this_user'));
84
        }
85
86
        if (!$this->checkDuplicateFolderAllowed($title) && $personal_folder == 0) {
87
            return $this->errorResponse($this->lang->get('error_group_exist'));
88
        }
89
90
        $parentFolderData = $this->getParentFolderData($parent_id);
91
92
        $parentComplexity = $this->checkComplexityLevel($parentFolderData, $complexity, $parent_id);
93
        if (isset($parentComplexity['error']) && $parentComplexity['error'] === true) {
94
            return $this->errorResponse($this->lang->get('error_folder_complexity_lower_than_top_folder') . " [<b>{$this->settings['TP_PW_COMPLEXITY'][$parentComplexity['valeur']][1]}</b>]");
95
        }
96
97
        return $this->createFolder($params, array_merge($parentFolderData, $parentComplexity), $options);
98
    }
99
100
    /**
101
     * Check if title is numeric
102
     *
103
     * @param string $title
104
     * @return boolean
105
     */
106
    private function isTitleNumeric($title)
107
    {
108
        return is_numeric($title);
109
    }
110
111
    /**
112
     * Check if parent folder is allowed
113
     *
114
     * @param integer $parent_id
115
     * @param array $user_accessible_folders
116
     * @param boolean $user_is_admin
117
     * @param boolean $user_can_create_root_folder
118
     * @return boolean
119
     */
120
    private function isParentFolderAllowed($parent_id, $user_accessible_folders, $user_is_admin, $user_can_create_root_folder)
121
    {
122
        if ($parent_id == 0 && $user_can_create_root_folder == true)
0 ignored issues
show
Coding Style Best Practice introduced by
It seems like you are loosely comparing two booleans. Considering using the strict comparison === instead.

When comparing two booleans, it is generally considered safer to use the strict comparison operator.

Loading history...
123
	    return true;
124
125
        if (in_array($parent_id, $user_accessible_folders) === false
126
            && (int) $user_is_admin !== 1
127
        ) {
128
            return false;
129
        }
130
        return true;
131
    }
132
133
    /**
134
     * Check if duplicate folder is allowed
135
     *
136
     * @param string $title
137
     * @return boolean
138
     */
139
    private function checkDuplicateFolderAllowed($title)
140
    {
141
        if (
142
            isset($this->settings['duplicate_folder']) === true
143
            && (int) $this->settings['duplicate_folder'] === 0
144
        ) {
145
            DB::query(
146
                'SELECT *
147
                FROM ' . prefixTable('nested_tree') . '
148
                WHERE title = %s AND personal_folder = 0',
149
                $title
150
            );
151
            $counter = DB::count();
152
            if ($counter !== 0) {
153
                return false;
154
            }
155
            return true;
156
        }
157
        return true;
158
    }
159
160
    /**
161
     * Get parent folder data
162
     *
163
     * @param integer $parent_id
164
     * @return array
165
     */
166
    private function getParentFolderData($parent_id)
167
    {
168
        //check if parent folder is personal
169
        $data = DB::queryFirstRow(
170
            'SELECT personal_folder, bloquer_creation, bloquer_modification
171
            FROM ' . prefixTable('nested_tree') . '
172
            WHERE id = %i',
173
            $parent_id
174
        );
175
176
        // inherit from parent the specific settings it has
177
        if (DB::count() > 0) {
178
            $parentBloquerCreation = $data['bloquer_creation'];
179
            $parentBloquerModification = $data['bloquer_modification'];
180
        } else {
181
            $parentBloquerCreation = 0;
182
            $parentBloquerModification = 0;
183
        }
184
185
        return [
186
            'isPersonal' => null !== $data['personal_folder'] ? $data['personal_folder'] : 0,
187
            'parentBloquerCreation' => $parentBloquerCreation,
188
            'parentBloquerModification' => $parentBloquerModification,
189
        ];
190
    }
191
192
    /**
193
     * Check complexity level
194
     *
195
     * @param array $data
196
     * @param integer $complexity
197
     * @param integer $parent_id
198
     * @return array|boolean
199
     */
200
    private function checkComplexityLevel(
201
        $data,
202
        $complexity,
203
        $parent_id
204
    )
205
    {
206
        if ((int) $data['isPersonal'] === 0) {    
207
            // get complexity level for this folder
208
            $data = DB::queryFirstRow(
209
                'SELECT valeur
210
                FROM ' . prefixTable('misc') . '
211
                WHERE intitule = %i AND type = %s',
212
                $parent_id,
213
                'complex'
214
            );
215
            if (isset($data['valeur']) === true && intval($complexity) < intval($data['valeur'])) {
216
                return [
217
                    'error' => true,
218
                ];
219
            }
220
        }
221
222
        return [
223
            'parent_complexity' => isset($data['valeur']) === true ? $data['valeur'] : 0,
224
        ];
225
    }
226
227
    /**
228
     * Creates a new folder in the system, applying necessary settings, permissions, 
229
     * cache updates, and user role management.
230
     *
231
     * @param array $params - Parameters for folder creation (e.g., title, icon, etc.)
232
     * @param array $parentFolderData - Parent folder data (e.g., ID, permissions)
233
     * @return array - Returns an array indicating success or failure
234
     */
235
    private function createFolder($params, $parentFolderData, $options)
236
    {
237
        // Decompose parameters
238
        $title = $params['title'] ?? '';
239
        $parent_id = $params['parent_id'] ?? 0;
240
        $isPersonal = $params['personal_folder'] ?? 0;
241
        $complexity = $params['complexity'] ?? 0;
242
        $access_rights = $params['access_rights'] ?? '';
243
        $user_is_admin = $params['user_is_admin'] ?? 0;
244
        $user_is_manager = $params['user_is_manager'] ?? 0;
245
        $user_can_create_root_folder = $params['user_can_create_root_folder'] ?? 0;
246
        $user_can_manage_all_users = $params['user_can_manage_all_users'] ?? 0;
247
        $user_id = $params['user_id'] ?? 0;
248
        $user_roles = $params['user_roles'] ?? '';    
249
250
        if ($this->canCreateFolder($isPersonal, $user_is_admin, $user_is_manager, $user_can_manage_all_users, $user_can_create_root_folder)) {
251
            $newId = $this->insertFolder($params, $parentFolderData);
252
            $this->addComplexity($newId, $complexity);
253
            if (isset($options['setFolderCategories']) && $options['setFolderCategories'] === true) {
254
                $this->setFolderCategories($newId);
255
            }
256
            $this->updateTimestamp();
257
            if (isset($options['rebuildFolderTree']) && $options['rebuildFolderTree'] === true) {
258
                $this->rebuildFolderTree($user_is_admin, $title, $parent_id, $isPersonal, $user_id, $newId);
259
            }
260
            if (isset($options['manageFolderPermissions']) && $options['manageFolderPermissions'] === true) {
261
                $this->manageFolderPermissions($parent_id, $newId, $user_roles, $access_rights, $user_is_admin);
262
            }
263
            if (isset($options['copyCustomFieldsCategories']) && $options['copyCustomFieldsCategories'] === true) {
264
                $this->copyCustomFieldsCategories($parent_id, $newId);
265
            }
266
            if (isset($options['refreshCacheForUsersWithSimilarRoles']) && $options['refreshCacheForUsersWithSimilarRoles'] === true) {
267
                $this->refreshCacheForUsersWithSimilarRoles($user_roles);
268
            }
269
270
            return ['error' => false, 'newId' => $newId];
271
        } else {
272
            return ['error' => true, 'newId' => null];
273
        }
274
    }
275
276
    /**
277
     * Checks if the user has the permissions to create a folder.
278
     */
279
    private function canCreateFolder($isPersonal, $user_is_admin, $user_is_manager, $user_can_manage_all_users, $user_can_create_root_folder)
280
    {
281
        return (int)$isPersonal === 1 ||
282
            (int)$user_is_admin === 1 ||
283
            ((int)$user_is_manager === 1 || (int)$user_can_manage_all_users === 1) ||
284
            ($this->settings['enable_user_can_create_folders'] ?? false) ||
285
            ((int)$user_can_create_root_folder === 1);
286
    }
287
288
    /**
289
     * Inserts a new folder into the database and returns the new folder ID.
290
     */
291
    private function insertFolder($params, $parentFolderData)
292
    {
293
        DB::insert(prefixTable('nested_tree'), [
294
            'parent_id' => $params['parent_id'],
295
            'title' => $params['title'],
296
            'personal_folder' => $params['personal_folder'] ?? 0,
297
            'renewal_period' => $params['duration'] ?? 0,
298
            'bloquer_creation' => $params['create_auth_without'] ?? $parentFolderData['parentBloquerCreation'],
299
            'bloquer_modification' => $params['edit_auth_without'] ?? $parentFolderData['parentBloquerModification'],
300
            'fa_icon' => empty($params['icon']) ? TP_DEFAULT_ICON : $params['icon'],
301
            'fa_icon_selected' => empty($params['icon_selected']) ? TP_DEFAULT_ICON_SELECTED : $params['icon_selected'],
302
            'categories' => '',
303
        ]);
304
305
        return DB::insertId();
306
    }
307
308
    /**
309
     * Adds complexity settings for the newly created folder.
310
     */
311
    private function addComplexity($folderId, $complexity)
312
    {
313
        DB::insert(prefixTable('misc'), [
314
            'type' => 'complex',
315
            'intitule' => $folderId,
316
            'valeur' => $complexity,
317
            'created_at' => time(),
318
        ]);
319
    }
320
321
    /**
322
     * Ensures that folder categories are set.
323
     */
324
    private function setFolderCategories($folderId)
325
    {
326
        handleFoldersCategories([$folderId]);
327
    }
328
329
    /**
330
     * Updates the last folder change timestamp.
331
     */
332
    private function updateTimestamp()
333
    {
334
        DB::update(prefixTable('misc'), [
335
            'valeur' => time(),
336
            'updated_at' => time(),
337
        ], 'type = %s AND intitule = %s', 'timestamp', 'last_folder_change');
338
    }
339
340
    /**
341
     * Rebuilds the folder tree and updates the cache for non-admin users.
342
     */
343
    private function rebuildFolderTree($user_is_admin, $title, $parent_id, $isPersonal, $user_id, $newId)
344
    {
345
        $tree = new NestedTree(prefixTable('nested_tree'), 'id', 'parent_id', 'title');
346
        $tree->rebuild();
347
        
348
        // Update session visible flolders
349
        $sess_key = $isPersonal ? 'user-personal_folders' : 'user-accessible_folders';
350
        SessionManager::addRemoveFromSessionArray($sess_key, [$newId], 'add');
351
352
        if ($user_is_admin === 0) {
353
            $this->updateUserFolderCache($tree, $title, $parent_id, $isPersonal, $user_id, $newId);
354
        }
355
    }
356
357
    /**
358
     * Updates the user folder cache for non-admin users.
359
     */
360
    private function updateUserFolderCache($tree, $title, $parent_id, $isPersonal, $user_id, $newId)
361
    {
362
        $path = '';
363
        $tree_path = $tree->getPath(0, false);
364
        foreach ($tree_path as $fld) {
365
            $path .= empty($path) ? $fld->title : '/' . $fld->title;
366
        }
367
368
        $new_json = [
369
            "path" => $path,
370
            "id" => $newId,
371
            "level" => count($tree_path),
372
            "title" => $title,
373
            "disabled" => 0,
374
            "parent_id" => $parent_id,
375
            "perso" => $isPersonal,
376
            "is_visible_active" => 0,
377
        ];
378
379
        $cache_tree = DB::queryFirstRow(
380
            'SELECT increment_id, folders, visible_folders 
381
            FROM ' . prefixTable('cache_tree') . ' 
382
            WHERE user_id = %i', 
383
            (int) $user_id
384
        );
385
386
        if (empty($cache_tree)) {
387
            DB::insert(
388
                prefixTable('cache_tree'), 
389
                [
390
                    'user_id' => $user_id,
391
                    'folders' => json_encode([$newId]),
392
                    'visible_folders' => json_encode($new_json),
393
                    'timestamp' => time(),
394
                    'data' => '[{}]',
395
                ]
396
            );
397
        } else {
398
            $folders = json_decode($cache_tree['folders'] ?? '[]', true);
399
            $visible_folders = json_decode($cache_tree['visible_folders'] ?? '[]', true);
400
            $folders[] = $newId;
401
            $visible_folders[] = $new_json;
402
403
            DB::update(
404
                prefixTable('cache_tree'), 
405
                [
406
                    'folders' => json_encode($folders),
407
                    'visible_folders' => json_encode($visible_folders),
408
                    'timestamp' => time(),
409
                ],
410
                'increment_id = %i', 
411
                (int) $cache_tree['increment_id']
412
            );
413
        }
414
    }
415
416
    /**
417
     * Manages folder permissions based on user roles or parent folder settings.
418
     */
419
    private function manageFolderPermissions($parent_id, $newId, $user_roles, $access_rights, $user_is_admin)
420
    {
421
        if ($parent_id !== 0 && $this->settings['subfolder_rights_as_parent'] ?? false) {
422
            $rows = DB::query('SELECT role_id, type FROM ' . prefixTable('roles_values') . ' WHERE folder_id = %i', $parent_id);
423
            foreach ($rows as $record) {
424
                DB::insert(prefixTable('roles_values'), [
425
                    'role_id' => $record['role_id'],
426
                    'folder_id' => $newId,
427
                    'type' => $record['type'],
428
                ]);
429
            }
430
        } elseif ((int)$user_is_admin !== 1) {
431
            foreach (array_unique(explode(';', $user_roles)) as $role) {
432
                if (!empty($role)) {
433
                    DB::insert(prefixTable('roles_values'), [
434
                        'role_id' => $role,
435
                        'folder_id' => $newId,
436
                        'type' => $access_rights,
437
                    ]);
438
                }
439
            }
440
        }
441
    }
442
443
    /**
444
     * Copies custom field categories from the parent folder to the newly created folder.
445
     */
446
    private function copyCustomFieldsCategories($parent_id, $newId)
447
    {
448
        $rows = DB::query('SELECT id_category FROM ' . prefixTable('categories_folders') . ' WHERE id_folder = %i', $parent_id);
449
        foreach ($rows as $record) {
450
            DB::insert(prefixTable('categories_folders'), [
451
                'id_category' => $record['id_category'],
452
                'id_folder' => $newId,
453
            ]);
454
        }
455
    }
456
457
    /**
458
     * Refresh the cache for users with similar roles to the current user.
459
     */
460
    private function refreshCacheForUsersWithSimilarRoles($user_roles)
461
    {
462
        $usersWithSimilarRoles = getUsersWithRoles(explode(";", $user_roles));
463
        foreach ($usersWithSimilarRoles as $user) {
464
465
            // Arguments field
466
            $arguments = json_encode([
467
                'user_id' => (int) $user,
468
            ], JSON_HEX_QUOT | JSON_HEX_TAG);
469
470
            // Search for existing job
471
            $count = DB::queryFirstRow(
472
                'SELECT COUNT(*) AS count
473
                FROM ' . prefixTable('background_tasks') . '
474
                WHERE is_in_progress = %i AND process_type = %s AND arguments = %s',
475
                0,
476
                'user_build_cache_tree',
477
                $arguments
478
            )['count'];
479
480
            // Don't insert duplicates
481
            if ($count > 0)
482
                continue;
483
484
            // Insert new background task
485
            DB::insert(
486
                prefixTable('background_tasks'),
487
                array(
488
                    'created_at' => time(),
489
                    'process_type' => 'user_build_cache_tree',
490
                    'arguments' => $arguments,
491
                    'updated_at' => null,
492
                    'finished_at' => null,
493
                    'output' => null,
494
                )
495
            );
496
        }
497
    }
498
499
    /**
500
     * Returns an error response.
501
     */
502
    private function errorResponse($message, $newIdSuffix = "")
503
    {
504
        return [
505
            'error' => true,
506
            'message' => $message,
507
            'newId' => '' . $newIdSuffix,
508
        ];
509
    }
510
}
511