Passed
Push — master ( e9e739...1e7f19 )
by Yannick
09:01
created

ResourceNodeVoter   F

Complexity

Total Complexity 109

Size/Duplication

Total Lines 509
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
eloc 259
dl 0
loc 509
rs 2
c 0
b 0
f 0
wmc 109

6 Methods

Rating   Name   Duplication   Size   Complexity  
A getEditorMask() 0 8 1
A getReaderMask() 0 7 1
A supports() 0 17 2
A __construct() 0 6 1
F voteOnAttribute() 0 429 97
B isBlogResource() 0 19 7

How to fix   Complexity   

Complex Class

Complex classes like ResourceNodeVoter 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 ResourceNodeVoter, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
declare(strict_types=1);
4
5
/* For licensing terms, see /license.txt */
6
7
namespace Chamilo\CoreBundle\Security\Authorization\Voter;
8
9
use Chamilo\CoreBundle\Entity\Course;
10
use Chamilo\CoreBundle\Entity\ResourceLink;
11
use Chamilo\CoreBundle\Entity\ResourceNode;
12
use Chamilo\CoreBundle\Entity\ResourceRight;
13
use Chamilo\CoreBundle\Entity\Session;
14
use Chamilo\CoreBundle\Settings\SettingsManager;
15
use Chamilo\CourseBundle\Entity\CDocument;
16
use Chamilo\CourseBundle\Entity\CGroup;
17
use Chamilo\CourseBundle\Entity\CQuizQuestion;
18
use Chamilo\CourseBundle\Entity\CQuizRelQuestion;
19
use Chamilo\CourseBundle\Entity\CStudentPublicationRelDocument;
20
use ChamiloSession;
21
use Doctrine\ORM\EntityManagerInterface;
22
use Laminas\Permissions\Acl\Acl;
23
use Laminas\Permissions\Acl\Resource\GenericResource;
24
use Laminas\Permissions\Acl\Role\GenericRole;
25
use Symfony\Bundle\SecurityBundle\Security;
26
use Symfony\Component\HttpFoundation\RequestStack;
27
use Symfony\Component\Security\Acl\Permission\MaskBuilder;
28
use Symfony\Component\Security\Core\Authentication\Token\NullToken;
29
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
30
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
31
use Symfony\Component\Security\Core\User\UserInterface;
32
33
/**
34
 * @extends Voter<'CREATE'|'VIEW'|'EDIT'|'DELETE'|'EXPORT', ResourceNode>
35
 */
