Bugfixes in access control and tweaked user_tops to properly handle permissions on one category while visiblity is only granted on subcategories of it.

This commit is contained in:
PMKuipers 2024-02-05 23:32:22 +01:00
parent 8aed72af70
commit dfc9f86d1d
9 changed files with 243 additions and 12001 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -31,6 +31,7 @@ use \local_treestudyplan\local\helpers\webservicehelper;
use \local_treestudyplan\completionscanner; use \local_treestudyplan\completionscanner;
use \local_treestudyplan\gradingscanner; use \local_treestudyplan\gradingscanner;
use \core_course_category; use \core_course_category;
use moodle_exception;
/** /**
* Webservice related to courses * Webservice related to courses
@ -61,6 +62,31 @@ class courseservice extends \external_api {
} }
$tops = []; $tops = [];
if (has_capability($capability,\context_system::instance(),$userid)) {
if ($capability == 'moodle/category:viewcourselist') {
// We are now just looking for the visible main level categories.
// Add all categories of depth = 1;
$rs = $DB->get_recordset("course_categories",["depth" => 1],'sortorder');
foreach( $rs as $rcat) {
// Get the category, and double check if the category is visible to the current user.
// Just in case it is a hidden category and the user does not have the viewhidden permission.
$cat = \core_course_category::get($rcat->id, \IGNORE_MISSING, false, $userid);
if ($cat !== null) {
// Register the category.
array_push($tops,$cat);
}
}
$rs->close();
} else {
// We were primarily searching for a
// Return the top visible categories for this user.
// Recurses only once.
return self::user_tops($userid);
}
} else {
// We need to search for the permissions on an individial context level.
// This part finds all top categories with a certain permission that are also visible for the user.
/* /*
SELECT UNIQUE ctx.* FROM mdl_context AS ctx SELECT UNIQUE ctx.* FROM mdl_context AS ctx
INNER JOIN mdl_role_assignments AS ra ON ra.contextid = ctx.id INNER JOIN mdl_role_assignments AS ra ON ra.contextid = ctx.id
@ -70,19 +96,19 @@ class courseservice extends \external_api {
AND ra.userid = 58 AND rc.capability = 'moodle/category:viewcourselist' AND ra.userid = 58 AND rc.capability = 'moodle/category:viewcourselist'
ORDER BY ctx.depth ASC, cat.sortorder ASC; ORDER BY ctx.depth ASC, cat.sortorder ASC;
*/ */
$capability = 'moodle/category:viewcourselist'; //$capability = 'moodle/category:viewcourselist';
$sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx $sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx
INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id
INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid
LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id
WHERE ( ctx.contextlevel = :ctxl_coursecat OR ctx.contextlevel = :ctxl_system ) WHERE ( ctx.contextlevel = :ctxl_coursecat )
AND ra.userid = :userid AND rc.capability = :capability AND ra.userid = :userid AND rc.capability = :capability
ORDER BY ctx.depth ASC, cat.sortorder ASC"; ORDER BY ctx.depth ASC, cat.sortorder ASC";
// Use recordset to handle the eventuality of a really big and complex moodle setup. // Use recordset to handle the eventuality of a really big and complex moodle setup.
$recordset = $DB->get_recordset_sql($sql, ["userid" => $userid, "capability" => $capability, $recordset = $DB->get_recordset_sql($sql, ["userid" => $userid, "capability" => $capability,
"ctxl_coursecat" => \CONTEXT_COURSECAT, "ctxl_system" => \CONTEXT_SYSTEM,]); "ctxl_coursecat" => \CONTEXT_COURSECAT,]);
$contextids = []; $contextids = [];
foreach ($recordset as $r) { foreach ($recordset as $r) {
@ -98,23 +124,73 @@ class courseservice extends \external_api {
// Double check permissions according to the moodle capability system. // Double check permissions according to the moodle capability system.
$ctx = \context::instance_by_id($r->id); $ctx = \context::instance_by_id($r->id);
if (has_capability($capability,$ctx,$userid)) { if (has_capability($capability,$ctx,$userid)) {
// Get the actual category object.
if ($r->contextlevel == \CONTEXT_SYSTEM) {
// The user can view all (non-hidden) categories, so add all categories of depth = 1;
$tops = []; // Reset the array, just in case.
$rs = $DB->get_recordset("course_categories",["depth" => 1],'sortorder');
foreach( $rs as $rcat) {
// Get the category, and double check if the category is visible to the current user. // Get the category, and double check if the category is visible to the current user.
// Just in case it is a hidden category and the user does not have the viewhidden permission. // Just in case it is a hidden category and the user does not have the viewhidden permission.
$cat = \core_course_category::get($rcat->id, \IGNORE_MISSING, false, $userid); $cat = \core_course_category::get($r->instanceid, \IGNORE_MISSING, false, $userid);
if ($cat !== null) { if ($cat !== null) {
// Register the context id in the list now, since we know the category is really visible.
array_push($contextids,$r->id);
// Register the category. // Register the category.
array_push($tops,$cat); array_push($tops,$cat);
} else {
// The category is not visible. Add the first known visible subcategories.
$children = self::get_first_visible_children($r->id,$userid);
foreach ($children as $cat) {
array_push($tops,$cat);
} }
} }
$rs->close(); }
break; // Stop the loop immediately so the list of visible depth 2 categories is returned. }
} else { // Can only be \CONTEXT_COURSECAT according to the SQL query. }
$recordset->close();
}
return $tops;
}
/**
* Find the top-most child categories for a given category that are visible.
*
* @param int $parentid The category to search for
* @return array of \core_course_category
*/
private static function get_first_visible_children($parentid, $userid) {
global $DB;
$capability = 'moodle/category:viewcourselist';
$tops = [];
$path_like = $DB->sql_like('ctx.path',':pathsearch');
$sql = "SELECT UNIQUE ctx.* FROM {context} AS ctx
INNER JOIN {role_assignments} AS ra ON ra.contextid = ctx.id
INNER JOIN {role_capabilities} AS rc ON ra.roleid = rc.roleid
LEFT JOIN {course_categories} AS cat ON ctx.instanceid = cat.id
WHERE ( ctx.contextlevel = :ctxl_coursecat )
AND ra.userid = :userid AND rc.capability = :capability
AND {$path_like}
ORDER BY ctx.depth ASC, cat.sortorder ASC";
// Use recordset to handle the eventuality of a really big and complex moodle setup.
$recordset = $DB->get_recordset_sql($sql, ["userid" => $userid,
"capability" => $capability,
"ctxl_coursecat" => \CONTEXT_COURSECAT,
"pathsearch" => "%/{$parentid}/%",
]);
$contextids = [];
foreach ($recordset as $r) {
// Get the paths as an array.
$parents = explode("/",$r->path);
// Strip the first item, since it is an empty one.
array_shift($parents);
// Strip the last item, since it refers to self.
array_pop($parents);
// Figure out if any of the remaining parent contexts are already contexts with permission.
$intersect = array_intersect($contextids,$parents);
if (count($intersect) == 0) {
// Double check permissions according to the moodle capability system.
$ctx = \context::instance_by_id($r->id);
if (has_capability($capability,$ctx,$userid)) {
// Get the category, and double check if the category is visible to the current user. // Get the category, and double check if the category is visible to the current user.
// Just in case it is a hidden category and the user does not have the viewhidden permission. // Just in case it is a hidden category and the user does not have the viewhidden permission.
$cat = \core_course_category::get($r->instanceid, \IGNORE_MISSING, false, $userid); $cat = \core_course_category::get($r->instanceid, \IGNORE_MISSING, false, $userid);
@ -127,11 +203,11 @@ class courseservice extends \external_api {
} }
} }
} }
}
$recordset->close(); $recordset->close();
return $tops; return $tops;
} }
/** /**
* Return value description for map_categories function * Return value description for map_categories function
*/ */
@ -179,18 +255,26 @@ class courseservice extends \external_api {
public static function map_categories($rootid = 0) { public static function map_categories($rootid = 0) {
global $USER; global $USER;
$root = \core_course_category::get($rootid,\MUST_EXIST,true);
// Determine top categories from provided context. // Determine top categories from provided context.
if ($root->id == 0) { if ($rootid == 0) {
// On the system level, determine the user's topmost allowed catecories. // On the system level, determine the user's topmost allowed catecories.
// This uses a custom function, since moodle's "core_course_category::user_top()" is somewhat deficient. // This uses a custom function, since moodle's "core_course_category::user_top()" is somewhat deficient.
$children = self::user_tops($USER->id); $children = self::user_tops();
} else if ($root->is_uservisible()) { if (count($children) == 0) {
throw new moodle_exception("error:nocategoriesvisible","local_treestudyplan");
}
} else {
$root = \core_course_category::get($rootid,\MUST_EXIST,true);
if ($root->is_uservisible()) {
$children = [$root]; $children = [$root];
} else { } else {
return []; // Category not user visible. $ci = new contextinfo($root->get_context());
$contextname = $ci->pathstr();
throw new moodle_exception("error:cannotviewcategory","local_treestudyplan",'',$contextname);
}
} }
$list = []; $list = [];
@ -493,36 +577,59 @@ class courseservice extends \external_api {
} else { // Operation == "view" || default. } else { // Operation == "view" || default.
$capability = self::CAP_VIEW; $capability = self::CAP_VIEW;
} }
// Retrieve context ids used.
$contextcounts = []; // Get the context ids of all categories the user has access to view and wich have the given permission.
$contextids = []; $contextids = [];
$tops = self::user_tops(null,$capability);
foreach ($tops as $cat) {
$ctx = \context_coursecat::instance($cat->id);
$contextids[] = $ctx->id;
}
// Now get an overview of the number of study plans in a given context.
$contextcounts = [];
$insertctxs = [];
$rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan} $rs = $DB->get_recordset_sql("SELECT DISTINCT context_id, COUNT(*) as num FROM {local_treestudyplan}
GROUP BY context_id"); GROUP BY context_id");
foreach ($rs as $r) { foreach ($rs as $r) {
// Build the counts.
$contextcounts[$r->context_id] = $r->num; $contextcounts[$r->context_id] = $r->num;
$contextids[] = $r->context_id; // Add any of the categories containing studyplans to the list.
$ctx = \context::instance_by_id($r->context_id);
if (has_capability($capability,$ctx) && !in_array($r->context_id,$contextids)) {
$insertctxs[] = $ctx;
} }
}
$rs->close(); $rs->close();
// Add system context to list if needed.
if (!in_array(1,$contextids)) {
array_unshift($contextids,1);
}
$cats = []; $cats = [];
// If the reference context id is not in the list, push it there
// If the reference context id is not in the list, push it there if the user has proper permissions in that context
if ($refctxid > 1 && !in_array($refctxid, $contextids)) { if ($refctxid > 1 && !in_array($refctxid, $contextids)) {
try { try {
// Get the context.
$refctx = \context::instance_by_id($refctxid); $refctx = \context::instance_by_id($refctxid);
$refpath = $refctx->get_parent_context_ids(true); // Double check permissions.
if (has_capability($capability,$refctx)) {
$insertctxs[] = $refctx;
}
} catch(\dml_missing_record_exception $x) {
// ignore context
}
}
foreach ($insertctxs as $ictx) {
// Place this context and all relevant direct parents in the correct spots.
$ipath = $ictx->get_parent_context_ids(true);
$found = false; $found = false;
foreach ($refpath as $i => $pid) { foreach ($ipath as $i => $pid) {
$idx = array_search($pid,$contextids); $idx = array_search($pid,$contextids);
if($idx !== false) { if($idx !== false) {
$contextids = array_merge( $contextids = array_merge(
array_slice($contextids, 0, $idx+1), array_slice($contextids, 0, $idx+1),
array_reverse(array_slice($refpath,0,$i)), array_reverse(array_slice($ipath,0,$i)),
array_slice($contextids, $idx+1, count($contextids) - 1) array_slice($contextids, $idx+1, count($contextids) - 1)
) ; ) ;
@ -531,26 +638,24 @@ class courseservice extends \external_api {
} }
} }
if(!$found) { if(!$found) {
array_unshift($contextids,$refctxid); array_unshift($contextids,$ictx->id);
}
} catch(\dml_missing_record_exception $x) {
// ignore context
} }
} }
// we only have to check these contexts for access permissions
// Now translate this to the list of categories.
foreach ($contextids as $ctxid ) { foreach ($contextids as $ctxid ) {
try { try {
$ctx = \context::instance_by_id($ctxid); $ctx = \context::instance_by_id($ctxid);
if (has_capability($capability, $ctx)) {
if ($ctx->contextlevel == CONTEXT_SYSTEM) { if ($ctx->contextlevel == CONTEXT_SYSTEM) {
$cat = \core_course_category::top(); $cat = \core_course_category::top();
} else if ($ctx->contextlevel == CONTEXT_COURSECAT) { } else if ($ctx->contextlevel == CONTEXT_COURSECAT) {
$cat = \core_course_category::get($ctx->instanceid,\MUST_EXIST,true); $cat = \core_course_category::get($ctx->instanceid,\MUST_EXIST,false);
} }
$cats[] = $cat; $cats[] = $cat;
if ($operation == "view" && $ctxid == $refctxid) { // In edit mode, also include direct children of the currently selected context.
// Include direct children for navigation purposes if ($operation == "edit" && $ctxid == $refctxid) {
// Include direct children for navigation purposes.
foreach ($cat->get_children() as $ccat) { foreach ($cat->get_children() as $ccat) {
$ccatctx = \context_coursecat::instance($ccat->id); $ccatctx = \context_coursecat::instance($ccat->id);
if (!in_array($ccatctx->id,$contextids)) { if (!in_array($ccatctx->id,$contextids)) {
@ -558,7 +663,6 @@ class courseservice extends \external_api {
} }
} }
} }
}
} catch (\dml_missing_record_exception $x) { } catch (\dml_missing_record_exception $x) {
// ignore context // ignore context
} }

View File

@ -58,7 +58,6 @@ if ($categoryid > 0) {
exit; exit;
} }
require_capability('local/treestudyplan:editstudyplan', $studyplancontext);
$ci = new contextinfo($studyplancontext); $ci = new contextinfo($studyplancontext);
$contextname = $ci->pathstr(); $contextname = $ci->pathstr();
@ -70,6 +69,21 @@ $PAGE->set_heading($contextname);
if ($studyplancontext->id > 1) { if ($studyplancontext->id > 1) {
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ])); navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
$PAGE->navbar->add(get_string('cfg_plans', 'local_treestudyplan')); $PAGE->navbar->add(get_string('cfg_plans', 'local_treestudyplan'));
// Coursecat context
$cat = \core_course_category::get($studyplancontext->instanceid,IGNORE_MISSING,true); // We checck visibility later
} else {
// System context
$cat = \core_course_category::top();
}
if (!$cat->is_uservisible()) {
throw new \moodle_exception("error:cannotviewcategory","local_treestudyplan","/local/treestudyplan/edit_plan.php",$contextname,print_r($cat,true));
}
if (!has_capability('local/treestudyplan:editstudyplan', $studyplancontext)) {
throw new \moodle_exception("error:nostudyplaneditaccess","local_treestudyplan","/local/treestudyplan/edit_plan.php",$contextname);
} }
// Load javascripts and specific css. // Load javascripts and specific css.

