Passed
Pull Request — master (#6774)
by
unknown
16:52 queued 08:54
created

Version20250918163700::getRoleIdByCodes()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 9
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 2
eloc 5
nc 2
nop 1
dl 0
loc 9
rs 10
c 1
b 0
f 0
1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Migrations\Schema\V200;
8
9
use Chamilo\CoreBundle\Migrations\AbstractMigrationChamilo;
10
use Doctrine\DBAL\Schema\Schema;
11
12
final class Version20250918163700 extends AbstractMigrationChamilo
13
{
14
    public function getDescription(): string
15
    {
16
        return "Remove SUPER_ADMIN role and grant 'user:loginas' to ADMIN. Migrate users' roles accordingly.";
17
    }
18
19
    public function up(Schema $schema): void
20
    {
21
        $conn = $this->connection;
22
        $conn->beginTransaction();
23
24
        try {
25
            $users = $conn->fetchAllAssociative("SELECT id, roles FROM `user`");
26
            $updateStmt = $conn->prepare("UPDATE `user` SET roles = :roles WHERE id = :id");
27
28
            foreach ($users as $u) {
29
                $id  = (int) $u['id'];
30
                $raw = $u['roles'];
31
32
                [$roles, $format] = $this->decodeRoles($raw);
33
                if (empty($roles)) {
34
                    continue;
35
                }
36
37
                $upper = array_values(array_unique(array_map(
38
                    fn ($r) => strtoupper(trim((string) $r)),
39
                    $roles
40
                )));
41
42
                $changed = false;
43
44
                if (in_array('ROLE_SUPER_ADMIN', $upper, true)) {
45
                    if (!in_array('ROLE_ADMIN', $upper, true)) {
46
                        $upper[] = 'ROLE_ADMIN';
47
                    }
48
                    $upper   = array_values(array_diff($upper, ['ROLE_SUPER_ADMIN']));
49
                    $changed = true;
50
                }
51
52
                if ($changed) {
53
                    $encoded = $this->encodeRoles($upper, $format);
54
                    $updateStmt->executeStatement(['roles' => $encoded, 'id' => $id]);
55
                }
56
            }
57
58
            $adminRoleId = $this->ensureAdminRoleId();
59
            $permId      = $this->ensurePermissionExists('user:loginas', 'Login as user', 'Login as another user');
60
            $this->ensurePermissionRelRole($permId, $adminRoleId);
61
62
            $superAdminId = $this->getRoleIdByCodes(['SUPER_ADMIN', 'SUA']);
63
            if ($superAdminId !== null) {
64
                $conn->executeStatement(
65
                    "DELETE FROM permission_rel_role WHERE role_id = :rid",
66
                    ['rid' => $superAdminId]
67
                );
68
                $conn->executeStatement("DELETE FROM role WHERE id = :rid", ['rid' => $superAdminId]);
69
            }
70
71
            $conn->commit();
72
        } catch (\Throwable $e) {
73
            $conn->rollBack();
74
            throw $e;
75
        }
76
    }
77
78
    public function down(Schema $schema): void
79
    {
80
        $conn = $this->connection;
81
        $conn->beginTransaction();
82
83
        try {
84
            $superAdminId = $this->ensureSuperAdminRoleId();
85
86
            $permId = $this->ensurePermissionExists('user:loginas', 'Login as user', 'Login as another user');
87
            $this->ensurePermissionRelRole($permId, $superAdminId);
88
89
            $adminRoleId = $this->getRoleIdByCodes(['ADMIN', 'ADM']);
90
            if ($adminRoleId !== null) {
91
                $conn->executeStatement(
92
                    "DELETE FROM permission_rel_role WHERE permission_id = :pid AND role_id = :rid",
93
                    ['pid' => $permId, 'rid' => $adminRoleId]
94
                );
95
            }
96
97
            $users = $conn->fetchAllAssociative("SELECT id, roles FROM `user`");
98
            $updateStmt = $conn->prepare("UPDATE `user` SET roles = :roles WHERE id = :id");
99
100
            foreach ($users as $u) {
101
                $id  = (int) $u['id'];
102
                $raw = $u['roles'];
103
104
                [$roles, $format] = $this->decodeRoles($raw);
105
                $upper = array_values(array_unique(array_map(
106
                    fn ($r) => strtoupper(trim((string) $r)),
107
                    $roles
108
                )));
109
110
                if (in_array('ROLE_ADMIN', $upper, true) && !in_array('ROLE_SUPER_ADMIN', $upper, true)) {
111
                    $upper[]  = 'ROLE_SUPER_ADMIN';
112
                    $encoded  = $this->encodeRoles($upper, $format);
113
                    $updateStmt->executeStatement(['roles' => $encoded, 'id' => $id]);
114
                }
115
            }
116
117
            $conn->commit();
118
        } catch (\Throwable $e) {
119
            $conn->rollBack();
120
            throw $e;
121
        }
122
    }
123
124
    private function decodeRoles($raw): array
125
    {
126
        if ($raw === null || $raw === '') {
127
            return [[], 'empty'];
128
        }
129
130
        if (is_array($raw)) {
131
            return [$raw, 'array'];
132
        }
133
134
        $s = (string) $raw;
135
136
        $json = json_decode($s, true);
137
        if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
138
            return [$json, 'json'];
139
        }
140
141
        $unser = @unserialize($s);
142
        if ($unser !== false && is_array($unser)) {
143
            return [$unser, 'php'];
144
        }
145
146
        $parts = preg_split('/[,\s]+/', $s, -1, PREG_SPLIT_NO_EMPTY);
147
        return [$parts ?: [], 'text'];
148
    }
149
150
    private function encodeRoles(array $roles, string $format): string
151
    {
152
        return match ($format) {
153
            'json', 'array', 'empty' => json_encode(array_values($roles), JSON_UNESCAPED_UNICODE),
154
            'php'                      => serialize(array_values($roles)),
155
            'text'                     => implode(',', array_values($roles)),
156
            default                    => json_encode(array_values($roles), JSON_UNESCAPED_UNICODE),
157
        };
158
    }
159
160
    private function getRoleIdByCodes(array $codes): ?int
161
    {
162
        $placeholders = implode(',', array_fill(0, count($codes), '?'));
163
        $row = $this->connection->fetchAssociative(
164
            "SELECT id FROM role WHERE code IN ($placeholders) LIMIT 1",
165
            $codes
166
        );
167
168
        return $row ? (int) $row['id'] : null;
169
    }
170
171
    private function ensureAdminRoleId(): int
172
    {
173
        $id = $this->getRoleIdByCodes(['ADMIN', 'ADM']);
174
        if ($id !== null) {
175
            return $id;
176
        }
177
        return $this->createRoleRow('ADMIN', 'Administrator', 'Platform administrator');
178
    }
179
180
    private function ensureSuperAdminRoleId(): int
181
    {
182
        $id = $this->getRoleIdByCodes(['SUPER_ADMIN', 'SUA']);
183
        if ($id !== null) {
184
            return $id;
185
        }
186
        return $this->createRoleRow('SUPER_ADMIN', 'Super Administrator', 'Full platform administrator');
187
    }
188
189
    private function createRoleRow(string $code, string $title, ?string $description = null): int
190
    {
191
        $conn = $this->connection;
192
        $sm   = $conn->createSchemaManager();
193
194
        $cols = [];
195
        foreach ($sm->listTableColumns('role') as $c) {
196
            $cols[strtoupper($c->getName())] = $c;
197
        }
198
199
        $now  = (new \DateTime())->format('Y-m-d H:i:s');
200
201
        $data = [
202
            'code'  => $code,
203
            'title' => $title,
204
        ];
205
206
        if ($description !== null && isset($cols['DESCRIPTION'])) {
207
            $data['description'] = $description;
208
        }
209
210
        if (isset($cols['CONSTANT_VALUE'])) {
211
            $type      = strtolower($cols['CONSTANT_VALUE']->getType()->getName());
212
            $isNumeric = in_array($type, ['integer', 'smallint', 'bigint', 'decimal', 'float', 'boolean'], true);
213
            $data['constant_value'] = $isNumeric ? 0 : ('ROLE_' . strtoupper($code));
214
        }
215
216
        if (isset($cols['SYSTEM_ROLE'])) {
217
            $data['system_role'] = 1;
218
        }
219
220
        if (isset($cols['CREATED_AT'])) {
221
            $data['created_at'] = $now;
222
        }
223
        if (isset($cols['UPDATED_AT'])) {
224
            $data['updated_at'] = $now;
225
        }
226
227
        $fields = array_keys($data);
228
        $placeholders = array_map(fn ($f) => ':' . $f, $fields);
229
        $sql = 'INSERT INTO role (' . implode(',', $fields) . ') VALUES (' . implode(',', $placeholders) . ')';
230
        $conn->executeStatement($sql, $data);
231
232
        return (int) $conn->lastInsertId();
233
    }
234
235
    private function ensurePermissionExists(string $slug, string $title, string $desc): int
236
    {
237
        $conn = $this->connection;
238
239
        $row = $conn->fetchAssociative("SELECT id FROM permission WHERE slug = :s", ['s' => $slug]);
240
        if ($row) {
241
            return (int) $row['id'];
242
        }
243
244
        $sm   = $conn->createSchemaManager();
245
        $cols = array_change_key_case(
246
            array_map(fn ($c) => $c->getName(), $sm->listTableColumns('permission')),
247
            CASE_UPPER
248
        );
249
250
        $now  = (new \DateTime())->format('Y-m-d H:i:s');
251
252
        $data = ['slug' => $slug, 'title' => $title, 'description' => $desc];
253
        if (isset($cols['CREATED_AT'])) {
254
            $data['created_at'] = $now;
255
        }
256
        if (isset($cols['UPDATED_AT'])) {
257
            $data['updated_at'] = $now;
258
        }
259
260
        $fields = array_keys($data);
261
        $place  = array_map(fn ($f) => ':' . $f, $fields);
262
        $sql    = 'INSERT INTO permission (' . implode(',', $fields) . ') VALUES (' . implode(',', $place) . ')';
263
        $conn->executeStatement($sql, $data);
264
265
        return (int) $conn->lastInsertId();
266
    }
267
268
    private function ensurePermissionRelRole(int $permissionId, int $roleId): void
269
    {
270
        $row = $this->connection->fetchAssociative(
271
            "SELECT 1 FROM permission_rel_role WHERE permission_id = :p AND role_id = :r",
272
            ['p' => $permissionId, 'r' => $roleId]
273
        );
274
        if (!$row) {
275
            $now = (new \DateTime())->format('Y-m-d H:i:s');
276
            $this->connection->executeStatement(
277
                "INSERT INTO permission_rel_role (permission_id, role_id, changeable, updated_at) VALUES (:p, :r, 1, :u)",
278
                ['p' => $permissionId, 'r' => $roleId, 'u' => $now]
279
            );
280
        }
281
    }
282
}
283