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:
parent
8aed72af70
commit
dfc9f86d1d
2
amd/build/vue/vue.min.js
vendored
2
amd/build/vue/vue.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
11907
amd/src/vue/vue.js
11907
amd/src/vue/vue.js
File diff suppressed because one or more lines are too long
|
@ -31,6 +31,7 @@ use \local_treestudyplan\local\helpers\webservicehelper;
|
|||
use \local_treestudyplan\completionscanner;
|
||||
use \local_treestudyplan\gradingscanner;
|
||||
use \core_course_category;
|
||||
use moodle_exception;
|
||||
|
||||
/**
|
||||
* Webservice related to courses
|
||||
|
@ -61,28 +62,120 @@ class courseservice extends \external_api {
|
|||
}
|
||||
$tops = [];
|
||||
|
||||
/*
|
||||
SELECT UNIQUE ctx.* FROM mdl_context AS ctx
|
||||
INNER JOIN mdl_role_assignments AS ra ON ra.contextid = ctx.id
|
||||
INNER JOIN mdl_role_capabilities AS rc ON ra.roleid = rc.roleid
|
||||
LEFT JOIN mdl_course_categories AS cat ON ctx.instanceid = cat.id
|
||||
WHERE ( ctx.contextlevel = 40 OR ctx.contextlevel = 10 )
|
||||
AND ra.userid = 58 AND rc.capability = 'moodle/category:viewcourselist'
|
||||
ORDER BY ctx.depth ASC, cat.sortorder ASC;
|
||||
*/
|
||||
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
|
||||
INNER JOIN mdl_role_assignments AS ra ON ra.contextid = ctx.id
|
||||
INNER JOIN mdl_role_capabilities AS rc ON ra.roleid = rc.roleid
|
||||
LEFT JOIN mdl_course_categories AS cat ON ctx.instanceid = cat.id
|
||||
WHERE ( ctx.contextlevel = 40 OR ctx.contextlevel = 10 )
|
||||
AND ra.userid = 58 AND rc.capability = 'moodle/category:viewcourselist'
|
||||
ORDER BY ctx.depth ASC, cat.sortorder ASC;
|
||||
*/
|
||||
//$capability = 'moodle/category:viewcourselist';
|
||||
|
||||
$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
|
||||
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,]);
|
||||
|
||||
$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.
|
||||
// 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);
|
||||
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.
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$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 OR ctx.contextlevel = :ctxl_system )
|
||||
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, "ctxl_system" => \CONTEXT_SYSTEM,]);
|
||||
$recordset = $DB->get_recordset_sql($sql, ["userid" => $userid,
|
||||
"capability" => $capability,
|
||||
"ctxl_coursecat" => \CONTEXT_COURSECAT,
|
||||
"pathsearch" => "%/{$parentid}/%",
|
||||
]);
|
||||
|
||||
$contextids = [];
|
||||
foreach ($recordset as $r) {
|
||||
|
@ -98,40 +191,23 @@ class courseservice extends \external_api {
|
|||
// Double check permissions according to the moodle capability system.
|
||||
$ctx = \context::instance_by_id($r->id);
|
||||
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.
|
||||
// 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();
|
||||
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.
|
||||
// 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($r->instanceid, \IGNORE_MISSING, false, $userid);
|
||||
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.
|
||||
array_push($tops,$cat);
|
||||
}
|
||||
}
|
||||
}
|
||||
// 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($r->instanceid, \IGNORE_MISSING, false, $userid);
|
||||
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.
|
||||
array_push($tops,$cat);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
$recordset->close();
|
||||
return $tops;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return value description for map_categories function
|
||||
*/
|
||||
|
@ -179,18 +255,26 @@ class courseservice extends \external_api {
|
|||
public static function map_categories($rootid = 0) {
|
||||
global $USER;
|
||||
|
||||
$root = \core_course_category::get($rootid,\MUST_EXIST,true);
|
||||
|
||||
// Determine top categories from provided context.
|
||||
|
||||
if ($root->id == 0) {
|
||||
if ($rootid == 0) {
|
||||
// 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.
|
||||
$children = self::user_tops($USER->id);
|
||||
} else if ($root->is_uservisible()) {
|
||||
$children = [$root];
|
||||
$children = self::user_tops();
|
||||
if (count($children) == 0) {
|
||||
throw new moodle_exception("error:nocategoriesvisible","local_treestudyplan");
|
||||
}
|
||||
} else {
|
||||
return []; // Category not user visible.
|
||||
|
||||
$root = \core_course_category::get($rootid,\MUST_EXIST,true);
|
||||
if ($root->is_uservisible()) {
|
||||
$children = [$root];
|
||||
} else {
|
||||
$ci = new contextinfo($root->get_context());
|
||||
$contextname = $ci->pathstr();
|
||||
throw new moodle_exception("error:cannotviewcategory","local_treestudyplan",'',$contextname);
|
||||
}
|
||||
}
|
||||
|
||||
$list = [];
|
||||
|
@ -493,69 +577,89 @@ class courseservice extends \external_api {
|
|||
} else { // Operation == "view" || default.
|
||||
$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 = [];
|
||||
$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}
|
||||
GROUP BY context_id");
|
||||
foreach ($rs as $r) {
|
||||
// Build the counts.
|
||||
$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();
|
||||
|
||||
// Add system context to list if needed.
|
||||
if (!in_array(1,$contextids)) {
|
||||
array_unshift($contextids,1);
|
||||
}
|
||||
|
||||
$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)) {
|
||||
try {
|
||||
// Get the context.
|
||||
$refctx = \context::instance_by_id($refctxid);
|
||||
$refpath = $refctx->get_parent_context_ids(true);
|
||||
$found = false;
|
||||
foreach ($refpath as $i => $pid) {
|
||||
$idx = array_search($pid,$contextids);
|
||||
if($idx !== false) {
|
||||
|
||||
$contextids = array_merge(
|
||||
array_slice($contextids, 0, $idx+1),
|
||||
array_reverse(array_slice($refpath,0,$i)),
|
||||
array_slice($contextids, $idx+1, count($contextids) - 1)
|
||||
) ;
|
||||
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!$found) {
|
||||
array_unshift($contextids,$refctxid);
|
||||
// Double check permissions.
|
||||
if (has_capability($capability,$refctx)) {
|
||||
$insertctxs[] = $refctx;
|
||||
}
|
||||
} catch(\dml_missing_record_exception $x) {
|
||||
// ignore context
|
||||
}
|
||||
}
|
||||
|
||||
// we only have to check these contexts for access permissions
|
||||
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;
|
||||
foreach ($ipath as $i => $pid) {
|
||||
$idx = array_search($pid,$contextids);
|
||||
if($idx !== false) {
|
||||
|
||||
$contextids = array_merge(
|
||||
array_slice($contextids, 0, $idx+1),
|
||||
array_reverse(array_slice($ipath,0,$i)),
|
||||
array_slice($contextids, $idx+1, count($contextids) - 1)
|
||||
) ;
|
||||
|
||||
$found = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if(!$found) {
|
||||
array_unshift($contextids,$ictx->id);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Now translate this to the list of categories.
|
||||
foreach ($contextids as $ctxid ) {
|
||||
try {
|
||||
$ctx = \context::instance_by_id($ctxid);
|
||||
if (has_capability($capability, $ctx)) {
|
||||
if ($ctx->contextlevel == CONTEXT_SYSTEM) {
|
||||
$cat = \core_course_category::top();
|
||||
} else if ($ctx->contextlevel == CONTEXT_COURSECAT) {
|
||||
$cat = \core_course_category::get($ctx->instanceid,\MUST_EXIST,true);
|
||||
}
|
||||
$cats[] = $cat;
|
||||
if ($operation == "view" && $ctxid == $refctxid) {
|
||||
// Include direct children for navigation purposes
|
||||
foreach ($cat->get_children() as $ccat) {
|
||||
$ccatctx = \context_coursecat::instance($ccat->id);
|
||||
if (!in_array($ccatctx->id,$contextids)) {
|
||||
$cats[] = $ccat;
|
||||
}
|
||||
if ($ctx->contextlevel == CONTEXT_SYSTEM) {
|
||||
$cat = \core_course_category::top();
|
||||
} else if ($ctx->contextlevel == CONTEXT_COURSECAT) {
|
||||
$cat = \core_course_category::get($ctx->instanceid,\MUST_EXIST,false);
|
||||
}
|
||||
$cats[] = $cat;
|
||||
// In edit mode, also include direct children of the currently selected context.
|
||||
if ($operation == "edit" && $ctxid == $refctxid) {
|
||||
// Include direct children for navigation purposes.
|
||||
foreach ($cat->get_children() as $ccat) {
|
||||
$ccatctx = \context_coursecat::instance($ccat->id);
|
||||
if (!in_array($ccatctx->id,$contextids)) {
|
||||
$cats[] = $ccat;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -58,7 +58,6 @@ if ($categoryid > 0) {
|
|||
exit;
|
||||
}
|
||||
|
||||
require_capability('local/treestudyplan:editstudyplan', $studyplancontext);
|
||||
$ci = new contextinfo($studyplancontext);
|
||||
$contextname = $ci->pathstr();
|
||||
|
||||
|
@ -70,6 +69,21 @@ $PAGE->set_heading($contextname);
|
|||
if ($studyplancontext->id > 1) {
|
||||
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
|
||||
$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.
|
||||
|
|
|
@ -428,4 +428,9 @@ $string["warning_incomplete_pass"] = 'Completion data was reset for this activit
|
|||
$string["warning_incomplete_nograderq"] = 'Because the grade is not marked as a requirement, your passing grade is not registering completion.';
|
||||
|
||||
$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.';
|
|
@ -429,4 +429,8 @@ $string["warning_incomplete_pass"] = 'Door een storing wordt je voldoende result
|
|||
$string["warning_incomplete_nograderq"] = 'Omdat het behalen van een cijfer niet als voorwaarde is aangegeven, telt het behalen van een voldoende resultaat niet mee voor voitooiing';
|
||||
|
||||
$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';
|
|
@ -22,7 +22,7 @@
|
|||
defined('MOODLE_INTERNAL') || die();
|
||||
|
||||
$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->release = "1.1.0";
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
|
||||
require_once("../../config.php");
|
||||
|
||||
use local_treestudyplan\contextinfo;
|
||||
use \local_treestudyplan\courseservice;
|
||||
|
||||
require_once($CFG->libdir.'/weblib.php');
|
||||
|
@ -60,8 +61,11 @@ if ($categoryid > 0) {
|
|||
exit;
|
||||
}
|
||||
|
||||
require_capability('local/treestudyplan:viewuserreports', $studyplancontext);
|
||||
$contextname = $studyplancontext->get_context_name(false, false);
|
||||
$ci = new contextinfo($studyplancontext);
|
||||
$contextname = $ci->pathstr();
|
||||
|
||||
|
||||
|
||||
|
||||
$PAGE->set_pagelayout('base');
|
||||
//$PAGE->set_context($studyplancontext);
|
||||
|
@ -71,6 +75,20 @@ $PAGE->set_heading(get_string('view_plan', 'local_treestudyplan')." - ".$context
|
|||
if ($studyplancontext->id > 1) {
|
||||
navigation_node::override_active_url(new moodle_url('/course/index.php', ['categoryid' => $categoryid ]));
|
||||
$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.
|
||||
|
|
Loading…
Reference in New Issue
Block a user