Passed
Pull Request — master (#4334)
by
unknown
05:33
created

FolderManager::getParentFolderData()   A

Complexity

Conditions 3
Paths 4

Size

Total Lines 23
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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