Skip to content

Commit 1775c7b

Browse files
authored
Merge pull request #6502 from christianbeeznest/rna-22586-4
Course: Enforce course access restrictions based on prerequisites - refs BT#22586
2 parents 419aebf + 38b4606 commit 1775c7b

File tree

2 files changed

+88
-29
lines changed

2 files changed

+88
-29
lines changed

src/CoreBundle/Repository/SequenceResourceRepository.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ public function deleteSequenceResource(int $resourceId, int $type): void
116116
*
117117
* @return array
118118
*/
119-
public function getRequirements(int $resourceId, int $type)
119+
public function getRequirements(int $resourceId, int $type): array
120120
{
121121
$sequencesResource = $this->findBy([
122122
'resourceId' => $resourceId,
@@ -222,7 +222,7 @@ public function getRequirementsAndDependenciesWithinSequences(int $resourceId, i
222222
*
223223
* @return array
224224
*/
225-
public function checkRequirementsForUser(array $sequences, int $type, int $userId)
225+
public function checkRequirementsForUser(array $sequences, int $type, int $userId): array
226226
{
227227
$sequenceList = [];
228228
$em = $this->getEntityManager();
@@ -372,7 +372,7 @@ public function checkCourseRequirements(int $userId, Course $course, int $sessio
372372
*
373373
* @return bool
374374
*/
375-
public function checkSequenceAreCompleted(array $sequences)
375+
public function checkSequenceAreCompleted(array $sequences): bool
376376
{
377377
foreach ($sequences as $sequence) {
378378
if (!isset($sequence['requirements'])) {

src/CoreBundle/Security/Authorization/Voter/CourseVoter.php

Lines changed: 85 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,17 @@
77
namespace Chamilo\CoreBundle\Security\Authorization\Voter;
88

99
use Chamilo\CoreBundle\Entity\Course;
10+
use Chamilo\CoreBundle\Entity\SequenceResource;
1011
use Chamilo\CoreBundle\Entity\Session;
1112
use Chamilo\CoreBundle\Entity\User;
13+
use Chamilo\CoreBundle\Exception\NotAllowedException;
1214
use Doctrine\ORM\EntityManagerInterface;
1315
use Symfony\Bundle\SecurityBundle\Security;
1416
use Symfony\Component\HttpFoundation\RequestStack;
1517
use Symfony\Component\Security\Core\Authentication\Token\TokenInterface;
1618
use Symfony\Component\Security\Core\Authorization\Voter\Voter;
1719
use Symfony\Component\Security\Core\User\UserInterface;
20+
use Symfony\Contracts\Translation\TranslatorInterface;
1821

1922
/**
2023
* @extends Voter<'VIEW'|'EDIT'|'DELETE', Course>
@@ -30,6 +33,7 @@ class CourseVoter extends Voter
3033

3134
public function __construct(
3235
private readonly Security $security,
36+
private readonly TranslatorInterface $translator,
3337
RequestStack $requestStack,
3438
EntityManagerInterface $entityManager
3539
) {
@@ -58,10 +62,6 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
5862
{
5963
/** @var User $user */
6064
$user = $token->getUser();
61-
// Anons can enter a course depending on the course visibility.
62-
/*if (!$user instanceof UserInterface) {
63-
return false;
64-
}*/
6565

6666
// Admins have access to everything.
6767
if ($this->security->isGranted('ROLE_ADMIN')) {
@@ -93,9 +93,15 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
9393
// "Open to the world" no need to check if user is registered or if user exists.
9494
// Course::OPEN_WORLD
9595
if ($course->isPublic()) {
96-
/*$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_STUDENT);
97-
$token->setUser($user);*/
98-
96+
if ($this->isStudent($user, $course, $session)) {
97+
if ($this->isCourseLockedForUser($user, $course, $session?->getId() ?? 0)) {
98+
throw new NotAllowedException(
99+
$this->translator->trans('This course is locked. You must complete the prerequisite(s) first.'),
100+
'warning',
101+
403
102+
);
103+
}
104+
}
99105
return true;
100106
}
101107

@@ -106,20 +112,16 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
106112

107113
// If user is logged in and is open platform, allow access.
108114
if (Course::OPEN_PLATFORM === $course->getVisibility()) {
109-
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_STUDENT);
110-
111-
if ($course->hasUserAsTeacher($user)) {
112-
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_TEACHER);
115+
if ($this->isStudent($user, $course, $session)) {
116+
if ($this->isCourseLockedForUser($user, $course, $session?->getId() ?? 0)) {
117+
throw new NotAllowedException(
118+
$this->translator->trans('This course is locked. You must complete the prerequisite(s) first.'),
119+
'warning',
120+
403
121+
);
122+
}
113123
}
114124

115-
$token->setUser($user);
116-
117-
return true;
118-
}
119-
120-
// Course::REGISTERED
121-
// User must be subscribed in the course no matter if is teacher/student
122-
if ($course->hasSubscriptionByUser($user)) {
123125
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_STUDENT);
124126

125127
if ($course->hasUserAsTeacher($user)) {
@@ -137,23 +139,45 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
137139
$userIsCourseCoach = $session->hasCourseCoachInCourse($user, $course);
138140
$userIsStudent = $session->hasUserInCourse($user, $course, Session::STUDENT);
139141

140-
if ($userIsGeneralCoach) {
142+
if ($userIsGeneralCoach || $userIsCourseCoach) {
141143
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_TEACHER);
142-
143144
return true;
144145
}
145146

146-
if ($userIsCourseCoach) {
147-
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_TEACHER);
147+
if ($userIsStudent) {
148+
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_STUDENT);
149+
if ($this->isCourseLockedForUser($user, $course, $session->getId())) {
150+
throw new NotAllowedException(
151+
$this->translator->trans('This course is locked. You must complete the prerequisite(s) first.'),
152+
'warning',
153+
403
154+
);
155+
}
148156

149157
return true;
150158
}
159+
}
151160

152-
if ($userIsStudent) {
153-
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_SESSION_STUDENT);
161+
// Course::REGISTERED
162+
// User must be subscribed in the course no matter if is teacher/student
163+
if ($course->hasSubscriptionByUser($user)) {
164+
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_STUDENT);
154165

155-
return true;
166+
if ($course->hasUserAsTeacher($user)) {
167+
$user->addRole(ResourceNodeVoter::ROLE_CURRENT_COURSE_TEACHER);
168+
}
169+
170+
if ($this->isCourseLockedForUser($user, $course)) {
171+
throw new NotAllowedException(
172+
$this->translator->trans('This course is locked. You must complete the prerequisite(s) first.'),
173+
'warning',
174+
403
175+
);
156176
}
177+
178+
$token->setUser($user);
179+
180+
return true;
157181
}
158182

159183
break;
@@ -173,4 +197,39 @@ protected function voteOnAttribute(string $attribute, $subject, TokenInterface $
173197

174198
return false;
175199
}
200+
201+
/**
202+
* Checks whether the given course is locked for the user
203+
* due to unmet prerequisite sequences.
204+
*/
205+
private function isCourseLockedForUser(User $user, Course $course, int $sessionId = 0): bool
206+
{
207+
$sequenceRepo = $this->entityManager->getRepository(SequenceResource::class);
208+
209+
$sequences = $sequenceRepo->getRequirements(
210+
$course->getId(),
211+
SequenceResource::COURSE_TYPE
212+
);
213+
214+
if (empty($sequences)) {
215+
return false;
216+
}
217+
218+
$statusList = $sequenceRepo->checkRequirementsForUser(
219+
$sequences,
220+
SequenceResource::COURSE_TYPE,
221+
$user->getId()
222+
);
223+
224+
return !$sequenceRepo->checkSequenceAreCompleted($statusList);
225+
}
226+
227+
private function isStudent(User $user, Course $course, ?Session $session): bool
228+
{
229+
if ($session) {
230+
return $session->hasUserInCourse($user, $course, Session::STUDENT);
231+
}
232+
233+
return $course->hasUserAsStudent($user);
234+
}
176235
}

0 commit comments

Comments
 (0)