View File

@ -429,3 +429,8 @@ $string["warning_incomplete_nograderq"] = 'Because the grade is not marked as a
$string["error:nosuchcompetency"] = 'Warning: This competency no longer exists'; $string["error:nosuchcompetency"] = 'Warning: This competency no longer exists';
$string["individuals"] = 'Individuals'; $string["individuals"] = 'Individuals';
$string["error:cannotviewcategory"] = 'Error: You do not have access to view this category or context: {$a}';
$string["error:nostudyplanviewaccess"] = 'Error: You do not have access to view study plans in this category or context: {$a}';
$string["error:nostudyplaneditaccess"] = 'Error: You do not have access to manage study plans in this category or context: {$a}';
$string["error:nocategoriesvisible"] = 'Error: You have no viewing permissions in any category. Therefore the course list remains empty.';

View File

@ -430,3 +430,7 @@ $string["warning_incomplete_nograderq"] = 'Omdat het behalen van een cijfer niet
$string["error:nosuchcompetency"] = 'Waarschuwing: deze competentie is niet langer beschikbaar. '; $string["error:nosuchcompetency"] = 'Waarschuwing: deze competentie is niet langer beschikbaar. ';
$string["individuals"] = 'Individueel'; $string["individuals"] = 'Individueel';
$string["error:cannotviewcategory"] = 'Fout: Je hebt geen rechten om deze category of context te bekijken: {$a}';
$string["error:nostudyplanviewaccess"] = 'Fout: Je hebt geen rechten om studieplannen in deze categorie of context te bekijken: {$a}';
$string["error:nostudyplaneditaccess"] = 'Fout: Je hebt geen rechten om studieplannen in deze categorie of context te beheren: {$a}';
$string["error:nocategoriesvisible"] = 'Fout: Je kunt geen cursussen in een categorie bekijken. Daarom blijft de cursuslijst leeg';

