Merge branch 'MDL-88061-main' of https://github.com/aanabit/moodle

This commit is contained in:
Sara Arjona
2026-03-13 16:02:50 +01:00
4 changed files with 337 additions and 4 deletions
@@ -66,6 +66,24 @@ class restricted_module {
$format = course_get_format($course);
$course->format = $format->get_format();
// Confirm the module has a view page. Redirect to section page otherwise.
if (!$cmurl = $cminfo->get_url()) {
$cmurl = $format->get_view_url($sectioninfo->section, ['navigation' => true]);
$cmurl->set_anchor('module-' . $cmdata->id);
return $this->redirect($response, $cmurl);
}
// Confirm the module is actually restricted. Redirect to module page otherwise.
if ($cminfo->get_user_visible()) {
return $this->redirect($response, $cmurl);
}
// Confirm the module is not stealth and restrictions are visible.
// Redirect to module page to require_login() otherwise.
if (!$cminfo->is_visible_on_course_page()) {
return $this->redirect($response, $cmurl);
}
$url = \core\router\util::get_path_for_callable(
[self::class, 'restricted_module_page'],
['cm' => $cmdata->id],
@@ -89,9 +107,13 @@ class restricted_module {
);
$renderer = $format->get_renderer($PAGE);
$response->getBody()->write($OUTPUT->header());
if ($header = $OUTPUT->header()) {
$response->getBody()->write($header);
}
$response->getBody()->write($renderer->render($cmoutput));
$response->getBody()->write($OUTPUT->footer());
if ($footer = $OUTPUT->footer()) {
$response->getBody()->write($footer);
}
$eventdata = [
'objectid' => $cmdata->id,
@@ -59,6 +59,13 @@ class restricted_section {
require_once($CFG->dirroot . '/course/lib.php');
// Confirm the section is actually restricted. Redirect to section page otherwise.
$modinfo = get_fast_modinfo($section->course);
$sectioninfo = $modinfo->get_section_info($section->section, MUST_EXIST);
if ($sectioninfo->uservisible) {
$url = course_get_url($section->course, $section->section, ['navigation' => true]);
return $this->redirect($response, $url);
}
$course = get_course($section->course);
$context = \context_course::instance($course->id);
$format = course_get_format($course->id);
@@ -81,9 +88,13 @@ class restricted_section {
$PAGE->set_heading($sectiontitle);
$PAGE->set_secondary_navigation(false);
$renderer = $format->get_renderer($PAGE);
$response->getBody()->write($OUTPUT->header());
if ($header = $OUTPUT->header()) {
$response->getBody()->write($header);
}
$response->getBody()->write($renderer->render($sectionoutput));
$response->getBody()->write($OUTPUT->footer());
if ($footer = $OUTPUT->footer()) {
$response->getBody()->write($footer);
}
// Trigger section viewed event.
course_section_view($context, $section->id, true);
@@ -0,0 +1,182 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\route\controller;
use core\router\route_loader_interface;
use core\tests\router\route_testcase;
use core\url;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
/**
* Restricted module controller tests.
*
* @package core_course
* @copyright 2026 Amaia Anabitarte <[email protected]>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
**/
#[CoversClass(\core_course\route\controller\restricted_module::class)]
final class restricted_module_test extends route_testcase {
/**
* Test the restricted module in different conditions.
*
* @param string $role
* @param string $modulename
* @param int $timedifference Positive number for not filled requirement,
* and negative for filled requirement.
* @param bool $showrestriction Whether the restriction should be available or not.
* @param bool $visible 'visible' value for hidden modules.
* @param bool $visibleoncourse 'visibleoncoursepage' value for stealth modules.
* @param int $expectedstatus Expected response status code.
* @param string $redirection Expected redirection URL.
*/
#[DataProvider('restricted_module_provider')]
public function test_restricted_module(
string $role = 'student',
string $modulename = 'page',
int $timedifference = 3600,
bool $showrestriction = true,
bool $visible = true,
bool $visibleoncourse = true,
int $expectedstatus = 200,
string $redirection = 'empty',
): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course();
$restriction = json_encode(\core_availability\tree::get_root_json(
[
\availability_date\condition::get_json(
\availability_date\condition::DIRECTION_FROM,
time() + $timedifference,
),
],
'&',
$showrestriction,
));
$module = $this->getDataGenerator()->create_module($modulename, [
'course' => $course->id,
'visible' => $visible,
'visibleoncourse' => $visibleoncourse,
'availability' => $restriction,
]);
$user = $generator->create_and_enrol($course, $role);
$this->setUser($user);
$response = $this->process_request(
'GET',
'course/cms/' . $module->cmid . '/restricted',
route_loader_interface::ROUTE_GROUP_PAGE
);
$this->assert_valid_response($response, $expectedstatus);
$location = $response->getHeader('Location');
switch ($redirection) {
case 'empty':
$this->assertEmpty($location, 'There is no redirection.');
break;
case 'module':
$this->assertNotEmpty($location[0]);
$this->assertEquals(
new url("/mod/{$modulename}/view.php", ['id' => $module->cmid]),
new url($location[0])
);
break;
case 'section':
$this->assertNotEmpty($location[0]);
$this->assertStringContainsString('course/section.php', $location[0]);
break;
}
}
/**
* Data provider for test_restricted_module.
*
* @return \Generator
*/
public static function restricted_module_provider(): \Generator {
yield 'Teacher skipping restrictions' => [
'role' => 'teacher',
'modulename' => 'page',
'timedifference' => 3600,
'showrestriction' => true,
'expectedstatus' => 302,
'redirection' => 'module',
];
yield 'Applied restriction (student)' => [
'role' => 'student',
'modulename' => 'page',
'timedifference' => 3600,
'showrestriction' => true,
'expectedstatus' => 200,
'redirection' => 'empty',
];
yield 'Not applied restriction (student)' => [
'role' => 'student',
'modulename' => 'page',
'timedifference' => -3600,
'showrestriction' => true,
'expectedstatus' => 302,
'redirection' => 'module',
];
yield 'Modules with no url - Text and media (student)' => [
// Redirect to section page and scroll to the module.
'role' => 'student',
'modulename' => 'label',
'timedifference' => 3600,
'showrestriction' => true,
'expectedstatus' => 302,
'redirection' => 'section',
];
yield 'Stealth restricted activities (student)' => [
// Show restricted page for stealth restricted pages.
'role' => 'student',
'modulename' => 'page',
'timedifference' => 3600,
'showrestriction' => true,
'visibleoncourse' => false,
'expectedstatus' => 200,
'redirection' => 'empty',
];
yield 'Hidden restricted activities (student)' => [
// Redirect to view page of hidden modules to let
// view.php and require_login() to take care of them.
'role' => 'student',
'modulename' => 'page',
'timedifference' => 3600,
'showrestriction' => true,
'visible' => false,
'visibleoncourse' => false,
'expectedstatus' => 302,
'redirection' => 'module',
];
yield 'Hidden restrictions (student)' => [
// Redirect to view page of hidden restriction modules to let
// view.php and require_login() to take care of them.
'role' => 'student',
'modulename' => 'page',
'timedifference' => 3600,
'showrestriction' => false,
'expectedstatus' => 302,
'redirection' => 'module',
];
}
}
@@ -0,0 +1,118 @@
<?php
// This file is part of Moodle - http://moodle.org/
//
// Moodle is free software: you can redistribute it and/or modify
// it under the terms of the GNU General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Moodle is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License
// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
namespace core_course\route\controller;
use core\router\route_loader_interface;
use core\tests\router\route_testcase;
use core\url;
use core_courseformat\formatactions;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
/**
* Restricted section controller tests.
*
* @package core_course
* @copyright 2026 Amaia Anabitarte <[email protected]>
* @license https://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
**/
#[CoversClass(\core_course\route\controller\restricted_section::class)]
final class restricted_section_test extends route_testcase {
/**
* Test the restricted section URL redirections for none restricted sections.
*
* The reason why the section is not restricted for the user
* (whether the user has enough permission to see the section page or
* whether the restrictions don't apply to the user) is not important.
* So we are testing only the case where the uservisible is true
* (because the user has enough permisssio) and uservisible is false
* (because the restriction applies to a student with no enough permission)
*
* @param string $role
* @param int $expectedstatus Expected response status code.
* @param bool $redirection Whether redirection should happen or not.
*/
#[DataProvider('restricted_section_provider')]
public function test_restricted_section(
string $role,
int $expectedstatus,
bool $redirection,
): void {
$this->resetAfterTest();
$generator = $this->getDataGenerator();
$course = $generator->create_course(['numsections' => 2]);
$modinfo = get_fast_modinfo($course);
$restriction = json_encode(\core_availability\tree::get_root_json(
[
\availability_date\condition::get_json(
\availability_date\condition::DIRECTION_FROM,
time() + 3600,
),
],
'&',
true,
));
// Restrict Section 1.
formatactions::section($course)->update(
$modinfo->get_section_info(1),
['availability' => $restriction],
);
$user = $generator->create_and_enrol($course, $role);
$this->setUser($user);
$restrictedsection = $modinfo->get_section_info(1);
$response = $this->process_request(
'GET',
'course/sections/' . $restrictedsection->id . '/restricted',
route_loader_interface::ROUTE_GROUP_PAGE
);
$this->assert_valid_response($response, $expectedstatus);
$location = $response->getHeader('Location'); // Just to consume the header if any.
if ($redirection) {
$this->assertNotEmpty($location, 'The redirection header should be present.');
$this->assertEquals(
new url('/course/section.php', ['id' => $restrictedsection->id]),
new url($location[0])
);
} else {
$this->assertEmpty($location, 'There is no redirection.');
}
}
/**
* Data provider for test_restricted_section.
*
* @return \Generator
*/
public static function restricted_section_provider(): \Generator {
yield 'Teacher - Permission to see restricted page' => [
'role' => 'teacher',
'expectedstatus' => 302,
'redirection' => true,
];
yield 'Student - Stays in the restricted page' => [
'role' => 'student',
'expectedstatus' => 200,
'redirection' => false,
];
}
}