Passed
Pull Request — master (#6799)
by
unknown
08:08
created

Version20250923214700::normalizeRoles()   A

Complexity

Conditions 5
Paths 3

Size

Total Lines 32
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 21
c 1
b 0
f 0
nc 3
nop 1
dl 0
loc 32
rs 9.2728
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 Version20250923214700 extends AbstractMigrationChamilo
13
{
14
    public function getDescription(): string
15
    {
16
        return "Normalize user.roles to canonical ROLE_* and serialize consistently";
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
                // Decode from any legacy format to array
33
                [$roles] = $this->decodeRoles($raw);
34
35
                // Normalize to canonical ROLE_* and ordering
36
                $normalized = $this->normalizeRoles($roles);
37
38
                // Re-encode ALWAYS as PHP serialized array (Doctrine array type compatibility)
39
                $encoded = serialize(array_values($normalized));
40
41
                // Update only if different to avoid unnecessary writes
42
                if ((string) $encoded !== (string) $raw) {
43
                    $updateStmt->executeStatement(['roles' => $encoded, 'id' => $id]);
44
                }
45
            }
46
47
            $conn->commit();
48
        } catch (\Throwable $e) {
49
            $conn->rollBack();
50
            throw $e;
51
        }
52
    }
53
54
    public function down(Schema $schema): void {}
55
56
    /**
57
     * Decode roles from various legacy formats into an array.
58
     * Supported: json, php-serialized, comma/space-separated text, array, empty.
59
     * @return array{0: array}
60
     */
61
    private function decodeRoles($raw): array
62
    {
63
        if ($raw === null || $raw === '') {
64
            return [[]];
65
        }
66
        if (is_array($raw)) {
67
            return [$raw];
68
        }
69
70
        $s = (string) $raw;
71
72
        // Try JSON
73
        $json = json_decode($s, true);
74
        if (json_last_error() === JSON_ERROR_NONE && is_array($json)) {
75
            return [$json];
76
        }
77
78
        // Try PHP serialized
79
        $unser = @unserialize($s);
80
        if ($unser !== false && is_array($unser)) {
81
            return [$unser];
82
        }
83
84
        // Fallback: split plain text by commas/spaces
85
        $parts = preg_split('/[,\s]+/', $s, -1, PREG_SPLIT_NO_EMPTY);
86
        return [$parts ?: []];
87
    }
88
89
    /**
90
     * Normalize role codes to canonical ROLE_* form.
91
     * - Uppercase
92
     * - Add ROLE_ prefix when missing
93
     * - Unify legacy aliases (e.g., SUPER_ADMIN → ROLE_GLOBAL_ADMIN)
94
     * - Deduplicate
95
     * - Sort by meaningful priority
96
     */
97
    private function normalizeRoles(array $roles): array
98
    {
99
        $out = [];
100
        foreach ($roles as $r) {
101
            $c = $this->normalizeRoleCode((string) $r);
102
            if ($c !== '' && !in_array($c, $out, true)) {
103
                $out[] = $c;
104
            }
105
        }
106
107
        // Sort by priority (stable intent)
108
        $prio = array_flip([
109
            'ROLE_GLOBAL_ADMIN',
110
            'ROLE_ADMIN',
111
            'ROLE_SESSION_MANAGER',
112
            'ROLE_HR',
113
            'ROLE_TEACHER',
114
            'ROLE_STUDENT_BOSS',
115
            'ROLE_INVITEE',
116
            'ROLE_STUDENT',
117
        ]);
118
119
        usort($out, function ($a, $b) use ($prio) {
120
            $pa = $prio[$a] ?? PHP_INT_MAX;
121
            $pb = $prio[$b] ?? PHP_INT_MAX;
122
            if ($pa === $pb) {
123
                return strcmp($a, $b);
124
            }
125
            return $pa <=> $pb;
126
        });
127
128
        return $out;
129
    }
130
131
    private function normalizeRoleCode(string $code): string
132
    {
133
        $c = strtoupper(trim($code));
134
        static $map = [
135
            'STUDENT'          => 'ROLE_STUDENT',
136
            'TEACHER'          => 'ROLE_TEACHER',
137
            'HR'               => 'ROLE_HR',
138
            'SESSION_MANAGER'  => 'ROLE_SESSION_MANAGER',
139
            'STUDENT_BOSS'     => 'ROLE_STUDENT_BOSS',
140
            'INVITEE'          => 'ROLE_INVITEE',
141
            'QUESTION_MANAGER' => 'ROLE_QUESTION_MANAGER',
142
            'ADMIN'            => 'ROLE_ADMIN',
143
            'GLOBAL_ADMIN'     => 'ROLE_GLOBAL_ADMIN',
144
            'SUPER_ADMIN'      => 'ROLE_GLOBAL_ADMIN',
145
            'ROLE_SUPER_ADMIN' => 'ROLE_GLOBAL_ADMIN',
146
        ];
147
148
        if (!str_starts_with($c, 'ROLE_')) {
149
            return $map[$c] ?? ('ROLE_' . $c);
150
        }
151
        return $map[$c] ?? $c;
152
    }
153
}
154