Completed
Push — master ( 021d11...186200 )
by Nils
28s queued 17s
created

FolderManager::canCreateFolder()   A

Complexity

Conditions 6
Paths 7

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

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