Passed
Push — master ( ad9515...b9d9d6 )
by Nils
11:37
created

refreshCacheForUsersWithSimilarRoles()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 34
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

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