View File

@ -22,7 +22,7 @@
defined('MOODLE_INTERNAL') || die(); defined('MOODLE_INTERNAL') || die();
$plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494). $plugin->component = 'local_treestudyplan'; // Recommended since 2.0.2 (MDL-26035). Required since 3.0 (MDL-48494).
$plugin->version = 2024020400; // YYYYMMDDHH (year, month, day, iteration). $plugin->version = 2024020502; // YYYYMMDDHH (year, month, day, iteration).
$plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11). $plugin->requires = 2021051700; // YYYYMMDDHH (This is the release version for Moodle 3.11).
$plugin->release = "1.1.0"; $plugin->release = "1.1.0";

View File

@ -22,6 +22,7 @@
require_once("../../config.php"); require_once("../../config.php");
use local_treestudyplan\contextinfo;
use \local_treestudyplan\courseservice; use \local_treestudyplan\courseservice;
require_once($CFG->libdir.'/weblib.php'); require_once($CFG->libdir.'/weblib.php');
@ -60,8 +61,11 @@ if ($categoryid > 0) {
exit; exit;
} }
require_capability('local/treestudyplan:viewuserreports', $studyplancontext); $ci = new contextinfo($studyplancontext);
$contextname = $studyplancontext->get_context_name(false, false); $contextname = $ci->pathstr();
$PAGE->set_pagelayout('base'); $PAGE->set_pagelayout('base');
//$PAGE->set_context($studyplancontext); //$PAGE->set_context($studyplancontext);
@ -71,6 +75,20 @@ $PAGE->set_heading(get_string('view_plan', 'local_treestudyplan')." - ".$context
if ($studyplancontext->id > 1) { if ($studyplancontext->id > 1) {
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ])); navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
$PAGE->navbar->add(get_string('view_plan', 'local_treestudyplan')); $PAGE->navbar->add(get_string('view_plan', 'local_treestudyplan'));
// Coursecat context
$cat = \core_course_category::get($studyplancontext->instanceid,IGNORE_MISSING,true); // We checck visibility later
} else {
// System context
$cat = \core_course_category::top();
}
if (!$cat->is_uservisible()) {
throw new \moodle_exception("error:cannotviewcategory","local_treestudyplan","/local/treestudyplan/view_plan.php",$contextname);
}
if(!has_capability('local/treestudyplan:viewuserreports', $studyplancontext)) {
throw new \moodle_exception("error:nostudyplanviewaccess","local_treestudyplan","/local/treestudyplan/view_plan.php",$contextname);
} }
// Load javascripts and specific css. // Load javascripts and specific css.