36
class ResourceNodeVoter extends Voter
37
{
38
    public const VIEW = 'VIEW';
39
    public const CREATE = 'CREATE';
40
    public const EDIT = 'EDIT';
41
    public const DELETE = 'DELETE';
42
    public const EXPORT = 'EXPORT';
43
    public const ROLE_CURRENT_COURSE_TEACHER = 'ROLE_CURRENT_COURSE_TEACHER';
44
    public const ROLE_CURRENT_COURSE_STUDENT = 'ROLE_CURRENT_COURSE_STUDENT';
45
    public const ROLE_CURRENT_COURSE_GROUP_TEACHER = 'ROLE_CURRENT_COURSE_GROUP_TEACHER';
46
    public const ROLE_CURRENT_COURSE_GROUP_STUDENT = 'ROLE_CURRENT_COURSE_GROUP_STUDENT';
47
    public const ROLE_CURRENT_COURSE_SESSION_TEACHER = 'ROLE_CURRENT_COURSE_SESSION_TEACHER';
48
    public const ROLE_CURRENT_COURSE_SESSION_STUDENT = 'ROLE_CURRENT_COURSE_SESSION_STUDENT';
49
50
    public function __construct(
51
        private Security $security,
52
        private RequestStack $requestStack,
53
        private SettingsManager $settingsManager,
54
        private EntityManagerInterface $entityManager
55
    ) {}
56
57
    public static function getReaderMask(): int
58
    {
59
        $builder = (new MaskBuilder())
60
            ->add(self::VIEW)
61
        ;
62
63
        return $builder->get();
64
    }
65
66
    public static function getEditorMask(): int
67
    {
68
        $builder = (new MaskBuilder())
69
            ->add(self::VIEW)
70
            ->add(self::EDIT)
71
        ;
72
73
        return $builder->get();
74
    }
75
76
    protected function supports(string $attribute, $subject): bool
77
    {
78
        $options = [
79
            self::VIEW,
80
            self::CREATE,
81
            self::EDIT,
82
            self::DELETE,
83
            self::EXPORT,
84
        ];
85
86
        // if the attribute isn't one we support, return false
87
        if (!\in_array($attribute, $options, true)) {
88
            return false;
89
        }
90
91
        // only vote on ResourceNode objects inside this voter
92
        return $subject instanceof ResourceNode;
93
    }
94
95
    protected function voteOnAttribute(string $attribute, $subject, TokenInterface $token): bool
96
    {
97
        // Make sure there is a user object (i.e. that the user is logged in)
98
        // Update. No, anons can enter a node depending in the visibility.
99
        // $user = $token->getUser();
100
        /*if (!$user instanceof UserInterface) {
101
            return false;
102
        }*/
103
        /** @var ResourceNode $resourceNode */
104
        $resourceNode = $subject;
105
        $resourceTypeName = $resourceNode->getResourceType()->getTitle();
106
107
        // Illustrations are always visible, nothing to check.
108
        if ('illustrations' === $resourceTypeName) {
109
            return true;
110
        }
111
112
        // Courses are also a Resource but courses are protected using the CourseVoter, not by ResourceNodeVoter.
113
        if ('courses' === $resourceTypeName) {
114
            return true;
115
        }
116
117
        // Checking admin role.
118
        if ($this->security->isGranted('ROLE_ADMIN')) {
119
            return true;
120
        }
121
122
        if (self::VIEW === $attribute && $this->isBlogResource($resourceNode)) {
123
            return true;
124
        }
125
126
        // @todo
127
        switch ($attribute) {
128
            case self::VIEW:
129
                if ($resourceNode->isPublic()) {
130
                    return true;
131
                }
132
133
                // Exception: allow access to hotspot question images if student can view the quiz
134
                $questionRepo = $this->entityManager->getRepository(CQuizQuestion::class);
135
                $question = $questionRepo->findOneBy(['resourceNode' => $resourceNode]);
136
                if ($question) {
137
                    // Check if it's a Hotspot-type question
138
                    if (\in_array($question->getType(), [6, 7, 8, 20], true)) { // HOT_SPOT, HOT_SPOT_ORDER, HOT_SPOT_DELINEATION, ANNOTATION
139
                        $rel = $this->entityManager
140
                            ->getRepository(CQuizRelQuestion::class)
141
                            ->findOneBy(['question' => $question])
142
                        ;
143
144
                        if ($rel && $rel->getQuiz()) {
145
                            $quiz = $rel->getQuiz();
146
                            // Allow if the user has VIEW rights on the quiz
147
                            if ($this->security->isGranted('VIEW', $quiz)) {
148
                                return true;
149
                            }
150
                        }
151
                    }
152
                }
153
154
                // no break
155
            case self::EDIT:
156
                break;
157
        }
158
159
        $user = $token->getUser();
160
        // Check if I'm the owner.
161
        $creator = $resourceNode->getCreator();
162
163
        if ($creator instanceof UserInterface
164
            && $user instanceof UserInterface
165
            && $user->getUserIdentifier() === $creator->getUserIdentifier()
166
        ) {
167
            return true;
168
        }
169
170
        $resourceTypeTitle = $resourceNode->getResourceType()->getTitle();
171
        if (
172
            \in_array($resourceTypeTitle, [
173
                'student_publications',
174
                'student_publications_corrections',
175
                'student_publications_comments',
176
            ], true)
177
        ) {
178
            if ($creator instanceof UserInterface
179
                && $user instanceof UserInterface
180
                && $user->getUserIdentifier() === $creator->getUserIdentifier()
181
            ) {
182
                return true;
183
            }
184
185
            if ($this->security->isGranted('ROLE_CURRENT_COURSE_STUDENT')
186
                || $this->security->isGranted('ROLE_CURRENT_COURSE_TEACHER')
187
                || $this->security->isGranted('ROLE_CURRENT_COURSE_SESSION_STUDENT')
188
                || $this->security->isGranted('ROLE_CURRENT_COURSE_SESSION_TEACHER')
189
            ) {
190
                return true;
191
            }
192
        }
193
194
        if ('files' === $resourceNode->getResourceType()->getTitle()) {
195
            $document = $this->entityManager
196
                ->getRepository(CDocument::class)
197
                ->findOneBy(['resourceNode' => $resourceNode])
198
            ;
199
200
            if ($document) {
201
                $exists = $this->entityManager
202
                    ->getRepository(CStudentPublicationRelDocument::class)
203
                    ->findOneBy(['document' => $document])
204
                ;
205
206
                if (null !== $exists) {
207
                    return true;
208
                }
209
            }
210
        }
211
212
        // Checking links connected to this resource.
213
        $request = $this->requestStack->getCurrentRequest();
214
215
        $courseId = (int) $request->get('cid');
216
        $sessionId = (int) $request->get('sid');
217
        $groupId = (int) $request->get('gid');
218
219
        // Try Session values.
220
        if (empty($courseId) && $request->hasSession()) {
221
            $courseId = (int) $request->getSession()->get('cid');
222
            $sessionId = (int) $request->getSession()->get('sid');
223
            $groupId = (int) $request->getSession()->get('gid');
224
225
            if (0 === $courseId) {
226
                $courseId = (int) ChamiloSession::read('cid');
227
                $sessionId = (int) ChamiloSession::read('sid');
228
                $groupId = (int) ChamiloSession::read('gid');
229
            }
230
        }
231
232
        $links = $resourceNode->getResourceLinks();
233
        $firstLink = $resourceNode->getResourceLinks()->first();
234
        if ($resourceNode->hasResourceFile() && $firstLink) {
235
            if (0 === $courseId && $firstLink->getCourse() instanceof Course) {
236
                $courseId = (int) $firstLink->getCourse()->getId();
237
            }
238
            if (0 === $sessionId && $firstLink->getSession() instanceof Session) {
239
                $sessionId = (int) $firstLink->getSession()->getId();
240
            }
241
            if (0 === $groupId && $firstLink->getGroup() instanceof CGroup) {
242
                $groupId = (int) $firstLink->getGroup()->getIid();
243
            }
244
            if ($firstLink->getUser() instanceof UserInterface
245
                && 'true' === $this->settingsManager->getSetting('security.access_to_personal_file_for_all')
246
            ) {
247
                return true;
248
            }
249
            if ($firstLink->getCourse() instanceof Course
250
                && $firstLink->getCourse()->isPublic()
251
            ) {
252
                return true;
253
            }
254
        }
255
256
        $linkFound = 0;
257
        $link = null;
258
259
        // @todo implement view, edit, delete.
260
        foreach ($links as $link) {
261
            // Check if resource was sent to the current user.
262
            $linkUser = $link->getUser();
263
            if ($linkUser instanceof UserInterface
264
                && $user instanceof UserInterface
265
                && $linkUser->getUserIdentifier() === $user->getUserIdentifier()) {
266
                $linkFound = 2;
267
268
                break;
269
            }
270
271
            $linkCourse = $link->getCourse();
272
273
            // Course found, but courseId not set, skip course checking.
274
            if ($linkCourse instanceof Course && empty($courseId)) {
275
                continue;
276
            }
277
278
            $linkSession = $link->getSession();
279
            $linkGroup = $link->getGroup();
280
            // $linkUserGroup = $link->getUserGroup();
281
282
            // @todo Check if resource was sent to a usergroup
283
284
            // Check if resource was sent inside a group in a course session.
285
            if (null === $linkUser
286
                && $linkGroup instanceof CGroup && !empty($groupId)
287
                && $linkSession instanceof Session && !empty($sessionId)
288
                && $linkCourse instanceof Course
289
                && ($linkCourse->getId() === $courseId
290
                && $linkSession->getId() === $sessionId
291
                && $linkGroup->getIid() === $groupId)
292
            ) {
293
                $linkFound = 3;
294
295
                break;
296
            }
297
298
            // Check if resource was sent inside a group in a base course.
299
            if (null === $linkUser
300
                && empty($sessionId)
301
                && $linkGroup instanceof CGroup && !empty($groupId)
302
                && $linkCourse instanceof Course && ($linkCourse->getId() === $courseId
303
                && $linkGroup->getIid() === $groupId)
304
            ) {
305
                $linkFound = 4;
306
307
                break;
308
            }
309
310
            // Check if resource was sent to a course inside a session.
311
            if (null === $linkUser
312
                && $linkSession instanceof Session && !empty($sessionId)
313
                && $linkCourse instanceof Course && ($linkCourse->getId() === $courseId
314
                && $linkSession->getId() === $sessionId)
315
            ) {
316
                $linkFound = 5;
317
318
                break;
319
            }
320
321
            // Check if resource was sent to a course.
322
            if (null === $linkUser
323
                && $linkCourse instanceof Course && $linkCourse->getId() === $courseId
324
            ) {
325
                $linkFound = 6;
326
327
                break;
328
            }
329
330
            /*if (ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()) {
331
             * $linkFound = true;
332
             * break;
333
             * }*/
334
        }
335
336
        // No link was found.
337
        if (0 === $linkFound) {
338
            return false;
339
        }
340
341
        // Getting rights from the link
342
        $rightsFromResourceLink = $link->getResourceRights();
343
        $allowAnonsToView = false;
344
345
        $rights = [];
346
        if ($rightsFromResourceLink->count() > 0) {
347
            // Taken rights from the link.
348
            $rights = $rightsFromResourceLink;
349
        }
350
351
        // Taken the rights from context (request + user roles).
352
        // $rights = $link->getResourceNode()->getTool()->getToolResourceRight();
353
        // $rights = $link->getResourceNode()->getResourceType()->getTool()->getToolResourceRight();
354
355
        // By default, the rights are:
356
        // Teachers: CRUD.
357
        // Students: Only read.
358
        // Anons: Only read.
359
        $readerMask = self::getReaderMask();
360
        $editorMask = self::getEditorMask();
361
362
        if ($courseId && $link->hasCourse() && $link->getCourse()->getId() === $courseId) {
363
            // If teacher.
364
            if ($this->security->isGranted(self::ROLE_CURRENT_COURSE_TEACHER)) {
365
                $resourceRight = (new ResourceRight())
366
                    ->setMask($editorMask)
367
                    ->setRole(self::ROLE_CURRENT_COURSE_TEACHER)
368
                ;
369
                $rights[] = $resourceRight;
370
            }
371
372
            // If student.
373
            if ($this->security->isGranted(self::ROLE_CURRENT_COURSE_STUDENT)
374
                && ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()
375
            ) {
376
                $resourceRight = (new ResourceRight())
377
                    ->setMask($readerMask)
378
                    ->setRole(self::ROLE_CURRENT_COURSE_STUDENT)
379
                ;
380
                $rights[] = $resourceRight;
381
            }
382
383
            // For everyone.
384
            if (ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()
385
                && $link->getCourse()->isPublic()
386
            ) {
387
                $allowAnonsToView = true;
388
                $resourceRight = (new ResourceRight())
389
                    ->setMask($readerMask)
390
                    ->setRole('IS_AUTHENTICATED_ANONYMOUSLY')
391
                ;
392
                $rights[] = $resourceRight;
393
            }
394
        }
395
396
        if (!empty($groupId)) {
397
            if ($this->security->isGranted(self::ROLE_CURRENT_COURSE_GROUP_TEACHER)) {
398
                $resourceRight = (new ResourceRight())
399
                    ->setMask($editorMask)
400
                    ->setRole(self::ROLE_CURRENT_COURSE_GROUP_TEACHER)
401
                ;
402
                $rights[] = $resourceRight;
403
            }
404
405
            if ($this->security->isGranted(self::ROLE_CURRENT_COURSE_GROUP_STUDENT)) {
406
                $resourceRight = (new ResourceRight())
407
                    ->setMask($readerMask)
408
                    ->setRole(self::ROLE_CURRENT_COURSE_GROUP_STUDENT)
409
                ;
410
                $rights[] = $resourceRight;
411
            }
412
        }
413
414
        if (!empty($sessionId)) {
415
            if ($this->security->isGranted(self::ROLE_CURRENT_COURSE_SESSION_TEACHER)) {
416
                $resourceRight = (new ResourceRight())
417
                    ->setMask($editorMask)
418
                    ->setRole(self::ROLE_CURRENT_COURSE_SESSION_TEACHER)
419
                ;
420
                $rights[] = $resourceRight;
421
            }
422
423
            if ($this->security->isGranted(self::ROLE_CURRENT_COURSE_SESSION_STUDENT)) {
424
                $resourceRight = (new ResourceRight())
425
                    ->setMask($readerMask)
426
                    ->setRole(self::ROLE_CURRENT_COURSE_SESSION_STUDENT)
427
                ;
428
                $rights[] = $resourceRight;
429
            }
430
        }
431
432
        if (empty($rights) && ResourceLink::VISIBILITY_PUBLISHED === $link->getVisibility()) {
433
            // Give just read access.
434
            $resourceRight = (new ResourceRight())
435
                ->setMask($readerMask)
436
                ->setRole('ROLE_USER')
437
            ;
438
            $rights[] = $resourceRight;
439
        }
440
441
        // Asked mask
442
        $mask = new MaskBuilder();
443
        $mask->add($attribute);
444
445
        $askedMask = (string) $mask->get();
446
447
        // Creating roles
448
        // @todo move this in a service
449
        $anon = new GenericRole('IS_AUTHENTICATED_ANONYMOUSLY');
450
        $userRole = new GenericRole('ROLE_USER');
451
        $student = new GenericRole('ROLE_STUDENT');
452
        $teacher = new GenericRole('ROLE_TEACHER');
453
        $studentBoss = new GenericRole('ROLE_STUDENT_BOSS');
454
455
        $currentStudent = new GenericRole(self::ROLE_CURRENT_COURSE_STUDENT);
456
        $currentTeacher = new GenericRole(self::ROLE_CURRENT_COURSE_TEACHER);
457
458
        $currentStudentGroup = new GenericRole(self::ROLE_CURRENT_COURSE_GROUP_STUDENT);
459
        $currentTeacherGroup = new GenericRole(self::ROLE_CURRENT_COURSE_GROUP_TEACHER);
460
461
        $currentStudentSession = new GenericRole(self::ROLE_CURRENT_COURSE_SESSION_STUDENT);
462
        $currentTeacherSession = new GenericRole(self::ROLE_CURRENT_COURSE_SESSION_TEACHER);
463
464
        // Setting Simple ACL.
465
        $acl = (new Acl())
466
            ->addRole($anon)
467
            ->addRole($userRole)
468
            ->addRole($student)
469
            ->addRole($teacher)
470
            ->addRole($studentBoss)
471
472
            ->addRole($currentStudent)
473
            ->addRole($currentTeacher, self::ROLE_CURRENT_COURSE_STUDENT)
474
475
            ->addRole($currentStudentSession)
476
            ->addRole($currentTeacherSession, self::ROLE_CURRENT_COURSE_SESSION_STUDENT)
477
478
            ->addRole($currentStudentGroup)
479
            ->addRole($currentTeacherGroup, self::ROLE_CURRENT_COURSE_GROUP_STUDENT)
480
        ;
481
482
        // Add a security resource.
483
        $linkId = (string) $link->getId();
484
        $acl->addResource(new GenericResource($linkId));
485
486
        // Check all the right this link has.
487
        // Set rights from the ResourceRight.
488
        foreach ($rights as $right) {
489
            $acl->allow($right->getRole(), null, (string) $right->getMask());
490
        }
491
492
        // Role and permissions settings
493
        // Student can just view (read)
494
        // $acl->allow($student, null, self::getReaderMask());
495
496
        // Teacher can view/edit
497
        /*$acl->allow(
498
            $teacher,
499
            null,
500
            [
501
                self::getReaderMask(),
502
                self::getEditorMask(),
503
            ]
504
        );*/
505
506
        // Anons can see.
507
        if ($allowAnonsToView) {
508
            $acl->allow($anon, null, (string) self::getReaderMask());
509
        }
510
511
        if ($token instanceof NullToken) {
512
            return $acl->isAllowed('IS_AUTHENTICATED_ANONYMOUSLY', $linkId, $askedMask);
513
        }
514
515
        $roles = $user->getRoles();
516
517
        foreach ($roles as $role) {
518
            if ($acl->isAllowed($role, $linkId, $askedMask)) {
519
                return true;
520
            }
521
        }
522
523
        return false;
524
    }
525
526
    private function isBlogResource(ResourceNode $node): bool
527
    {
528
        $type = $node->getResourceType()?->getTitle();
529
        if (\in_array($type, ['blog', 'blogs', 'c_blog', 'c_blogs'], true)) {
530
            return true;
531
        }
532
533
        $firstLink = $node->getResourceLinks()->first();
534
        if ($firstLink && method_exists($firstLink, 'getTool') && $firstLink->getTool()) {
535
            $toolName = method_exists($firstLink->getTool(), 'getName')
536
                ? $firstLink->getTool()->getName()
537
                : $firstLink->getTool()->getTitle();
538
539
            if (\in_array(\strtolower((string) $toolName), ['blog', 'blogs'], true)) {
540
                return true;
541
            }
542
        }
543
544
        return false;
545
    }
546
}
547