/** * Check whether a user has a particular capability in a given context. * * For example: * $context = get_context_instance(CONTEXT_MODULE, $cm->id); * has_capability('mod/forum:replypost',$context) * * By default checks the capabilities of the current user, but you can pass a * different userid. By default will return true for admin users, but you can override that with the fourth argument. * * Guest and not-logged-in users can never get any dangerous capability - that is any write capability * or capabilities with XSS, config or data loss risks. * * @param string $capability the name of the capability to check. For example mod/forum:view * @param context $context the context to check the capability in. You normally get this with {@link get_context_instance}. * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user. * @param boolean $doanything If false, ignores effect of admin role assignment * @return boolean true if the user has this capability. Otherwise false. */ function has_capability($capability, context $context, $user = null, $doanything = true) { global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE; if (during_initial_install()) { if ($SCRIPT === "/{$CFG->admin}/index.php" or $SCRIPT === "/{$CFG->admin}/cli/install.php" or $SCRIPT === "/{$CFG->admin}/cli/install_database.php") { // we are in an installer - roles can not work yet return true; } else { return false; } } if (strpos($capability, 'moodle/legacy:') === 0) { throw new coding_exception('Legacy capabilities can not be used any more!'); } if (!is_bool($doanything)) { throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.'); } // capability must exist if (!($capinfo = get_capability_info($capability))) { debugging('Capability "' . $capability . '" was not found! This has to be fixed in code.'); return false; } if (!isset($USER->id)) { // should never happen $USER->id = 0; } // make sure there is a real user specified if ($user === null) { $userid = $USER->id; } else { $userid = is_object($user) ? $user->id : $user; } // make sure forcelogin cuts off not-logged-in users if enabled if (!empty($CFG->forcelogin) and $userid == 0) { return false; } // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are. if ($capinfo->captype === 'write' or $capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) { if (isguestuser($userid) or $userid == 0) { return false; } } // somehow make sure the user is not deleted and actually exists if ($userid != 0) { if ($userid == $USER->id and isset($USER->deleted)) { // this prevents one query per page, it is a bit of cheating, // but hopefully session is terminated properly once user is deleted if ($USER->deleted) { return false; } } else { if (!context_user::instance($userid, IGNORE_MISSING)) { // no user context == invalid userid return false; } } } // context path/depth must be valid if (empty($context->path) or $context->depth == 0) { // this should not happen often, each upgrade tries to rebuild the context paths debugging('Context id ' . $context->id . ' does not have valid path, please use build_context_path()'); if (is_siteadmin($userid)) { return true; } else { return false; } } // Find out if user is admin - it is not possible to override the doanything in any way // and it is not possible to switch to admin role either. if ($doanything) { if (is_siteadmin($userid)) { if ($userid != $USER->id) { return true; } // make sure switchrole is not used in this context if (empty($USER->access['rsw'])) { return true; } $parts = explode('/', trim($context->path, '/')); $path = ''; $switched = false; foreach ($parts as $part) { $path .= '/' . $part; if (!empty($USER->access['rsw'][$path])) { $switched = true; break; } } if (!$switched) { return true; } //ok, admin switched role in this context, let's use normal access control rules } } // Careful check for staleness... $context->reload_if_dirty(); if ($USER->id == $userid) { if (!isset($USER->access)) { load_all_capabilities(); } $access =& $USER->access; } else { // make sure user accessdata is really loaded get_user_accessdata($userid, true); $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]; } // Load accessdata for below-the-course context if necessary, // all contexts at and above all courses are already loaded if ($context->contextlevel != CONTEXT_COURSE and $coursecontext = $context->get_course_context(false)) { load_course_context($userid, $coursecontext, $access); } return has_capability_in_accessdata($capability, $context, $access); }
/** * Check whether a user has a particular capability in a given context. * * For example:: * $context = get_context_instance(CONTEXT_MODULE, $cm->id); * has_capability('mod/forum:replypost',$context) * * By default checks the capabilities of the current user, but you can pass a * different userid. By default will return true for admin users, but you can override that with the fourth argument. * * Guest and not-logged-in users can never get any dangerous capability - that is any write capability * or capabilities with XSS, config or data loss risks. * * @param string $capability the name of the capability to check. For example mod/forum:view * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}. * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user. * @param boolean $doanything If false, ignores effect of admin role assignment * @return boolean true if the user has this capability. Otherwise false. */ function has_capability($capability, $context, $user = null, $doanything = true) { global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE; if (during_initial_install()) { if ($SCRIPT === "/{$CFG->admin}/index.php" or $SCRIPT === "/{$CFG->admin}/cliupgrade.php") { // we are in an installer - roles can not work yet return true; } else { return false; } } if (strpos($capability, 'moodle/legacy:') === 0) { throw new coding_exception('Legacy capabilities can not be used any more!'); } // the original $CONTEXT here was hiding serious errors // for security reasons do not reuse previous context if (empty($context)) { debugging('Incorrect context specified'); return false; } if (!is_bool($doanything)) { throw new coding_exception('Capability parameter "doanything" is wierd ("' . $doanything . '"). This has to be fixed in code.'); } // make sure there is a real user specified if ($user === null) { $userid = isset($USER->id) ? $USER->id : 0; } else { $userid = is_object($user) ? $user->id : $user; } // capability must exist if (!($capinfo = get_capability_info($capability))) { debugging('Capability "' . $capability . '" was not found! This should be fixed in code.'); return false; } // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are. if ($capinfo->captype === 'write' or (int) $capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS)) { if (isguestuser($userid) or $userid == 0) { return false; } } if (is_null($context->path) or $context->depth == 0) { //this should not happen $contexts = array(SYSCONTEXTID, $context->id); $context->path = '/' . SYSCONTEXTID . '/' . $context->id; debugging('Context id ' . $context->id . ' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER); } else { $contexts = explode('/', $context->path); array_shift($contexts); } if (CLI_SCRIPT && !isset($USER->access)) { // In cron, some modules setup a 'fake' $USER, // ensure we load the appropriate accessdata. if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { $ACCESSLIB_PRIVATE->dirtycontexts = null; //load fresh dirty contexts } else { load_user_accessdata($userid); $ACCESSLIB_PRIVATE->dirtycontexts = array(); } $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]; } else { if (isset($USER->id) && $USER->id == $userid && !isset($USER->access)) { // caps not loaded yet - better to load them to keep BC with 1.8 // not-logged-in user or $USER object set up manually first time here load_all_capabilities(); $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now $ACCESSLIB_PRIVATE->roledefinitions = array(); } } // Load dirty contexts list if needed if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) { if (isset($USER->access['time'])) { $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']); } else { $ACCESSLIB_PRIVATE->dirtycontexts = array(); } } // Careful check for staleness... if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) { // reload all capabilities - preserving loginas, roleswitches, etc // and then cleanup any marks of dirtyness... at least from our short // term memory! :-) $ACCESSLIB_PRIVATE->accessdatabyuser = array(); $ACCESSLIB_PRIVATE->roledefinitions = array(); if (CLI_SCRIPT) { load_user_accessdata($userid); $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]; $ACCESSLIB_PRIVATE->dirtycontexts = array(); } else { reload_all_capabilities(); } } // Find out if user is admin - it is not possible to override the doanything in any way // and it is not possible to switch to admin role either. if ($doanything) { if (is_siteadmin($userid)) { if ($userid != $USER->id) { return true; } // make sure switchrole is not used in this context if (empty($USER->access['rsw'])) { return true; } $parts = explode('/', trim($context->path, '/')); $path = ''; $switched = false; foreach ($parts as $part) { $path .= '/' . $part; if (!empty($USER->access['rsw'][$path])) { $switched = true; break; } } if (!$switched) { return true; } //ok, admin switched role in this context, let's use normal access control rules } } // divulge how many times we are called //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability"); if (isset($USER->id) && $USER->id == $userid) { // we must accept strings and integers in $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_capability_in_accessdata($capability, $context, $USER->access); } // Load accessdata for below-the-course contexts if (!path_inaccessdata($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']}"); load_subcontext($USER->id, $context, $USER->access); } return has_capability_in_accessdata($capability, $context, $USER->access); } if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { load_user_accessdata($userid); } if ($context->contextlevel <= CONTEXT_COURSE) { // Course and above are always preloaded return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); } // Load accessdata for below-the-course contexts as needed if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) { // 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']}"); load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); } return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]); }
/** * Get an array of courses (with magic extra bits) * where the accessdata and in DB enrolments show * that the cap requested is available. * * The main use is for get_my_courses(). * * Notes * * - $fields is an array of fieldnames to ADD * so name the fields you really need, which will * be added and uniq'd * * - the course records have $c->context which is a fully * valid context object. Saves you a query per course! * * - the course records have $c->categorypath to make * category lookups cheap * * - current implementation is split in - * * - if the user has the cap systemwide, stupidly * grab *every* course for a capcheck. This eats * a TON of bandwidth, specially on large sites * with separate DBs... * * - otherwise, fetch "likely" courses with a wide net * that should get us _cheaply_ at least the courses we need, and some * we won't - we get courses that... * - are in a category where user has the cap * - or where use has a role-assignment (any kind) * - or where the course has an override on for this cap * * - walk the courses recordset checking the caps oneach one * the checks are all in memory and quite fast * (though we could implement a specialised variant of the * has_capability_in_accessdata() code to speed it up) * * @param string $capability - name of the capability * @param array $accessdata - accessdata session array * @param bool $doanything - if false, ignore do anything * @param string $sort - sorting fields - prefix each fieldname with "c." * @param array $fields - additional fields you are interested in... * @param int $limit - set if you want to limit the number of courses * @return array $courses - ordered array of course objects - see notes above * */ function get_user_courses_bycap($userid, $cap, $accessdata, $doanything, $sort = 'c.sortorder ASC', $fields = NULL, $limit = 0) { global $CFG, $DB; // Slim base fields, let callers ask for what they need... $basefields = array('id', 'sortorder', 'shortname', 'idnumber'); if (!is_null($fields)) { $fields = array_merge($basefields, $fields); $fields = array_unique($fields); } else { $fields = $basefields; } // If any of the fields is '*', leave it alone, discarding the rest // to avoid ambiguous columns under some silly DBs. See MDL-18746 :-D if (in_array('*', $fields)) { $fields = array('*'); } $coursefields = 'c.' . implode(',c.', $fields); $sort = trim($sort); if ($sort !== '') { $sort = "ORDER BY {$sort}"; } $sysctx = get_context_instance(CONTEXT_SYSTEM); if (has_capability_in_accessdata($cap, $sysctx, $accessdata, $doanything)) { // // Apparently the user has the cap sitewide, so walk *every* course // (the cap checks are moderately fast, but this moves massive bandwidth w the db) // Yuck. // $sql = "SELECT {$coursefields},\n ctx.id AS ctxid, ctx.path AS ctxpath,\n ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,\n cc.path AS categorypath\n FROM {course} c\n JOIN {course_categories} cc\n ON c.category=cc.id\n JOIN {context} ctx\n ON (c.id=ctx.instanceid AND ctx.contextlevel=" . CONTEXT_COURSE . ")\n {$sort} "; $rs = $DB->get_recordset_sql($sql); } else { // // narrow down where we have the caps to a few contexts // this will be a combination of // - courses where user has an explicit enrolment // - courses that have an override (any status) on that capability // - categories where user has the rights (granted status) on that capability // $sql = "SELECT ctx.*\n FROM {context} ctx\n WHERE ctx.contextlevel=" . CONTEXT_COURSECAT . "\n ORDER BY ctx.depth"; $rs = $DB->get_recordset_sql($sql); $catpaths = array(); foreach ($rs as $catctx) { if ($catctx->path != '' && has_capability_in_accessdata($cap, $catctx, $accessdata, $doanything)) { $catpaths[] = $catctx->path; } } $rs->close(); $catclause = ''; $params = array(); if (count($catpaths)) { $cc = count($catpaths); for ($n = 0; $n < $cc; $n++) { $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'"; } $catclause = 'WHERE (' . implode(' OR ', $catpaths) . ')'; } unset($catpaths); $capany = ''; if ($doanything) { $capany = " OR rc.capability=:doany"; $params['doany'] = 'moodle/site:doanything'; } /// UNION 3 queries: /// - user role assignments in courses /// - user capability (override - any status) in courses /// - user right (granted status) in categories (optionally executed) /// Enclosing the 3-UNION into an inline_view to avoid column names conflict and making the ORDER BY cross-db /// and to allow selection of TEXT columns in the query (MSSQL and Oracle limitation). MDL-16209 $sql = "\n SELECT {$coursefields}, ctxid, ctxpath, ctxdepth, ctxlevel, categorypath\n FROM (\n SELECT c.id,\n ctx.id AS ctxid, ctx.path AS ctxpath,\n ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,\n cc.path AS categorypath\n FROM {course} c\n JOIN {course_categories} cc\n ON c.category=cc.id\n JOIN {context} ctx\n ON (c.id=ctx.instanceid AND ctx.contextlevel=" . CONTEXT_COURSE . ")\n JOIN {role_assignments} ra\n ON (ra.contextid=ctx.id AND ra.userid=:userid)\n UNION\n SELECT c.id,\n ctx.id AS ctxid, ctx.path AS ctxpath,\n ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,\n cc.path AS categorypath\n FROM {course} c\n JOIN {course_categories} cc\n ON c.category=cc.id\n JOIN {context} ctx\n ON (c.id=ctx.instanceid AND ctx.contextlevel=" . CONTEXT_COURSE . ")\n JOIN {role_capabilities} rc\n ON (rc.contextid=ctx.id AND (rc.capability=:cap {$capany})) "; if (!empty($catclause)) { /// If we have found the right in categories, add child courses here too $sql .= "\n UNION\n SELECT c.id,\n ctx.id AS ctxid, ctx.path AS ctxpath,\n ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,\n cc.path AS categorypath\n FROM {course} c\n JOIN {course_categories} cc\n ON c.category=cc.id\n JOIN {context} ctx\n ON (c.id=ctx.instanceid AND ctx.contextlevel=" . CONTEXT_COURSE . ")\n {$catclause}"; } /// Close the inline_view and join with courses table to get requested $coursefields $sql .= "\n ) inline_view\n INNER JOIN {course} c\n ON inline_view.id = c.id"; /// To keep cross-db we need to strip any prefix in the ORDER BY clause for queries using UNION $sql .= "\n " . preg_replace('/[a-z]+\\./i', '', $sort); /// Add ORDER BY clause $params['userid'] = $userid; $params['cap'] = $cap; $rs = $DB->get_recordset_sql($sql, $params); } /// Confirm rights (granted capability) for each course returned $courses = array(); $cc = 0; // keep count if ($rs) { foreach ($rs as $c) { // build the context obj $c = make_context_subobj($c); if (has_capability_in_accessdata($cap, $c->context, $accessdata, $doanything)) { $courses[] = $c; if ($limit > 0 && $cc++ > $limit) { break; } } } $rs->close(); } return $courses; }
function get_user_courses_bycap($userid, $cap, $accessdata, $doanything, $sort = 'c.sortorder ASC', $fields = NULL, $limit = 0) { global $CFG; // Slim base fields, let callers ask for what they need... $basefields = array('id', 'sortorder', 'shortname', 'idnumber'); if (!is_null($fields)) { $fields = array_merge($basefields, $fields); $fields = array_unique($fields); } else { $fields = $basefields; } $coursefields = 'c.' . implode(',c.', $fields); $sort = trim($sort); if ($sort !== '') { $sort = "ORDER BY {$sort}"; } $sysctx = get_context_instance(CONTEXT_SYSTEM); if (has_capability_in_accessdata($cap, $sysctx, $accessdata, $doanything)) { // // Apparently the user has the cap sitewide, so walk *every* course // (the cap checks are moderately fast, but this moves massive bandwidth w the db) // Yuck. // $sql = "SELECT {$coursefields},\n ctx.id AS ctxid, ctx.path AS ctxpath,\n ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,\n cc.path AS categorypath\n FROM {$CFG->prefix}course c\n JOIN {$CFG->prefix}course_categories cc\n ON c.category=cc.id\n JOIN {$CFG->prefix}context ctx \n ON (c.id=ctx.instanceid AND ctx.contextlevel=" . CONTEXT_COURSE . ")\n {$sort} "; $rs = get_recordset_sql($sql); } else { // // narrow down where we have the caps to a few contexts // this will be a combination of // - categories where we have the rights // - courses where we have an explicit enrolment OR that have an override // $sql = "SELECT ctx.*\n FROM {$CFG->prefix}context ctx\n WHERE ctx.contextlevel=" . CONTEXT_COURSECAT . "\n ORDER BY ctx.depth"; $rs = get_recordset_sql($sql); $catpaths = array(); if ($rs->RecordCount()) { while ($catctx = rs_fetch_next_record($rs)) { if ($catctx->path != '' && has_capability_in_accessdata($cap, $catctx, $accessdata, $doanything)) { $catpaths[] = $catctx->path; } } } rs_close($rs); $catclause = ''; if (count($catpaths)) { $cc = count($catpaths); for ($n = 0; $n < $cc; $n++) { $catpaths[$n] = "ctx.path LIKE '{$catpaths[$n]}/%'"; } $catclause = 'OR (' . implode(' OR ', $catpaths) . ')'; } unset($catpaths); $capany = ''; if ($doanything) { $capany = " OR rc.capability='moodle/site:doanything'"; } // // Note here that we *have* to have the compound clauses // in the LEFT OUTER JOIN condition for them to return NULL // appropriately and narrow things down... // $sql = "SELECT {$coursefields},\n ctx.id AS ctxid, ctx.path AS ctxpath,\n ctx.depth AS ctxdepth, ctx.contextlevel AS ctxlevel,\n cc.path AS categorypath\n FROM {$CFG->prefix}course c\n JOIN {$CFG->prefix}course_categories cc\n ON c.category=cc.id\n JOIN {$CFG->prefix}context ctx \n ON (c.id=ctx.instanceid AND ctx.contextlevel=" . CONTEXT_COURSE . ")\n LEFT OUTER JOIN {$CFG->prefix}role_assignments ra\n ON (ra.contextid=ctx.id AND ra.userid={$userid})\n LEFT OUTER JOIN {$CFG->prefix}role_capabilities rc\n ON (rc.contextid=ctx.id AND (rc.capability='{$cap}' {$capany}))\n WHERE ra.id IS NOT NULL\n OR rc.id IS NOT NULL\n {$catclause}\n {$sort} "; $rs = get_recordset_sql($sql); } $courses = array(); $cc = 0; // keep count if ($rs->RecordCount()) { while ($c = rs_fetch_next_record($rs)) { // build the context obj $c = make_context_subobj($c); if (has_capability_in_accessdata($cap, $c->context, $accessdata, $doanything)) { $courses[] = $c; if ($limit > 0 && $cc++ > $limit) { break; } } } } rs_close($rs); return $courses; }