From 74ac5b66cbfb458524fa9b7db2454f00091cde23 Mon Sep 17 00:00:00 2001 From: martinlanghoff Date: Wed, 19 Sep 2007 07:02:44 +0000 Subject: [PATCH] accesslib: has_capability() now works for the logged in user With this commit, the new has_capability works for the logged in user correctly. - load_all_capabilities() no longer calls load_user_capability()! 6K dbqueries less on login... ;-) - When delving into a context we haven't loaded yet, we call get_user_access_bycontext() - Introduce: get_user_access_bycontext() - Several fixes in get_user_access_sitewide() (renamed from get_user_sitewide_access()) - Fixes in has_cap_fromsess() - Introduce access_insess() to check if we have to call get_user_access_bycontext() for the context --- lib/accesslib.php | 254 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 233 insertions(+), 21 deletions(-) diff --git a/lib/accesslib.php b/lib/accesslib.php index 3a355cf3c0..71fb0250b6 100755 --- a/lib/accesslib.php +++ b/lib/accesslib.php @@ -358,27 +358,52 @@ function has_capability($capability, $context=NULL, $userid=NULL, $doanything=tr $context = $CONTEXT; } } + if (empty($CONTEXT)) { + $CONTEXT = $context; + } if (is_null($userid) || $userid===0) { $userid = $USER->id; } + //error_log(print_r($context,1)); $contexts = array(); - if ($context->path === '') { - $contexts(SITECONTEXTID, $context->id); + if (empty($context->path)) { + $contexts[] = SYSCONTEXTID; + $context->path = '/' . SYSCONTEXTID; + if (isset($context->id) && $context->id ==! SYSCONTEXTID) { + $contexts[] = $context->id; + $context->path .= '/' . $context->id; + } } else { $contexts = explode('/', $context->path); array_shift($contexts); } if ($USER->id === $userid) { + // + // For the logged in user, we have $USER->access + // which will have all RAs and caps preloaded for + // course and above contexts. + // + // Contexts below courses && contexts that do not + // hang from courses are loaded into $USER->access + // on demand, and listed in $USER->access[loaded] + // if ($context->contextlevel <= CONTEXT_COURSE) { // Course and above are always preloaded return has_cap_fromsess($capability, $context, $USER->access, $doanything); } - ///$coursepath = get_course_from_path($context->path); - /// if ($USER->access) { - /// $USER->access['courses'] = get_course_access($context, $userid); + // Load it as needed + if (!access_insess($context->path,$USER->access)) { + error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}"); + // $bt = debug_backtrace(); + // error_log("bt {$bt[0]['file']} {$bt[0]['line']}"); + $USER->access = get_user_access_bycontext($USER->id, $context, + $USER->access); + } + return has_cap_fromsess($capability, $context, $USER->access, $doanything); + } error_log("not implemented $userid $capability {$context->contextlevel} {$context->path} "); @@ -403,6 +428,27 @@ function get_course_from_path ($path) { return false; } +function access_insess($path, $sess) { + + // assume that contexts hang from sys or from a course + // this will only work well with stuff that hangs from a course + if (in_array($path, $sess['loaded'], true)) { + error_log("found it!"); + return true; + } + $base = '/' . SYSCONTEXTID; + while (preg_match('!^(/.+)/\d+$!', $path, $matches)) { + $path = $matches[1]; + if ($path === $base) { + return false; + } + if (in_array($path, $sess['loaded'], true)) { + return true; + } + } + return false; +} + function has_cap_fromsess($capability, $context, $sess, $doanything) { $path = $context->path; @@ -420,17 +466,15 @@ function has_cap_fromsess($capability, $context, $sess, $doanything) { // From the bottom up... for ($n=$cc-1;$n>=0;$n--) { $ctxp = $contexts[$n]; - if (isset($USER->access['ra'][$ctxp])) { + if (isset($sess['ra'][$ctxp])) { // Found a role assignment $roleid = $sess['ra'][$ctxp]; - error_log("found ra $roleid for $ctxp"); // Walk the path for capabilities // from the bottom up... for ($m=$cc-1;$m>=0;$m--) { $capctxp = $contexts[$m]; if (isset($sess['rdef']["{$capctxp}:$roleid"][$capability])) { $perm = $sess['rdef']["{$capctxp}:$roleid"][$capability]; - error_log("found rc for $roleid for $ctxp in {$capctxp}:$roleid $capability $perm"); if ($perm === CAP_PROHIBIT) { return false; } else { @@ -1277,7 +1321,7 @@ function load_user_capability($capability='', $context=NULL, $userid=NULL, $chec } /** - * It will return 2 arrays showing role assignments + * It will return a nested array showing role assignments * all relevant role capabilities for the user at * site/metacourse/course_category/course levels * @@ -1286,11 +1330,12 @@ function load_user_capability($capability='', $context=NULL, $userid=NULL, $chec * * [ra] => [/path/] = roleid * [rdef] => [/path/:roleid][capability]=permission + * [loaded] => array('/path', '/path') * * @param $userid integer - the id of the user * */ -function get_user_sitewide_access($userid) { +function get_user_access_sitewide($userid) { global $CFG; @@ -1307,9 +1352,10 @@ function get_user_sitewide_access($userid) { * - below this user's RAs - limited to course level */ - $sw = array(); // named list - $sw['ra'] = array(); - $sw['rdef'] = array(); + $acc = array(); // named list + $acc['ra'] = array(); + $acc['rdef'] = array(); + $acc['loaded'] = array(); $sitectx = get_field('context', 'id','contextlevel', CONTEXT_SYSTEM); $base = "/$sitectx"; @@ -1334,10 +1380,10 @@ function get_user_sitewide_access($userid) { $raparents = array(); if ($rs->RecordCount()) { while ($ra = rs_fetch_next_record($rs)) { - $sw['ra'][$ra->path] = $ra->roleid; + $acc['ra'][$ra->path] = $ra->roleid; if (!empty($ra->capability)) { $k = "{$ra->path}:{$ra->roleid}"; - $sw['rdef'][$k][$ra->capability] = $ra->permission; + $acc['rdef'][$k][$ra->capability] = $ra->permission; } $parentids = explode('/', $ra->path); array_pop($parentids); array_shift($parentids); @@ -1347,6 +1393,7 @@ function get_user_sitewide_access($userid) { $raparents[$ra->roleid] = $parentids; } } + unset($ra); } rs_close($rs); @@ -1376,8 +1423,9 @@ function get_user_sitewide_access($userid) { if ($rs->RecordCount()) { while ($rd = rs_fetch_next_record($rs)) { $k = "{$rd->path}:{$rd->roleid}"; - $sw['rdef'][$k][$rd->capability] = $rd->permission; + $acc['rdef'][$k][$rd->capability] = $rd->permission; } + unset($rd); } rs_close($rs); @@ -1401,20 +1449,184 @@ function get_user_sitewide_access($userid) { ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id) WHERE ra.userid = $userid AND sctx.contextlevel <= ".CONTEXT_COURSE." - ORDER BY sctx.depth, sctx.path, $ra.roleid"; + ORDER BY sctx.depth, sctx.path, ra.roleid"; if ($rs->RecordCount()) { while ($rd = rs_fetch_next_record($rs)) { $k = "{$rd->path}:{$rd->roleid}"; - $sw['rdef'][$k][$rd->capability] = $rd->permission; + $acc['rdef'][$k][$rd->capability] = $rd->permission; } + unset($rd); } rs_close($rs); // TODO: compact capsets? - return $sw; + return $acc; } +/** + * It add to the access ctrl array the data + * needed for a given context + * + * @param $userid integer - the id of the user + * @param $context context obj - needs path! + * @param $acc access array + * + */ +function get_user_access_bycontext($userid, $context, $acc=NULL) { + + global $CFG; + + /* Get in 3 cheap DB queries... + * - role assignments - with role_caps + * - relevant role caps + * - above this user's RAs + * - below this user's RAs - limited to course level + */ + + if (is_null($acc)) { + $acc = array(); // named list + $acc['ra'] = array(); + $acc['rdef'] = array(); + $acc['loaded'] = array(); + } + + $base = "/" . SYSCONTEXTID; + + // Determine the course context we'll go + // after, though we are usually called + // with a lower ctx. We have 3 easy cases + // + // - Course + // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM + // - BLOCK/MODULE/GROUP hanging from a course + // + // For course contexts, we _already_ have the RAs + // but the cost of re-fetching is minimal so we don't care. + // ... for now! + // + $targetpath; + $targetlevel; + if ($context->contextlevel === CONTEXT_COURSE) { + $targetpath = $context->path; + $targetlevel = $context->contextlevel; + } elseif ($context->path === "$base/{$context->id}") { + $targetpath = $context->path; + $targetlevel = $context->contextlevel; + } else { + // Assumption: the course _must_ be our parent + // If we ever see stuff nested further this needs to + // change to do 1 query over the exploded path to + // find out which one is the course + $targetpath = get_course_from_path($context->path); + $targetlevel = CONTEXT_COURSE; + } + + // + // Role assignments in the context and below - and any rolecaps directly linked + // because it's cheap to read rolecaps here over many + // RAs + // + $sql = "SELECT ctx.path, ra.roleid, rc.capability, rc.permission + FROM {$CFG->prefix}role_assignments ra + JOIN {$CFG->prefix}context ctx + ON ra.contextid=ctx.id + LEFT OUTER JOIN {$CFG->prefix}role_capabilities rc + ON (rc.roleid=ra.roleid AND rc.contextid=ra.contextid) + WHERE ra.userid = $userid + AND (ctx.path = '$targetpath' OR ctx.path LIKE '{$targetpath}/%') + ORDER BY ctx.depth, ctx.path"; + $rs = get_recordset_sql($sql); + + // parent paths & roles we need to walk up + // this array will bulk up quite a bit with dups + // which we'll later clear up + $raparents = array(); + if ($rs->RecordCount()) { + while ($ra = rs_fetch_next_record($rs)) { + $acc['ra'][$ra->path] = $ra->roleid; + if (!empty($ra->capability)) { + $k = "{$ra->path}:{$ra->roleid}"; + $acc['rdef'][$k][$ra->capability] = $ra->permission; + } + $parentids = explode('/', $ra->path); + array_pop($parentids); array_shift($parentids); + if (isset($raparents[$ra->roleid])) { + $raparents[$ra->roleid] = array_merge($raparents[$ra->roleid], $parentids); + } else { + $raparents[$ra->roleid] = $parentids; + } + } + } + rs_close($rs); + + // Walk up the tree to grab all the roledefs + // of interest to our user... + // NOTE: we use a series of IN clauses here - which + // might explode on huge sites with very convoluted nesting of + // categories... - extremely unlikely that the number of categories + // and roletypes is so large that we hit the limits of IN() + if (count($raparents)) { + $clauses = array(); + foreach ($raparents as $roleid=>$contexts) { + $contexts = sql_intarray_to_in(array_unique($contexts)); + if ($contexts ==! '') { + $clauses[] = "(roleid=$roleid AND contextid IN ($contexts))"; + } + } + $clauses = join(" OR ", $clauses); + $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission + FROM {$CFG->prefix}role_capabilities rc + JOIN {$CFG->prefix}context ctx + ON rc.contextid=ctx.id + WHERE $clauses + ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC "; + + $rs = get_recordset_sql($sql); + + if ($rs->RecordCount()) { + while ($rd = rs_fetch_next_record($rs)) { + $k = "{$rd->path}:{$rd->roleid}"; + $acc['rdef'][$k][$rd->capability] = $rd->permission; + } + } + rs_close($rs); + } + + // + // Overrides for the role assignments IN SUBCONTEXTS + // + // NOTE that the JOIN w sctx is with 3-way triangulation to + // catch overrides to the applicable role in any subcontext, based + // on the path field of the parent. + // + $sql = "SELECT sctx.path, ra.roleid, + ctx.path AS parentpath, + rco.capability, rco.permission + FROM {$CFG->prefix}role_assignments ra + JOIN {$CFG->prefix}context ctx + ON ra.contextid=ctx.id + JOIN {$CFG->prefix}context sctx + ON (sctx.path LIKE ctx.path||'/%') + JOIN {$CFG->prefix}role_capabilities rco + ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id) + WHERE ra.userid = $userid AND + ORDER BY sctx.depth, sctx.path, ra.roleid"; + if ($rs->RecordCount()) { + while ($rd = rs_fetch_next_record($rs)) { + $k = "{$rd->path}:{$rd->roleid}"; + $acc['rdef'][$k][$rd->capability] = $rd->permission; + } + } + rs_close($rs); + + // TODO: compact capsets? + + error_log("loaded $targetpath"); + $acc['loaded'][] = $targetpath; + + return $acc; +} /** * A convenience function to completely load all the capabilities @@ -1436,8 +1648,8 @@ function load_all_capabilities() { $defcaps = load_defaultuser_role(true); } - load_user_capability(); - $USER->access=get_user_sitewide_access($USER->id); + //load_user_capability(); + $USER->access=get_user_access_sitewide($USER->id); // when in "course login as" - load only course caqpabilitites (it may not always work as expected) if (!empty($USER->realuser) and $USER->loginascontext->contextlevel != CONTEXT_SYSTEM) { -- 2.39.5