MessageVoter   B
last analyzed

Complexity

Total Complexity 52

Size/Duplication

Total Lines 247
Duplicated Lines 0 %

Test Coverage

Coverage 11.82%

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 113
c 1
b 0
f 0
dl 0
loc 247
ccs 13
cts 110
cp 0.1182
rs 7.44
wmc 52

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 2 1
A canSetPriority() 0 2 1
A canCreate() 0 2 1
A voteOnAttribute() 0 14 1
A canRemove() 0 2 1
B canView() 0 32 9
A isMemberOfTypeAndStudyGroup() 0 12 4
A canUpload() 0 8 3
A supports() 0 13 3
A canDismiss() 0 17 5
A canEdit() 0 18 4
A checkUserType() 0 10 5
A isMemberOfStudyGroups() 0 13 5
A getUserTypes() 0 2 1
A canVote() 0 11 3
A canConfirm() 0 11 3
A canDownload() 0 7 2

How to fix   Complexity   

Complex Class

Complex classes like MessageVoter often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use MessageVoter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace App\Security\Voter;
4
5
use App\Entity\Message;
6
use App\Entity\Student;
7
use App\Entity\StudyGroup;
8
use App\Entity\StudyGroupMembership;
9
use App\Entity\User;
10
use App\Entity\UserType;
11
use App\Entity\UserTypeEntity;
12
use App\Message\MessageConfirmationHelper;
13
use App\Utils\ArrayUtils;
14
use Doctrine\Common\Collections\Collection;
15
use LogicException;
16
use SchulIT\CommonBundle\Helper\DateHelper;
17
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
18
use Symfony\Component\Security\Core\Authorization\AccessDecisionManagerInterface;
19
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
20
21
class MessageVoter extends Voter {
22
23
    public const New = 'new-message';
24
    public const View = 'view';
25
    public const Edit = 'edit';
26
    public const Remove = 'remove';
27
    public const Confirm = 'confirm';
28
    public const Dismiss = 'dismiss';
29
    public const Download = 'download';
30
    public const Upload = 'upload';
31
    public const Priority = 'message-priority';
32
    public const Poll = 'poll';
33
34
    public function __construct(private AccessDecisionManagerInterface $accessDecisionManager, private MessageConfirmationHelper $confirmationHelper, private DateHelper $dateHelper)
35 10
    {
36 10
    }
37 10
38 10
    /**
39
     * @inheritDoc
40
     */
41
    protected function supports($attribute, $subject): bool {
42
        $attributes = [
43 10
            self::View,
44
            self::Edit,
45 10
            self::Remove,
46 10
            self::Confirm,
47 10
            self::Dismiss,
48 10
            self::Download,
49 10
            self::Upload,
50 10
            self::Poll
51 10
        ];
52
53
        return in_array($attribute, [ self::New, self::Priority]) || (in_array($attribute, $attributes) && $subject instanceof Message);
54 10
    }
55
56
    /**
57
     * @inheritDoc
58
     */
59
    protected function voteOnAttribute($attribute, $subject, TokenInterface $token): bool
60
    {
61
        return match ($attribute) {
62
            self::New => $this->canCreate($token),
63
            self::View => $this->canView($subject, $token),
64
            self::Edit => $this->canEdit($subject, $token),
65
            self::Remove => $this->canRemove($subject, $token),
66
            self::Confirm => $this->canConfirm($subject, $token),
67
            self::Dismiss => $this->canDismiss($subject, $token),
68
            self::Download => $this->canDownload($subject, $token),
69
            self::Upload => $this->canUpload($subject, $token),
70
            self::Priority => $this->canSetPriority($token),
71
            self::Poll => $this->canVote($subject, $token),
72
            default => throw new LogicException('This code should not be reached.'),
73
        };
74
    }
75
76
    private function canCreate(TokenInterface $token): bool {
77
        return $this->accessDecisionManager->decide($token, ['ROLE_MESSAGE_CREATOR']);
78
    }
79
80
    private function canView(Message $message, TokenInterface $token): bool {
81
        $user = $token->getUser();
82
83
        if(!$user instanceof User) {
84
            return false;
85
        }
86
87
        // Admins see all messages
88
        if($this->accessDecisionManager->decide($token, ['ROLE_MESSAGE_ADMIN']) || $this->accessDecisionManager->decide($token, ['ROLE_KIOSK'])) {
89
            return true;
90
        }
91
92
        // Teachers can see all messages
93
        if($user->isTeacher()) {
94
            return true;
95
        }
96
97
        // You can see your own messages
98
        if($message->getCreatedBy() !== null && $message->getCreatedBy()->getId() === $user->getId()) {
99
            return true;
100
        }
101
102
        if($this->isMemberOfTypeAndStudyGroup($token, $this->getUserTypes($message->getVisibilities()), $message->getStudyGroups()->toArray(), false) === true) {
103
            return true;
104
        }
105
106
        if($user->isStudentOrParent() === false) {
107
            // all checks passed for non-student/-parent users
108
            return true;
109
        }
110
111
        return false;
112
    }
113
114
    private function canEdit(Message $message, TokenInterface $token): bool {
115
        if($this->accessDecisionManager->decide($token, ['ROLE_MESSAGE_CREATOR']) !== true) {
116
            return false;
117
        }
118
119
        if($this->accessDecisionManager->decide($token, ['ROLE_MESSAGE_ADMIN'])) {
120
            // Admins can edit all messages
121
            return true;
122
        }
123
124
        // Creators can only edit their messages
125
        $user = $token->getUser();
126
127
        if(!$user instanceof User) {
128
            return false;
129
        }
130
131
        return $message->getCreatedBy()->getId() === $user->getId();
132
    }
133
134
    private function canRemove(Message $message, TokenInterface $token): bool {
135
        return $this->canEdit($message, $token);
136
    }
137
138
    private function canConfirm(Message $message, TokenInterface $token): bool {
139
        if($this->accessDecisionManager->decide($token, [ 'ROLE_KIOSK' ])) {
140
            return false;
141
        }
142
143
        return $message->mustConfirm()
144
            && $this->isMemberOfTypeAndStudyGroup(
145
                $token,
146
                $this->getUserTypes($message->getConfirmationRequiredUserTypes()),
147
                $message->getConfirmationRequiredStudyGroups()->toArray(),
148
                true
149
            );
150
    }
151
152
    private function canVote(Message $message, TokenInterface $token): bool {
153
        if($this->accessDecisionManager->decide($token, [ 'ROLE_KIOSK' ])) {
154
            return false;
155
        }
156
157
        return $message->isPollEnabled()
158
            && $this->isMemberOfTypeAndStudyGroup(
159
                $token,
160
                $this->getUserTypes($message->getPollUserTypes()),
161
                $message->getPollStudyGroups()->toArray(),
162
                true
163
            );
164
    }
165
166
    private function canDismiss(Message $message, TokenInterface $token): bool {
167
        if($this->accessDecisionManager->decide($token, [ 'ROLE_KIOSK' ])) {
168
            return false;
169
        }
170
171
        if($message->mustConfirm() === false || $this->canConfirm($message, $token) === false) {
172
            return true;
173
        }
174
175
        // only allow dismissing message in case the user has confirmed the message!
176
        $user = $token->getUser();
177
178
        if(!$user instanceof User) {
179
            return false;
180
        }
181
182
        return $this->confirmationHelper->isMessageConfirmed($message, $user);
183
    }
184
185
    /**
186
     * @param Collection<UserTypeEntity> $collection
187
     * @return UserType[]
188
     */
189
    private function getUserTypes(Collection $collection): array {
190
        return array_map(fn(UserTypeEntity $userTypeEntity) => $userTypeEntity->getUserType(), $collection->toArray());
191
    }
192
193
    /**
194
     * @param UserType[] $allowedUserTypes
195
     */
196
    private function checkUserType(array $allowedUserTypes, UserType $userType, bool $strict = true): bool {
197
        if(ArrayUtils::inArray($userType, $allowedUserTypes)) {
198
            return true;
199
        }
200
201
        if($strict === false && $userType === UserType::Parent && ArrayUtils::inArray(UserType::Student, $allowedUserTypes))  {
202
            return true;
203
        }
204
205
        return false;
206
    }
207
208
    /**
209
     * @param StudyGroup[] $studyGroups
210
     * @param Student[] $students
211
     */
212
    private function isMemberOfStudyGroups(array $studyGroups, array $students): bool {
213
        foreach($students as $student) {
214
            foreach($studyGroups as $studyGroup) {
215
                /** @var StudyGroupMembership $membership */
216
                foreach($studyGroup->getMemberships() as $membership) {
217
                    if($membership->getStudent()->getId() === $student->getId()) {
218
                        return true;
219
                    }
220
                }
221
            }
222
        }
223
224
        return false;
225
    }
226
227
    /**
228
     * @param UserType[] $userTypes
229
     * @param StudyGroup[] $studyGroups
230
     */
231
    private function isMemberOfTypeAndStudyGroup(TokenInterface $token, array $userTypes, array $studyGroups, bool $strict = true): bool {
232
        $user = $token->getUser();
233
234
        if(!$user instanceof User) {
235
            return false;
236
        }
237
238
        if($user->isStudentOrParent() && $this->isMemberOfStudyGroups($studyGroups, $user->getStudents()->toArray()) !== true) {
239
            return false;
240
        }
241
242
        return $this->checkUserType($userTypes, $user->getUserType(), $strict);
243
    }
244
245
    private function canDownload(Message $message, TokenInterface $token): bool {
246
        return $message->isDownloadsEnabled()
247
            && $this->isMemberOfTypeAndStudyGroup(
248
                $token,
249
                $this->getUserTypes($message->getDownloadEnabledUserTypes()),
250
                $message->getDownloadEnabledStudyGroups()->toArray(),
251
                true
252
            );
253
    }
254
255
    private function canUpload(Message $message, TokenInterface $token): bool {
256
        return $message->isUploadsEnabled()
257
            && $this->dateHelper->getToday() <= $message->getExpireDate()
258
            && $this->isMemberOfTypeAndStudyGroup(
259
                $token,
260
                $this->getUserTypes($message->getUploadEnabledUserTypes()),
261
                $message->getUploadEnabledStudyGroups()->toArray(),
262
                true
263
            );
264
    }
265
266
    private function canSetPriority(TokenInterface $token): bool {
267
        return $this->accessDecisionManager->decide($token, ['ROLE_MESSAGE_PRIORITY']);
268
    }
269
}