Passed
Pull Request — master (#7134)
by
unknown
09:48
created

isAnnouncementFileVisibleForCurrentRequest()   B

Complexity

Conditions 7
Paths 7

Size

Total Lines 34
Code Lines 21

Duplication

Lines 0
Ratio 0 %

Importance

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