Passed
Push — master ( b9d9d6...11abf5 )
by Nils
08:06 queued 01:32
created

FolderManager   F

Complexity

Total Complexity 72

Size/Duplication

Total Lines 443
Duplicated Lines 0 %

Importance

Changes 7
Bugs 0 Features 0
Metric Value
eloc 191
c 7
b 0
f 0
dl 0
loc 443
rs 2.64
wmc 72

20 Methods

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

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