Passed
Push — main ( 4cfd43...9ceccf )
by Nobufumi
09:41
created

UserModel::cannotDeleteAdmin()   A

Complexity

Conditions 3
Paths 3

Size

Total Lines 19
Code Lines 11

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 3
eloc 11
c 0
b 0
f 0
nc 3
nop 1
dl 0
loc 19
rs 9.9
1
<?php
2
3
namespace Jidaikobo\Kontiki\Models;
4
5
class UserModel extends BaseModel
6
{
7
    use Traits\CRUDTrait;
8
    use Traits\MetaDataTrait;
0 ignored issues
show
introduced by
The trait Jidaikobo\Kontiki\Models\Traits\MetaDataTrait requires some properties which are not provided by Jidaikobo\Kontiki\Models\UserModel: $meta_key, $meta_value
Loading history...
9
    use Traits\IndexTrait;
10
11
    protected string $table = 'users';
12
13
    protected function defineFieldDefinitions(): void
14
    {
15
        // add dynamic rules at $this->processFieldDefinitions()
16
        $this->fieldDefinitions = [
17
            'id' => $this->getIdField(),
18
19
            'username' => $this->getField(
20
                __('username'),
21
                [
22
                    'rules' => [
23
                        'required',
24
                        ['lengthMin', 3]
25
                    ],
26
                    'display_in_list' => true
27
                ]
28
            ),
29
30
            'password' => $this->getField(
31
                __('password'),
32
                [
33
                    'type' => 'password',
34
                    'rules' => [
35
                        'required',
36
                        ['lengthMin', 8]
37
                    ],
38
                    'filter' => FILTER_UNSAFE_RAW,
39
                ]
40
            ),
41
42
            'role' => $this->getField(
43
                __('role'),
44
                [
45
                    'type' => 'select',
46
                    'options' => [
47
                        'editor' => __('editor'),
48
                        'admin' => __('admin'),
49
                    ],
50
                    'rules' => [
51
                        'required',
52
                    ],
53
                    'attributes' => [
54
                        'class' => 'form-control form-select'
55
                    ],
56
                    'display_in_list' => true
57
                ]
58
            ),
59
60
            'created_at' => $this->getReadOnlyField(
61
                __('created_at', 'Created'),
62
                [
63
                    'display_in_list' => true
64
                ]
65
            ),
66
        ];
67
    }
68
69
    protected function processFieldDefinitions(
70
        string $context = '',
71
        array $data = [],
72
        int $id = null
73
    ): void {
74
        // add rule
75
        $this->fieldDefinitions['username']['rules'][] = [
76
            'unique',
77
            $this->table,
78
            'username',
79
            $id
80
        ];
81
82
        if ($context == 'create') {
83
            return;
84
        }
85
86
        // Exclude `required` from password's rules
87
        // No password specified means no change
88
        $this->fieldDefinitions['password']['rules'] = array_filter(
89
            $this->fieldDefinitions['password']['rules'],
90
            fn($rule) => $rule !== 'required'
91
        );
92
93
        $this->fieldDefinitions['password']['description'] = __('users_edit_message');
94
95
        // disable form elements
96
        if (in_array($context, ['trash', 'restore', 'delete'])) {
97
            $this->disableFormFieldsForContext();
98
        }
99
    }
100
101
    public function validate(array $data, array $context): array
102
    {
103
        $result = parent::validate($data, $context);
104
        $adminCheck = $this->atLeastOneAdmin($data, $context);
105
        $deleteCheck = $this->cannotDeleteAdmin($context);
106
107
        return [
108
            'valid' => $result['valid'] && $adminCheck['valid'] && $deleteCheck['valid'],
109
            'errors' => array_merge_recursive(
110
                $result['errors'],
111
                $adminCheck['errors'],
112
                $deleteCheck['errors']
113
            )
114
        ];
115
    }
116
117
    private function atLeastOneAdmin(array $data, array $context): array
118
    {
119
        $result = [
120
            'valid' => true,
121
            'errors' => []
122
        ];
123
124
        $id = $context['id'] ?? 0;
125
        if (!$id || !isset($data['role'])) {
126
            return $result;
127
        }
128
129
        // Retrieve the target user's data from the database
130
        $targetUser = $this->getById($id);
131
132
        // If the user is an "admin" and is attempting to change their role
133
        if ($targetUser && $targetUser['role'] === 'admin' && $data['role'] !== 'admin') {
134
            // Count the number of other admins in the system
135
            $adminCount = $this->db->table($this->table)
136
                ->where('role', 'admin')
137
                ->where('id', '!=', $targetUser['id']) // Exclude the current user
138
                ->count();
139
140
            // If no other admins remain, return a validation error
141
            if ($adminCount === 0) {
142
                $result['valid'] = false;
143
                $result['errors']['role']['messages'] = [__('at_least_one_admin')];
144
            }
145
        }
146
147
        return $result;
148
    }
149
150
    private function cannotDeleteAdmin(array $context): array
151
    {
152
        $result = [
153
            'valid' => true,
154
            'errors' => []
155
        ];
156
157
        if (($context['context'] ?? '') !== 'delete') {
158
            return $result;
159
        }
160
161
        $id = $context['id'] ?? 0;
162
        $targetUser = $this->getById($id);
163
164
        if ($targetUser['role'] !== 'admin') return $result;
165
        $result['valid'] = false;
166
        $result['errors']['role']['messages'] = [__('cannot_delete_admin')];
167
168
        return $result;
169
    }
170
171
    private function hashPassword(string $password): string
172
    {
173
        return password_hash($password, PASSWORD_BCRYPT);
0 ignored issues
show
Bug Best Practice introduced by
The expression return password_hash($pa...Models\PASSWORD_BCRYPT) could return the type null which is incompatible with the type-hinted return string. Consider adding an additional type-check to rule them out.
Loading history...
174
    }
175
176
    protected function processDataForForm(string $actionType, array $data): array
177
    {
178
        if ($actionType == 'edit') {
179
            $data['password'] = '';
180
        }
181
        return $data;
182
    }
183
184
    protected function afterProcessDataBeforeSave(string $context, array $data): array
185
    {
186
        if ($context == 'create') {
187
            $data['password'] = $this->hashPassword($data['password']);
188
        }
189
190
        if ($context == 'update') {
191
            // Branching password processing
192
            if (isset($data['password'])) {
193
                if (trim($data['password']) === '') {
194
                    unset($data['password']);
195
                } else {
196
                    $data['password'] = $this->hashPassword($data['password']);
197
                }
198
            }
199
        }
200
        return $data;
201
    }
202
}
203