PageRenderTime 92ms CodeModel.GetById 17ms RepoModel.GetById 1ms app.codeStats 1ms

/lib/accesslib.php

https://github.com/mylescarrick/moodle
PHP | 6291 lines | 3637 code | 755 blank | 1899 comment | 749 complexity | 8ce3d4a97f29057435ac570bd95d5440 MD5 | raw file
Possible License(s): GPL-3.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * This file contains functions for managing user access
  18. *
  19. * <b>Public API vs internals</b>
  20. *
  21. * General users probably only care about
  22. *
  23. * Context handling
  24. * - get_context_instance()
  25. * - get_context_instance_by_id()
  26. * - get_parent_contexts()
  27. * - get_child_contexts()
  28. *
  29. * Whether the user can do something...
  30. * - has_capability()
  31. * - has_any_capability()
  32. * - has_all_capabilities()
  33. * - require_capability()
  34. * - require_login() (from moodlelib)
  35. *
  36. * What courses has this user access to?
  37. * - get_user_courses_bycap()
  38. *
  39. * What users can do X in this context?
  40. * - get_users_by_capability()
  41. *
  42. * Enrol/unenrol
  43. * - enrol_into_course()
  44. * - role_assign()/role_unassign()
  45. *
  46. *
  47. * Advanced use
  48. * - load_all_capabilities()
  49. * - reload_all_capabilities()
  50. * - has_capability_in_accessdata()
  51. * - is_siteadmin()
  52. * - get_user_access_sitewide()
  53. * - load_subcontext()
  54. * - get_role_access_bycontext()
  55. *
  56. * <b>Name conventions</b>
  57. *
  58. * "ctx" means context
  59. *
  60. * <b>accessdata</b>
  61. *
  62. * Access control data is held in the "accessdata" array
  63. * which - for the logged-in user, will be in $USER->access
  64. *
  65. * For other users can be generated and passed around (but may also be cached
  66. * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser.
  67. *
  68. * $accessdata is a multidimensional array, holding
  69. * role assignments (RAs), role-capabilities-perm sets
  70. * (role defs) and a list of courses we have loaded
  71. * data for.
  72. *
  73. * Things are keyed on "contextpaths" (the path field of
  74. * the context table) for fast walking up/down the tree.
  75. * <code>
  76. * $accessdata[ra][$contextpath]= array($roleid)
  77. * [$contextpath]= array($roleid)
  78. * [$contextpath]= array($roleid)
  79. * </code>
  80. *
  81. * Role definitions are stored like this
  82. * (no cap merge is done - so it's compact)
  83. *
  84. * <code>
  85. * $accessdata[rdef][$contextpath:$roleid][mod/forum:viewpost] = 1
  86. * [mod/forum:editallpost] = -1
  87. * [mod/forum:startdiscussion] = -1000
  88. * </code>
  89. *
  90. * See how has_capability_in_accessdata() walks up/down the tree.
  91. *
  92. * Normally - specially for the logged-in user, we only load
  93. * rdef and ra down to the course level, but not below. This
  94. * keeps accessdata small and compact. Below-the-course ra/rdef
  95. * are loaded as needed. We keep track of which courses we
  96. * have loaded ra/rdef in
  97. * <code>
  98. * $accessdata[loaded] = array($contextpath, $contextpath)
  99. * </code>
  100. *
  101. * <b>Stale accessdata</b>
  102. *
  103. * For the logged-in user, accessdata is long-lived.
  104. *
  105. * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
  106. * context paths affected by changes. Any check at-or-below
  107. * a dirty context will trigger a transparent reload of accessdata.
  108. *
  109. * Changes at the system level will force the reload for everyone.
  110. *
  111. * <b>Default role caps</b>
  112. * The default role assignment is not in the DB, so we
  113. * add it manually to accessdata.
  114. *
  115. * This means that functions that work directly off the
  116. * DB need to ensure that the default role caps
  117. * are dealt with appropriately.
  118. *
  119. * @package core
  120. * @subpackage role
  121. * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
  122. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  123. */
  124. defined('MOODLE_INTERNAL') || die();
  125. /** permission definitions */
  126. define('CAP_INHERIT', 0);
  127. /** permission definitions */
  128. define('CAP_ALLOW', 1);
  129. /** permission definitions */
  130. define('CAP_PREVENT', -1);
  131. /** permission definitions */
  132. define('CAP_PROHIBIT', -1000);
  133. /** context definitions */
  134. define('CONTEXT_SYSTEM', 10);
  135. /** context definitions */
  136. define('CONTEXT_USER', 30);
  137. /** context definitions */
  138. define('CONTEXT_COURSECAT', 40);
  139. /** context definitions */
  140. define('CONTEXT_COURSE', 50);
  141. /** context definitions */
  142. define('CONTEXT_MODULE', 70);
  143. /** context definitions */
  144. define('CONTEXT_BLOCK', 80);
  145. /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
  146. define('RISK_MANAGETRUST', 0x0001);
  147. /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
  148. define('RISK_CONFIG', 0x0002);
  149. /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
  150. define('RISK_XSS', 0x0004);
  151. /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
  152. define('RISK_PERSONAL', 0x0008);
  153. /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
  154. define('RISK_SPAM', 0x0010);
  155. /** capability risks - see {@link http://docs.moodle.org/en/Development:Hardening_new_Roles_system} */
  156. define('RISK_DATALOSS', 0x0020);
  157. /** rolename displays - the name as defined in the role definition */
  158. define('ROLENAME_ORIGINAL', 0);
  159. /** rolename displays - the name as defined by a role alias */
  160. define('ROLENAME_ALIAS', 1);
  161. /** rolename displays - Both, like this: Role alias (Original)*/
  162. define('ROLENAME_BOTH', 2);
  163. /** rolename displays - the name as defined in the role definition and the shortname in brackets*/
  164. define('ROLENAME_ORIGINALANDSHORT', 3);
  165. /** rolename displays - the name as defined by a role alias, in raw form suitable for editing*/
  166. define('ROLENAME_ALIAS_RAW', 4);
  167. /** rolename displays - the name is simply short role name*/
  168. define('ROLENAME_SHORT', 5);
  169. /**
  170. * Internal class provides a cache of context information. The cache is
  171. * restricted in size.
  172. *
  173. * This cache should NOT be used outside accesslib.php!
  174. *
  175. * @private
  176. * @author Sam Marshall
  177. */
  178. class context_cache {
  179. private $contextsbyid;
  180. private $contexts;
  181. private $count;
  182. /**
  183. * @var int Maximum number of contexts that will be cached.
  184. */
  185. const MAX_SIZE = 2500;
  186. /**
  187. * @var int Once contexts reach maximum number, this many will be removed from cache.
  188. */
  189. const REDUCE_SIZE = 1000;
  190. /**
  191. * Initialises (empty)
  192. */
  193. public function __construct() {
  194. $this->reset();
  195. }
  196. /**
  197. * Resets the cache to remove all data.
  198. */
  199. public function reset() {
  200. $this->contexts = array();
  201. $this->contextsbyid = array();
  202. $this->count = 0;
  203. }
  204. /**
  205. * Adds a context to the cache. If the cache is full, discards a batch of
  206. * older entries.
  207. * @param stdClass $context New context to add
  208. */
  209. public function add(stdClass $context) {
  210. if ($this->count >= self::MAX_SIZE) {
  211. for ($i=0; $i<self::REDUCE_SIZE; $i++) {
  212. if ($first = reset($this->contextsbyid)) {
  213. unset($this->contextsbyid[$first->id]);
  214. unset($this->contexts[$first->contextlevel][$first->instanceid]);
  215. }
  216. }
  217. $this->count -= self::REDUCE_SIZE;
  218. if ($this->count < 0) {
  219. // most probably caused by the drift, the reset() above
  220. // might have returned false because there might not be any more elements
  221. $this->count = 0;
  222. }
  223. }
  224. $this->contexts[$context->contextlevel][$context->instanceid] = $context;
  225. $this->contextsbyid[$context->id] = $context;
  226. // Note the count may get out of synch slightly if you cache a context
  227. // that is already cached, but it doesn't really matter much and I
  228. // didn't think it was worth the performance hit.
  229. $this->count++;
  230. }
  231. /**
  232. * Removes a context from the cache.
  233. * @param stdClass $context Context object to remove (must include fields
  234. * ->id, ->contextlevel, ->instanceid at least)
  235. */
  236. public function remove(stdClass $context) {
  237. unset($this->contexts[$context->contextlevel][$context->instanceid]);
  238. unset($this->contextsbyid[$context->id]);
  239. // Again the count may get a bit out of synch if you remove things
  240. // that don't exist
  241. $this->count--;
  242. if ($this->count < 0) {
  243. $this->count = 0;
  244. }
  245. }
  246. /**
  247. * Gets a context from the cache.
  248. * @param int $contextlevel Context level
  249. * @param int $instance Instance ID
  250. * @return stdClass|bool Context or false if not in cache
  251. */
  252. public function get($contextlevel, $instance) {
  253. if (isset($this->contexts[$contextlevel][$instance])) {
  254. return $this->contexts[$contextlevel][$instance];
  255. }
  256. return false;
  257. }
  258. /**
  259. * Gets a context from the cache based on its id.
  260. * @param int $id Context ID
  261. * @return stdClass|bool Context or false if not in cache
  262. */
  263. public function get_by_id($id) {
  264. if (isset($this->contextsbyid[$id])) {
  265. return $this->contextsbyid[$id];
  266. }
  267. return false;
  268. }
  269. /**
  270. * @return int Count of contexts in cache (approximately)
  271. */
  272. public function get_approx_count() {
  273. return $this->count;
  274. }
  275. }
  276. /**
  277. * Although this looks like a global variable, it isn't really.
  278. *
  279. * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
  280. * It is used to cache various bits of data between function calls for performance reasons.
  281. * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
  282. * as methods of a class, instead of functions.
  283. *
  284. * @global stdClass $ACCESSLIB_PRIVATE
  285. * @name $ACCESSLIB_PRIVATE
  286. */
  287. global $ACCESSLIB_PRIVATE;
  288. $ACCESSLIB_PRIVATE = new stdClass();
  289. $ACCESSLIB_PRIVATE->contexcache = new context_cache();
  290. $ACCESSLIB_PRIVATE->systemcontext = null; // Used in get_system_context
  291. $ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache
  292. $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the $accessdata structure for users other than $USER
  293. $ACCESSLIB_PRIVATE->roledefinitions = array(); // role definitions cache - helps a lot with mem usage in cron
  294. $ACCESSLIB_PRIVATE->croncache = array(); // Used in get_role_access
  295. $ACCESSLIB_PRIVATE->preloadedcourses = array(); // Used in preload_course_contexts.
  296. $ACCESSLIB_PRIVATE->capabilities = null; // detailed information about the capabilities
  297. /**
  298. * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
  299. *
  300. * This method should ONLY BE USED BY UNIT TESTS. It clears all of
  301. * accesslib's private caches. You need to do this before setting up test data,
  302. * and also at the end of the tests.
  303. */
  304. function accesslib_clear_all_caches_for_unit_testing() {
  305. global $UNITTEST, $USER, $ACCESSLIB_PRIVATE;
  306. if (empty($UNITTEST->running)) {
  307. throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
  308. }
  309. $ACCESSLIB_PRIVATE->contexcache = new context_cache();
  310. $ACCESSLIB_PRIVATE->systemcontext = null;
  311. $ACCESSLIB_PRIVATE->dirtycontexts = null;
  312. $ACCESSLIB_PRIVATE->accessdatabyuser = array();
  313. $ACCESSLIB_PRIVATE->roledefinitions = array();
  314. $ACCESSLIB_PRIVATE->croncache = array();
  315. $ACCESSLIB_PRIVATE->preloadedcourses = array();
  316. $ACCESSLIB_PRIVATE->capabilities = null;
  317. unset($USER->access);
  318. }
  319. /**
  320. * This is really slow!!! do not use above course context level
  321. *
  322. * @param int $roleid
  323. * @param object $context
  324. * @return array
  325. */
  326. function get_role_context_caps($roleid, $context) {
  327. global $DB;
  328. //this is really slow!!!! - do not use above course context level!
  329. $result = array();
  330. $result[$context->id] = array();
  331. // first emulate the parent context capabilities merging into context
  332. $searchcontexts = array_reverse(get_parent_contexts($context));
  333. array_push($searchcontexts, $context->id);
  334. foreach ($searchcontexts as $cid) {
  335. if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
  336. foreach ($capabilities as $cap) {
  337. if (!array_key_exists($cap->capability, $result[$context->id])) {
  338. $result[$context->id][$cap->capability] = 0;
  339. }
  340. $result[$context->id][$cap->capability] += $cap->permission;
  341. }
  342. }
  343. }
  344. // now go through the contexts bellow given context
  345. $searchcontexts = array_keys(get_child_contexts($context));
  346. foreach ($searchcontexts as $cid) {
  347. if ($capabilities = $DB->get_records('role_capabilities', array('roleid'=>$roleid, 'contextid'=>$cid))) {
  348. foreach ($capabilities as $cap) {
  349. if (!array_key_exists($cap->contextid, $result)) {
  350. $result[$cap->contextid] = array();
  351. }
  352. $result[$cap->contextid][$cap->capability] = $cap->permission;
  353. }
  354. }
  355. }
  356. return $result;
  357. }
  358. /**
  359. * Gets the accessdata for role "sitewide" (system down to course)
  360. *
  361. * @param int $roleid
  362. * @param array $accessdata defaults to null
  363. * @return array
  364. */
  365. function get_role_access($roleid, $accessdata = null) {
  366. global $CFG, $DB;
  367. /* Get it in 1 cheap DB query...
  368. * - relevant role caps at the root and down
  369. * to the course level - but not below
  370. */
  371. if (is_null($accessdata)) {
  372. $accessdata = array(); // named list
  373. $accessdata['ra'] = array();
  374. $accessdata['rdef'] = array();
  375. $accessdata['loaded'] = array();
  376. }
  377. //
  378. // Overrides for the role IN ANY CONTEXTS
  379. // down to COURSE - not below -
  380. //
  381. $sql = "SELECT ctx.path,
  382. rc.capability, rc.permission
  383. FROM {context} ctx
  384. JOIN {role_capabilities} rc
  385. ON rc.contextid=ctx.id
  386. WHERE rc.roleid = ?
  387. AND ctx.contextlevel <= ".CONTEXT_COURSE."
  388. ORDER BY ctx.depth, ctx.path";
  389. $params = array($roleid);
  390. // we need extra caching in CLI scripts and cron
  391. if (CLI_SCRIPT) {
  392. global $ACCESSLIB_PRIVATE;
  393. if (!isset($ACCESSLIB_PRIVATE->croncache[$roleid])) {
  394. $ACCESSLIB_PRIVATE->croncache[$roleid] = array();
  395. $rs = $DB->get_recordset_sql($sql, $params);
  396. foreach ($rs as $rd) {
  397. $ACCESSLIB_PRIVATE->croncache[$roleid][] = $rd;
  398. }
  399. $rs->close();
  400. }
  401. foreach ($ACCESSLIB_PRIVATE->croncache[$roleid] as $rd) {
  402. $k = "{$rd->path}:{$roleid}";
  403. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  404. }
  405. } else {
  406. $rs = $DB->get_recordset_sql($sql, $params);
  407. if ($rs->valid()) {
  408. foreach ($rs as $rd) {
  409. $k = "{$rd->path}:{$roleid}";
  410. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  411. }
  412. unset($rd);
  413. }
  414. $rs->close();
  415. }
  416. return $accessdata;
  417. }
  418. /**
  419. * Gets the accessdata for role "sitewide" (system down to course)
  420. *
  421. * @param int $roleid
  422. * @param array $accessdata defaults to null
  423. * @return array
  424. */
  425. function get_default_frontpage_role_access($roleid, $accessdata = null) {
  426. global $CFG, $DB;
  427. $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
  428. $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
  429. //
  430. // Overrides for the role in any contexts related to the course
  431. //
  432. $sql = "SELECT ctx.path,
  433. rc.capability, rc.permission
  434. FROM {context} ctx
  435. JOIN {role_capabilities} rc
  436. ON rc.contextid=ctx.id
  437. WHERE rc.roleid = ?
  438. AND (ctx.id = ".SYSCONTEXTID." OR ctx.path LIKE ?)
  439. AND ctx.contextlevel <= ".CONTEXT_COURSE."
  440. ORDER BY ctx.depth, ctx.path";
  441. $params = array($roleid, "$base/%");
  442. $rs = $DB->get_recordset_sql($sql, $params);
  443. if ($rs->valid()) {
  444. foreach ($rs as $rd) {
  445. $k = "{$rd->path}:{$roleid}";
  446. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  447. }
  448. unset($rd);
  449. }
  450. $rs->close();
  451. return $accessdata;
  452. }
  453. /**
  454. * Get the default guest role
  455. *
  456. * @return stdClass role
  457. */
  458. function get_guest_role() {
  459. global $CFG, $DB;
  460. if (empty($CFG->guestroleid)) {
  461. if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
  462. $guestrole = array_shift($roles); // Pick the first one
  463. set_config('guestroleid', $guestrole->id);
  464. return $guestrole;
  465. } else {
  466. debugging('Can not find any guest role!');
  467. return false;
  468. }
  469. } else {
  470. if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
  471. return $guestrole;
  472. } else {
  473. //somebody is messing with guest roles, remove incorrect setting and try to find a new one
  474. set_config('guestroleid', '');
  475. return get_guest_role();
  476. }
  477. }
  478. }
  479. /**
  480. * Check whether a user has a particular capability in a given context.
  481. *
  482. * For example::
  483. * $context = get_context_instance(CONTEXT_MODULE, $cm->id);
  484. * has_capability('mod/forum:replypost',$context)
  485. *
  486. * By default checks the capabilities of the current user, but you can pass a
  487. * different userid. By default will return true for admin users, but you can override that with the fourth argument.
  488. *
  489. * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
  490. * or capabilities with XSS, config or data loss risks.
  491. *
  492. * @param string $capability the name of the capability to check. For example mod/forum:view
  493. * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
  494. * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
  495. * @param boolean $doanything If false, ignores effect of admin role assignment
  496. * @return boolean true if the user has this capability. Otherwise false.
  497. */
  498. function has_capability($capability, $context, $user = null, $doanything = true) {
  499. global $USER, $CFG, $DB, $SCRIPT, $ACCESSLIB_PRIVATE;
  500. if (during_initial_install()) {
  501. if ($SCRIPT === "/$CFG->admin/index.php" or $SCRIPT === "/$CFG->admin/cliupgrade.php") {
  502. // we are in an installer - roles can not work yet
  503. return true;
  504. } else {
  505. return false;
  506. }
  507. }
  508. if (strpos($capability, 'moodle/legacy:') === 0) {
  509. throw new coding_exception('Legacy capabilities can not be used any more!');
  510. }
  511. // the original $CONTEXT here was hiding serious errors
  512. // for security reasons do not reuse previous context
  513. if (empty($context)) {
  514. debugging('Incorrect context specified');
  515. return false;
  516. }
  517. if (!is_bool($doanything)) {
  518. throw new coding_exception('Capability parameter "doanything" is wierd ("'.$doanything.'"). This has to be fixed in code.');
  519. }
  520. // make sure there is a real user specified
  521. if ($user === null) {
  522. $userid = !empty($USER->id) ? $USER->id : 0;
  523. } else {
  524. $userid = !empty($user->id) ? $user->id : $user;
  525. }
  526. // capability must exist
  527. if (!$capinfo = get_capability_info($capability)) {
  528. debugging('Capability "'.$capability.'" was not found! This should be fixed in code.');
  529. return false;
  530. }
  531. // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
  532. if (($capinfo->captype === 'write') or ((int)$capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
  533. if (isguestuser($userid) or $userid == 0) {
  534. return false;
  535. }
  536. }
  537. if (is_null($context->path) or $context->depth == 0) {
  538. //this should not happen
  539. $contexts = array(SYSCONTEXTID, $context->id);
  540. $context->path = '/'.SYSCONTEXTID.'/'.$context->id;
  541. debugging('Context id '.$context->id.' does not have valid path, please use build_context_path()', DEBUG_DEVELOPER);
  542. } else {
  543. $contexts = explode('/', $context->path);
  544. array_shift($contexts);
  545. }
  546. if (CLI_SCRIPT && !isset($USER->access)) {
  547. // In cron, some modules setup a 'fake' $USER,
  548. // ensure we load the appropriate accessdata.
  549. if (isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
  550. $ACCESSLIB_PRIVATE->dirtycontexts = null; //load fresh dirty contexts
  551. } else {
  552. load_user_accessdata($userid);
  553. $ACCESSLIB_PRIVATE->dirtycontexts = array();
  554. }
  555. $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
  556. } else if (isset($USER->id) && ($USER->id == $userid) && !isset($USER->access)) {
  557. // caps not loaded yet - better to load them to keep BC with 1.8
  558. // not-logged-in user or $USER object set up manually first time here
  559. load_all_capabilities();
  560. $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // reset the cache for other users too, the dirty contexts are empty now
  561. $ACCESSLIB_PRIVATE->roledefinitions = array();
  562. }
  563. // Load dirty contexts list if needed
  564. if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
  565. if (isset($USER->access['time'])) {
  566. $ACCESSLIB_PRIVATE->dirtycontexts = get_dirty_contexts($USER->access['time']);
  567. }
  568. else {
  569. $ACCESSLIB_PRIVATE->dirtycontexts = array();
  570. }
  571. }
  572. // Careful check for staleness...
  573. if (count($ACCESSLIB_PRIVATE->dirtycontexts) !== 0 and is_contextpath_dirty($contexts, $ACCESSLIB_PRIVATE->dirtycontexts)) {
  574. // reload all capabilities - preserving loginas, roleswitches, etc
  575. // and then cleanup any marks of dirtyness... at least from our short
  576. // term memory! :-)
  577. $ACCESSLIB_PRIVATE->accessdatabyuser = array();
  578. $ACCESSLIB_PRIVATE->roledefinitions = array();
  579. if (CLI_SCRIPT) {
  580. load_user_accessdata($userid);
  581. $USER->access = $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
  582. $ACCESSLIB_PRIVATE->dirtycontexts = array();
  583. } else {
  584. reload_all_capabilities();
  585. }
  586. }
  587. // Find out if user is admin - it is not possible to override the doanything in any way
  588. // and it is not possible to switch to admin role either.
  589. if ($doanything) {
  590. if (is_siteadmin($userid)) {
  591. if ($userid != $USER->id) {
  592. return true;
  593. }
  594. // make sure switchrole is not used in this context
  595. if (empty($USER->access['rsw'])) {
  596. return true;
  597. }
  598. $parts = explode('/', trim($context->path, '/'));
  599. $path = '';
  600. $switched = false;
  601. foreach ($parts as $part) {
  602. $path .= '/' . $part;
  603. if (!empty($USER->access['rsw'][$path])) {
  604. $switched = true;
  605. break;
  606. }
  607. }
  608. if (!$switched) {
  609. return true;
  610. }
  611. //ok, admin switched role in this context, let's use normal access control rules
  612. }
  613. }
  614. // divulge how many times we are called
  615. //// error_log("has_capability: id:{$context->id} path:{$context->path} userid:$userid cap:$capability");
  616. if (isset($USER->id) && ($USER->id == $userid)) { // we must accept strings and integers in $userid
  617. //
  618. // For the logged in user, we have $USER->access
  619. // which will have all RAs and caps preloaded for
  620. // course and above contexts.
  621. //
  622. // Contexts below courses && contexts that do not
  623. // hang from courses are loaded into $USER->access
  624. // on demand, and listed in $USER->access[loaded]
  625. //
  626. if ($context->contextlevel <= CONTEXT_COURSE) {
  627. // Course and above are always preloaded
  628. return has_capability_in_accessdata($capability, $context, $USER->access);
  629. }
  630. // Load accessdata for below-the-course contexts
  631. if (!path_inaccessdata($context->path,$USER->access)) {
  632. // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
  633. // $bt = debug_backtrace();
  634. // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
  635. load_subcontext($USER->id, $context, $USER->access);
  636. }
  637. return has_capability_in_accessdata($capability, $context, $USER->access);
  638. }
  639. if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
  640. load_user_accessdata($userid);
  641. }
  642. if ($context->contextlevel <= CONTEXT_COURSE) {
  643. // Course and above are always preloaded
  644. return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
  645. }
  646. // Load accessdata for below-the-course contexts as needed
  647. if (!path_inaccessdata($context->path, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
  648. // error_log("loading access for context {$context->path} for $capability at {$context->contextlevel} {$context->id}");
  649. // $bt = debug_backtrace();
  650. // error_log("bt {$bt[0]['file']} {$bt[0]['line']}");
  651. load_subcontext($userid, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
  652. }
  653. return has_capability_in_accessdata($capability, $context, $ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
  654. }
  655. /**
  656. * Check if the user has any one of several capabilities from a list.
  657. *
  658. * This is just a utility method that calls has_capability in a loop. Try to put
  659. * the capabilities that most users are likely to have first in the list for best
  660. * performance.
  661. *
  662. * There are probably tricks that could be done to improve the performance here, for example,
  663. * check the capabilities that are already cached first.
  664. *
  665. * @see has_capability()
  666. * @param array $capabilities an array of capability names.
  667. * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
  668. * @param integer $userid A user id. By default (null) checks the permissions of the current user.
  669. * @param boolean $doanything If false, ignore effect of admin role assignment
  670. * @return boolean true if the user has any of these capabilities. Otherwise false.
  671. */
  672. function has_any_capability($capabilities, $context, $userid = null, $doanything = true) {
  673. if (!is_array($capabilities)) {
  674. debugging('Incorrect $capabilities parameter in has_any_capabilities() call - must be an array');
  675. return false;
  676. }
  677. foreach ($capabilities as $capability) {
  678. if (has_capability($capability, $context, $userid, $doanything)) {
  679. return true;
  680. }
  681. }
  682. return false;
  683. }
  684. /**
  685. * Check if the user has all the capabilities in a list.
  686. *
  687. * This is just a utility method that calls has_capability in a loop. Try to put
  688. * the capabilities that fewest users are likely to have first in the list for best
  689. * performance.
  690. *
  691. * There are probably tricks that could be done to improve the performance here, for example,
  692. * check the capabilities that are already cached first.
  693. *
  694. * @see has_capability()
  695. * @param array $capabilities an array of capability names.
  696. * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
  697. * @param integer $userid A user id. By default (null) checks the permissions of the current user.
  698. * @param boolean $doanything If false, ignore effect of admin role assignment
  699. * @return boolean true if the user has all of these capabilities. Otherwise false.
  700. */
  701. function has_all_capabilities($capabilities, $context, $userid = null, $doanything = true) {
  702. if (!is_array($capabilities)) {
  703. debugging('Incorrect $capabilities parameter in has_all_capabilities() call - must be an array');
  704. return false;
  705. }
  706. foreach ($capabilities as $capability) {
  707. if (!has_capability($capability, $context, $userid, $doanything)) {
  708. return false;
  709. }
  710. }
  711. return true;
  712. }
  713. /**
  714. * Check if the user is an admin at the site level.
  715. *
  716. * Please note that use of proper capabilities is always encouraged,
  717. * this function is supposed to be used from core or for temporary hacks.
  718. *
  719. * @param int|object $user_or_id user id or user object
  720. * @returns bool true if user is one of the administrators, false otherwise
  721. */
  722. function is_siteadmin($user_or_id = null) {
  723. global $CFG, $USER;
  724. if ($user_or_id === null) {
  725. $user_or_id = $USER;
  726. }
  727. if (empty($user_or_id)) {
  728. return false;
  729. }
  730. if (!empty($user_or_id->id)) {
  731. // we support
  732. $userid = $user_or_id->id;
  733. } else {
  734. $userid = $user_or_id;
  735. }
  736. $siteadmins = explode(',', $CFG->siteadmins);
  737. return in_array($userid, $siteadmins);
  738. }
  739. /**
  740. * Returns true if user has at least one role assign
  741. * of 'coursecontact' role (is potentially listed in some course descriptions).
  742. *
  743. * @param $userid
  744. * @return stdClass
  745. */
  746. function has_coursecontact_role($userid) {
  747. global $DB, $CFG;
  748. if (empty($CFG->coursecontact)) {
  749. return false;
  750. }
  751. $sql = "SELECT 1
  752. FROM {role_assignments}
  753. WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
  754. return $DB->record_exists_sql($sql, array('userid'=>$userid));
  755. }
  756. /**
  757. * @param string $path
  758. * @return string
  759. */
  760. function get_course_from_path($path) {
  761. // assume that nothing is more than 1 course deep
  762. if (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
  763. return $matches[1];
  764. }
  765. return false;
  766. }
  767. /**
  768. * @param string $path
  769. * @param array $accessdata
  770. * @return bool
  771. */
  772. function path_inaccessdata($path, $accessdata) {
  773. if (empty($accessdata['loaded'])) {
  774. return false;
  775. }
  776. // assume that contexts hang from sys or from a course
  777. // this will only work well with stuff that hangs from a course
  778. if (in_array($path, $accessdata['loaded'], true)) {
  779. // error_log("found it!");
  780. return true;
  781. }
  782. $base = '/' . SYSCONTEXTID;
  783. while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
  784. $path = $matches[1];
  785. if ($path === $base) {
  786. return false;
  787. }
  788. if (in_array($path, $accessdata['loaded'], true)) {
  789. return true;
  790. }
  791. }
  792. return false;
  793. }
  794. /**
  795. * Does the user have a capability to do something?
  796. *
  797. * Walk the accessdata array and return true/false.
  798. * Deals with prohibits, roleswitching, aggregating
  799. * capabilities, etc.
  800. *
  801. * The main feature of here is being FAST and with no
  802. * side effects.
  803. *
  804. * Notes:
  805. *
  806. * Switch Roles exits early
  807. * ------------------------
  808. * cap checks within a switchrole need to exit early
  809. * in our bottom up processing so they don't "see" that
  810. * there are real RAs that can do all sorts of things.
  811. *
  812. * Switch Role merges with default role
  813. * ------------------------------------
  814. * If you are a teacher in course X, you have at least
  815. * teacher-in-X + defaultloggedinuser-sitewide. So in the
  816. * course you'll have techer+defaultloggedinuser.
  817. * We try to mimic that in switchrole.
  818. *
  819. * Permission evaluation
  820. * ---------------------
  821. * Originally there was an extremely complicated way
  822. * to determine the user access that dealt with
  823. * "locality" or role assignments and role overrides.
  824. * Now we simply evaluate access for each role separately
  825. * and then verify if user has at least one role with allow
  826. * and at the same time no role with prohibit.
  827. *
  828. * @param string $capability
  829. * @param object $context
  830. * @param array $accessdata
  831. * @return bool
  832. */
  833. function has_capability_in_accessdata($capability, $context, array $accessdata) {
  834. global $CFG;
  835. if (empty($context->id)) {
  836. throw new coding_exception('Invalid context specified');
  837. }
  838. // Build $paths as a list of current + all parent "paths" with order bottom-to-top
  839. $contextids = explode('/', trim($context->path, '/'));
  840. $paths = array($context->path);
  841. while ($contextids) {
  842. array_pop($contextids);
  843. $paths[] = '/' . implode('/', $contextids);
  844. }
  845. unset($contextids);
  846. $roles = array();
  847. $switchedrole = false;
  848. // Find out if role switched
  849. if (!empty($accessdata['rsw'])) {
  850. // From the bottom up...
  851. foreach ($paths as $path) {
  852. if (isset($accessdata['rsw'][$path])) {
  853. // Found a switchrole assignment - check for that role _plus_ the default user role
  854. $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
  855. $switchedrole = true;
  856. break;
  857. }
  858. }
  859. }
  860. if (!$switchedrole) {
  861. // get all users roles in this context and above
  862. foreach ($paths as $path) {
  863. if (isset($accessdata['ra'][$path])) {
  864. foreach ($accessdata['ra'][$path] as $roleid) {
  865. $roles[$roleid] = null;
  866. }
  867. }
  868. }
  869. }
  870. // Now find out what access is given to each role, going bottom-->up direction
  871. foreach ($roles as $roleid => $ignored) {
  872. foreach ($paths as $path) {
  873. if (isset($accessdata['rdef']["{$path}:$roleid"][$capability])) {
  874. $perm = (int)$accessdata['rdef']["{$path}:$roleid"][$capability];
  875. if ($perm === CAP_PROHIBIT or is_null($roles[$roleid])) {
  876. $roles[$roleid] = $perm;
  877. }
  878. }
  879. }
  880. }
  881. // any CAP_PROHIBIT found means no permission for the user
  882. if (array_search(CAP_PROHIBIT, $roles) !== false) {
  883. return false;
  884. }
  885. // at least one CAP_ALLOW means the user has a permission
  886. return (array_search(CAP_ALLOW, $roles) !== false);
  887. }
  888. /**
  889. * @param object $context
  890. * @param array $accessdata
  891. * @return array
  892. */
  893. function aggregate_roles_from_accessdata($context, $accessdata) {
  894. $path = $context->path;
  895. // build $contexts as a list of "paths" of the current
  896. // contexts and parents with the order top-to-bottom
  897. $contexts = array($path);
  898. while (preg_match('!^(/.+)/\d+$!', $path, $matches)) {
  899. $path = $matches[1];
  900. array_unshift($contexts, $path);
  901. }
  902. $cc = count($contexts);
  903. $roles = array();
  904. // From the bottom up...
  905. for ($n=$cc-1; $n>=0; $n--) {
  906. $ctxp = $contexts[$n];
  907. if (isset($accessdata['ra'][$ctxp]) && count($accessdata['ra'][$ctxp])) {
  908. // Found assignments on this leaf
  909. $addroles = $accessdata['ra'][$ctxp];
  910. $roles = array_merge($roles, $addroles);
  911. }
  912. }
  913. return array_unique($roles);
  914. }
  915. /**
  916. * A convenience function that tests has_capability, and displays an error if
  917. * the user does not have that capability.
  918. *
  919. * NOTE before Moodle 2.0, this function attempted to make an appropriate
  920. * require_login call before checking the capability. This is no longer the case.
  921. * You must call require_login (or one of its variants) if you want to check the
  922. * user is logged in, before you call this function.
  923. *
  924. * @see has_capability()
  925. *
  926. * @param string $capability the name of the capability to check. For example mod/forum:view
  927. * @param object $context the context to check the capability in. You normally get this with {@link get_context_instance}.
  928. * @param integer $userid A user id. By default (null) checks the permissions of the current user.
  929. * @param bool $doanything If false, ignore effect of admin role assignment
  930. * @param string $errorstring The error string to to user. Defaults to 'nopermissions'.
  931. * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
  932. * @return void terminates with an error if the user does not have the given capability.
  933. */
  934. function require_capability($capability, $context, $userid = null, $doanything = true,
  935. $errormessage = 'nopermissions', $stringfile = '') {
  936. if (!has_capability($capability, $context, $userid, $doanything)) {
  937. throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
  938. }
  939. }
  940. /**
  941. * Get an array of courses where cap requested is available
  942. * and user is enrolled, this can be relatively slow.
  943. *
  944. * @param string $capability - name of the capability
  945. * @param array $accessdata_ignored
  946. * @param bool $doanything_ignored
  947. * @param string $sort - sorting fields - prefix each fieldname with "c."
  948. * @param array $fields - additional fields you are interested in...
  949. * @param int $limit_ignored
  950. * @return array $courses - ordered array of course objects - see notes above
  951. */
  952. function get_user_courses_bycap($userid, $cap, $accessdata_ignored, $doanything_ignored, $sort = 'c.sortorder ASC', $fields = null, $limit_ignored = 0) {
  953. //TODO: this should be most probably deprecated
  954. $courses = enrol_get_users_courses($userid, true, $fields, $sort);
  955. foreach ($courses as $id=>$course) {
  956. $context = get_context_instance(CONTEXT_COURSE, $id);
  957. if (!has_capability($cap, $context, $userid)) {
  958. unset($courses[$id]);
  959. }
  960. }
  961. return $courses;
  962. }
  963. /**
  964. * Return a nested array showing role assignments
  965. * all relevant role capabilities for the user at
  966. * site/course_category/course levels
  967. *
  968. * We do _not_ delve deeper than courses because the number of
  969. * overrides at the module/block levels is HUGE.
  970. *
  971. * [ra] => [/path/][]=roleid
  972. * [rdef] => [/path/:roleid][capability]=permission
  973. * [loaded] => array('/path', '/path')
  974. *
  975. * @param int $userid - the id of the user
  976. * @return array
  977. */
  978. function get_user_access_sitewide($userid) {
  979. global $CFG, $DB;
  980. /* Get in 3 cheap DB queries...
  981. * - role assignments
  982. * - relevant role caps
  983. * - above and within this user's RAs
  984. * - below this user's RAs - limited to course level
  985. */
  986. $accessdata = array(); // named list
  987. $accessdata['ra'] = array();
  988. $accessdata['rdef'] = array();
  989. $accessdata['loaded'] = array();
  990. //
  991. // Role assignments
  992. //
  993. $sql = "SELECT ctx.path, ra.roleid
  994. FROM {role_assignments} ra
  995. JOIN {context} ctx ON ctx.id=ra.contextid
  996. WHERE ra.userid = ? AND ctx.contextlevel <= ".CONTEXT_COURSE;
  997. $params = array($userid);
  998. $rs = $DB->get_recordset_sql($sql, $params);
  999. //
  1000. // raparents collects paths & roles we need to walk up
  1001. // the parenthood to build the rdef
  1002. //
  1003. $raparents = array();
  1004. if ($rs) {
  1005. foreach ($rs as $ra) {
  1006. // RAs leafs are arrays to support multi
  1007. // role assignments...
  1008. if (!isset($accessdata['ra'][$ra->path])) {
  1009. $accessdata['ra'][$ra->path] = array();
  1010. }
  1011. $accessdata['ra'][$ra->path][$ra->roleid] = $ra->roleid;
  1012. // Concatenate as string the whole path (all related context)
  1013. // for this role. This is damn faster than using array_merge()
  1014. // Will unique them later
  1015. if (isset($raparents[$ra->roleid])) {
  1016. $raparents[$ra->roleid] .= $ra->path;
  1017. } else {
  1018. $raparents[$ra->roleid] = $ra->path;
  1019. }
  1020. }
  1021. unset($ra);
  1022. $rs->close();
  1023. }
  1024. // Walk up the tree to grab all the roledefs
  1025. // of interest to our user...
  1026. //
  1027. // NOTE: we use a series of IN clauses here - which
  1028. // might explode on huge sites with very convoluted nesting of
  1029. // categories... - extremely unlikely that the number of categories
  1030. // and roletypes is so large that we hit the limits of IN()
  1031. $clauses = '';
  1032. $cparams = array();
  1033. foreach ($raparents as $roleid=>$strcontexts) {
  1034. $contexts = implode(',', array_unique(explode('/', trim($strcontexts, '/'))));
  1035. if ($contexts ==! '') {
  1036. if ($clauses) {
  1037. $clauses .= ' OR ';
  1038. }
  1039. $clauses .= "(roleid=? AND contextid IN ($contexts))";
  1040. $cparams[] = $roleid;
  1041. }
  1042. }
  1043. if ($clauses !== '') {
  1044. $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
  1045. FROM {role_capabilities} rc
  1046. JOIN {context} ctx ON rc.contextid=ctx.id
  1047. WHERE $clauses";
  1048. unset($clauses);
  1049. $rs = $DB->get_recordset_sql($sql, $cparams);
  1050. if ($rs) {
  1051. foreach ($rs as $rd) {
  1052. $k = "{$rd->path}:{$rd->roleid}";
  1053. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  1054. }
  1055. unset($rd);
  1056. $rs->close();
  1057. }
  1058. }
  1059. //
  1060. // Overrides for the role assignments IN SUBCONTEXTS
  1061. // (though we still do _not_ go below the course level.
  1062. //
  1063. // NOTE that the JOIN w sctx is with 3-way triangulation to
  1064. // catch overrides to the applicable role in any subcontext, based
  1065. // on the path field of the parent.
  1066. //
  1067. $sql = "SELECT sctx.path, ra.roleid,
  1068. ctx.path AS parentpath,
  1069. rco.capability, rco.permission
  1070. FROM {role_assignments} ra
  1071. JOIN {context} ctx
  1072. ON ra.contextid=ctx.id
  1073. JOIN {context} sctx
  1074. ON (sctx.path LIKE " . $DB->sql_concat('ctx.path',"'/%'"). " )
  1075. JOIN {role_capabilities} rco
  1076. ON (rco.roleid=ra.roleid AND rco.contextid=sctx.id)
  1077. WHERE ra.userid = ?
  1078. AND ctx.contextlevel <= ".CONTEXT_COURSECAT."
  1079. AND sctx.contextlevel <= ".CONTEXT_COURSE."
  1080. ORDER BY sctx.depth, sctx.path, ra.roleid";
  1081. $params = array($userid);
  1082. $rs = $DB->get_recordset_sql($sql, $params);
  1083. if ($rs) {
  1084. foreach ($rs as $rd) {
  1085. $k = "{$rd->path}:{$rd->roleid}";
  1086. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  1087. }
  1088. unset($rd);
  1089. $rs->close();
  1090. }
  1091. return $accessdata;
  1092. }
  1093. /**
  1094. * Add to the access ctrl array the data needed by a user for a given context
  1095. *
  1096. * @param integer $userid the id of the user
  1097. * @param object $context needs path!
  1098. * @param array $accessdata accessdata array
  1099. * @return void
  1100. */
  1101. function load_subcontext($userid, $context, &$accessdata) {
  1102. global $CFG, $DB;
  1103. /* Get the additional RAs and relevant rolecaps
  1104. * - role assignments - with role_caps
  1105. * - relevant role caps
  1106. * - above this user's RAs
  1107. * - below this user's RAs - limited to course level
  1108. */
  1109. $base = "/" . SYSCONTEXTID;
  1110. //
  1111. // Replace $context with the target context we will
  1112. // load. Normally, this will be a course context, but
  1113. // may be a different top-level context.
  1114. //
  1115. // We have 3 cases
  1116. //
  1117. // - Course
  1118. // - BLOCK/PERSON/USER/COURSE(sitecourse) hanging from SYSTEM
  1119. // - BLOCK/MODULE/GROUP hanging from a course
  1120. //
  1121. // For course contexts, we _already_ have the RAs
  1122. // but the cost of re-fetching is minimal so we don't care.
  1123. //
  1124. if ($context->contextlevel !== CONTEXT_COURSE
  1125. && $context->path !== "$base/{$context->id}") {
  1126. // Case BLOCK/MODULE/GROUP hanging from a course
  1127. // Assumption: the course _must_ be our parent
  1128. // If we ever see stuff nested further this needs to
  1129. // change to do 1 query over the exploded path to
  1130. // find out which one is the course
  1131. $courses = explode('/',get_course_from_path($context->path));
  1132. $targetid = array_pop($courses);
  1133. $context = get_context_instance_by_id($targetid);
  1134. }
  1135. //
  1136. // Role assignments in the context and below
  1137. //
  1138. $sql = "SELECT ctx.path, ra.roleid
  1139. FROM {role_assignments} ra
  1140. JOIN {context} ctx
  1141. ON ra.contextid=ctx.id
  1142. WHERE ra.userid = ?
  1143. AND (ctx.path = ? OR ctx.path LIKE ?)
  1144. ORDER BY ctx.depth, ctx.path, ra.roleid";
  1145. $params = array($userid, $context->path, $context->path."/%");
  1146. $rs = $DB->get_recordset_sql($sql, $params);
  1147. //
  1148. // Read in the RAs, preventing duplicates
  1149. //
  1150. if ($rs) {
  1151. $localroles = array();
  1152. $lastseen = '';
  1153. foreach ($rs as $ra) {
  1154. if (!isset($accessdata['ra'][$ra->path])) {
  1155. $accessdata['ra'][$ra->path] = array();
  1156. }
  1157. // only add if is not a repeat caused
  1158. // by capability join...
  1159. // (this check is cheaper than in_array())
  1160. if ($lastseen !== $ra->path.':'.$ra->roleid) {
  1161. $lastseen = $ra->path.':'.$ra->roleid;
  1162. $accessdata['ra'][$ra->path][$ra->roleid] = $ra->roleid;
  1163. array_push($localroles, $ra->roleid);
  1164. }
  1165. }
  1166. $rs->close();
  1167. }
  1168. //
  1169. // Walk up and down the tree to grab all the roledefs
  1170. // of interest to our user...
  1171. //
  1172. // NOTES
  1173. // - we use IN() but the number of roles is very limited.
  1174. //
  1175. $courseroles = aggregate_roles_from_accessdata($context, $accessdata);
  1176. // Do we have any interesting "local" roles?
  1177. $localroles = array_diff($localroles,$courseroles); // only "new" local roles
  1178. $wherelocalroles='';
  1179. if (count($localroles)) {
  1180. // Role defs for local roles in 'higher' contexts...
  1181. $contexts = substr($context->path, 1); // kill leading slash
  1182. $contexts = str_replace('/', ',', $contexts);
  1183. $localroleids = implode(',',$localroles);
  1184. $wherelocalroles="OR (rc.roleid IN ({$localroleids})
  1185. AND ctx.id IN ($contexts))" ;
  1186. }
  1187. // We will want overrides for all of them
  1188. $whereroles = '';
  1189. if ($roleids = implode(',',array_merge($courseroles,$localroles))) {
  1190. $whereroles = "rc.roleid IN ($roleids) AND";
  1191. }
  1192. $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
  1193. FROM {role_capabilities} rc
  1194. JOIN {context} ctx
  1195. ON rc.contextid=ctx.id
  1196. WHERE ($whereroles
  1197. (ctx.id=? OR ctx.path LIKE ?))
  1198. $wherelocalroles
  1199. ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
  1200. $params = array($context->id, $context->path."/%");
  1201. $newrdefs = array();
  1202. $rs = $DB->get_recordset_sql($sql, $params);
  1203. foreach ($rs as $rd) {
  1204. $k = "{$rd->path}:{$rd->roleid}";
  1205. if (!array_key_exists($k, $newrdefs)) {
  1206. $newrdefs[$k] = array();
  1207. }
  1208. $newrdefs[$k][$rd->capability] = $rd->permission;
  1209. }
  1210. $rs->close();
  1211. compact_rdefs($newrdefs);
  1212. foreach ($newrdefs as $key=>$value) {
  1213. $accessdata['rdef'][$key] =& $newrdefs[$key];
  1214. }
  1215. // error_log("loaded {$context->path}");
  1216. $accessdata['loaded'][] = $context->path;
  1217. }
  1218. /**
  1219. * Add to the access ctrl array the data needed by a role for a given context.
  1220. *
  1221. * The data is added in the rdef key.
  1222. *
  1223. * This role-centric function is useful for role_switching
  1224. * and to get an overview of what a role gets under a
  1225. * given context and below...
  1226. *
  1227. * @param integer $roleid the id of the user
  1228. * @param object $context needs path!
  1229. * @param array $accessdata accessdata array null by default
  1230. * @return array
  1231. */
  1232. function get_role_access_bycontext($roleid, $context, $accessdata = null) {
  1233. global $CFG, $DB;
  1234. /* Get the relevant rolecaps into rdef
  1235. * - relevant role caps
  1236. * - at ctx and above
  1237. * - below this ctx
  1238. */
  1239. if (is_null($accessdata)) {
  1240. $accessdata = array(); // named list
  1241. $accessdata['ra'] = array();
  1242. $accessdata['rdef'] = array();
  1243. $accessdata['loaded'] = array();
  1244. }
  1245. $contexts = substr($context->path, 1); // kill leading slash
  1246. $contexts = str_replace('/', ',', $contexts);
  1247. //
  1248. // Walk up and down the tree to grab all the roledefs
  1249. // of interest to our role...
  1250. //
  1251. // NOTE: we use an IN clauses here - which
  1252. // might explode on huge sites with very convoluted nesting of
  1253. // categories... - extremely unlikely that the number of nested
  1254. // categories is so large that we hit the limits of IN()
  1255. //
  1256. $sql = "SELECT ctx.path, rc.capability, rc.permission
  1257. FROM {role_capabilities} rc
  1258. JOIN {context} ctx
  1259. ON rc.contextid=ctx.id
  1260. WHERE rc.roleid=? AND
  1261. ( ctx.id IN ($contexts) OR
  1262. ctx.path LIKE ? )
  1263. ORDER BY ctx.depth ASC, ctx.path DESC, rc.roleid ASC ";
  1264. $params = array($roleid, $context->path."/%");
  1265. $rs = $DB->get_recordset_sql($sql, $params);
  1266. foreach ($rs as $rd) {
  1267. $k = "{$rd->path}:{$roleid}";
  1268. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  1269. }
  1270. $rs->close();
  1271. return $accessdata;
  1272. }
  1273. /**
  1274. * Load accessdata for a user into the $ACCESSLIB_PRIVATE->accessdatabyuser global
  1275. *
  1276. * Used by has_capability() - but feel free
  1277. * to call it if you are about to run a BIG
  1278. * cron run across a bazillion users.
  1279. *
  1280. * @param int $userid
  1281. * @return array returns ACCESSLIB_PRIVATE->accessdatabyuser[userid]
  1282. */
  1283. function load_user_accessdata($userid) {
  1284. global $CFG, $ACCESSLIB_PRIVATE;
  1285. $base = '/'.SYSCONTEXTID;
  1286. $accessdata = get_user_access_sitewide($userid);
  1287. $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
  1288. //
  1289. // provide "default role" & set 'dr'
  1290. //
  1291. if (!empty($CFG->defaultuserroleid)) {
  1292. $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
  1293. if (!isset($accessdata['ra'][$base])) {
  1294. $accessdata['ra'][$base] = array();
  1295. }
  1296. $accessdata['ra'][$base][$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
  1297. $accessdata['dr'] = $CFG->defaultuserroleid;
  1298. }
  1299. //
  1300. // provide "default frontpage role"
  1301. //
  1302. if (!empty($CFG->defaultfrontpageroleid)) {
  1303. $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
  1304. $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
  1305. if (!isset($accessdata['ra'][$base])) {
  1306. $accessdata['ra'][$base] = array();
  1307. }
  1308. $accessdata['ra'][$base][$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
  1309. }
  1310. // for dirty timestamps in cron
  1311. $accessdata['time'] = time();
  1312. $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
  1313. compact_rdefs($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]['rdef']);
  1314. return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
  1315. }
  1316. /**
  1317. * Use shared copy of role definitions stored in ACCESSLIB_PRIVATE->roledefinitions;
  1318. *
  1319. * @param array $rdefs array of role definitions in contexts
  1320. */
  1321. function compact_rdefs(&$rdefs) {
  1322. global $ACCESSLIB_PRIVATE;
  1323. /*
  1324. * This is a basic sharing only, we could also
  1325. * use md5 sums of values. The main purpose is to
  1326. * reduce mem in cron jobs - many users in $ACCESSLIB_PRIVATE->accessdatabyuser array.
  1327. */
  1328. foreach ($rdefs as $key => $value) {
  1329. if (!array_key_exists($key, $ACCESSLIB_PRIVATE->roledefinitions)) {
  1330. $ACCESSLIB_PRIVATE->roledefinitions[$key] = $rdefs[$key];
  1331. }
  1332. $rdefs[$key] =& $ACCESSLIB_PRIVATE->roledefinitions[$key];
  1333. }
  1334. }
  1335. /**
  1336. * A convenience function to completely load all the capabilities
  1337. * for the current user. This is what gets called from complete_user_login()
  1338. * for example. Call it only _after_ you've setup $USER and called
  1339. * check_enrolment_plugins();
  1340. * @see check_enrolment_plugins()
  1341. *
  1342. * @return void
  1343. */
  1344. function load_all_capabilities() {
  1345. global $CFG, $ACCESSLIB_PRIVATE;
  1346. //NOTE: we can not use $USER here because it may no be linked to $_SESSION['USER'] yet!
  1347. // roles not installed yet - we are in the middle of installation
  1348. if (during_initial_install()) {
  1349. return;
  1350. }
  1351. $base = '/'.SYSCONTEXTID;
  1352. if (isguestuser($_SESSION['USER'])) {
  1353. $guest = get_guest_role();
  1354. // Load the rdefs
  1355. $_SESSION['USER']->access = get_role_access($guest->id);
  1356. // Put the ghost enrolment in place...
  1357. $_SESSION['USER']->access['ra'][$base] = array($guest->id => $guest->id);
  1358. } else if (!empty($_SESSION['USER']->id)) { // can not use isloggedin() yet
  1359. $accessdata = get_user_access_sitewide($_SESSION['USER']->id);
  1360. //
  1361. // provide "default role" & set 'dr'
  1362. //
  1363. if (!empty($CFG->defaultuserroleid)) {
  1364. $accessdata = get_role_access($CFG->defaultuserroleid, $accessdata);
  1365. if (!isset($accessdata['ra'][$base])) {
  1366. $accessdata['ra'][$base] = array();
  1367. }
  1368. $accessdata['ra'][$base][$CFG->defaultuserroleid] = $CFG->defaultuserroleid;
  1369. $accessdata['dr'] = $CFG->defaultuserroleid;
  1370. }
  1371. $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
  1372. //
  1373. // provide "default frontpage role"
  1374. //
  1375. if (!empty($CFG->defaultfrontpageroleid)) {
  1376. $base = '/'. SYSCONTEXTID .'/'. $frontpagecontext->id;
  1377. $accessdata = get_default_frontpage_role_access($CFG->defaultfrontpageroleid, $accessdata);
  1378. if (!isset($accessdata['ra'][$base])) {
  1379. $accessdata['ra'][$base] = array();
  1380. }
  1381. $accessdata['ra'][$base][$CFG->defaultfrontpageroleid] = $CFG->defaultfrontpageroleid;
  1382. }
  1383. $_SESSION['USER']->access = $accessdata;
  1384. } else if (!empty($CFG->notloggedinroleid)) {
  1385. $_SESSION['USER']->access = get_role_access($CFG->notloggedinroleid);
  1386. $_SESSION['USER']->access['ra'][$base] = array($CFG->notloggedinroleid => $CFG->notloggedinroleid);
  1387. }
  1388. // Timestamp to read dirty context timestamps later
  1389. $_SESSION['USER']->access['time'] = time();
  1390. $ACCESSLIB_PRIVATE->dirtycontexts = array();
  1391. // Clear to force a refresh
  1392. unset($_SESSION['USER']->mycourses);
  1393. }
  1394. /**
  1395. * A convenience function to completely reload all the capabilities
  1396. * for the current user when roles have been updated in a relevant
  1397. * context -- but PRESERVING switchroles and loginas.
  1398. *
  1399. * That is - completely transparent to the user.
  1400. *
  1401. * Note: rewrites $USER->access completely.
  1402. *
  1403. * @return void
  1404. */
  1405. function reload_all_capabilities() {
  1406. global $USER, $DB;
  1407. // error_log("reloading");
  1408. // copy switchroles
  1409. $sw = array();
  1410. if (isset($USER->access['rsw'])) {
  1411. $sw = $USER->access['rsw'];
  1412. // error_log(print_r($sw,1));
  1413. }
  1414. unset($USER->access);
  1415. unset($USER->mycourses);
  1416. load_all_capabilities();
  1417. foreach ($sw as $path => $roleid) {
  1418. $context = $DB->get_record('context', array('path'=>$path));
  1419. role_switch($roleid, $context);
  1420. }
  1421. }
  1422. /**
  1423. * Adds a temp role to an accessdata array.
  1424. *
  1425. * Useful for the "temporary guest" access
  1426. * we grant to logged-in users.
  1427. *
  1428. * Note - assumes a course context!
  1429. *
  1430. * @param object $content
  1431. * @param int $roleid
  1432. * @param array $accessdata
  1433. * @return array Returns access data
  1434. */
  1435. function load_temp_role($context, $roleid, array $accessdata) {
  1436. global $CFG, $DB;
  1437. //
  1438. // Load rdefs for the role in -
  1439. // - this context
  1440. // - all the parents
  1441. // - and below - IOWs overrides...
  1442. //
  1443. // turn the path into a list of context ids
  1444. $contexts = substr($context->path, 1); // kill leading slash
  1445. $contexts = str_replace('/', ',', $contexts);
  1446. $sql = "SELECT ctx.path, rc.capability, rc.permission
  1447. FROM {context} ctx
  1448. JOIN {role_capabilities} rc
  1449. ON rc.contextid=ctx.id
  1450. WHERE (ctx.id IN ($contexts)
  1451. OR ctx.path LIKE ?)
  1452. AND rc.roleid = ?
  1453. ORDER BY ctx.depth, ctx.path";
  1454. $params = array($context->path."/%", $roleid);
  1455. $rs = $DB->get_recordset_sql($sql, $params);
  1456. foreach ($rs as $rd) {
  1457. $k = "{$rd->path}:{$roleid}";
  1458. $accessdata['rdef'][$k][$rd->capability] = $rd->permission;
  1459. }
  1460. $rs->close();
  1461. //
  1462. // Say we loaded everything for the course context
  1463. // - which we just did - if the user gets a proper
  1464. // RA in this session, this data will need to be reloaded,
  1465. // but that is handled by the complete accessdata reload
  1466. //
  1467. array_push($accessdata['loaded'], $context->path);
  1468. //
  1469. // Add the ghost RA
  1470. //
  1471. if (!isset($accessdata['ra'][$context->path])) {
  1472. $accessdata['ra'][$context->path] = array();
  1473. }
  1474. $accessdata['ra'][$context->path][$roleid] = $roleid;
  1475. return $accessdata;
  1476. }
  1477. /**
  1478. * Removes any extra guest roles from accessdata
  1479. * @param object $context
  1480. * @param array $accessdata
  1481. * @return array access data
  1482. */
  1483. function remove_temp_roles($context, array $accessdata) {
  1484. global $DB, $USER;
  1485. $sql = "SELECT DISTINCT ra.roleid AS id
  1486. FROM {role_assignments} ra
  1487. WHERE ra.contextid = :contextid AND ra.userid = :userid";
  1488. $ras = $DB->get_records_sql($sql, array('contextid'=>$context->id, 'userid'=>$USER->id));
  1489. if ($ras) {
  1490. $accessdata['ra'][$context->path] = array_combine(array_keys($ras), array_keys($ras));
  1491. } else {
  1492. $accessdata['ra'][$context->path] = array();
  1493. }
  1494. return $accessdata;
  1495. }
  1496. /**
  1497. * Returns array of all role archetypes.
  1498. *
  1499. * @return array
  1500. */
  1501. function get_role_archetypes() {
  1502. return array(
  1503. 'manager' => 'manager',
  1504. 'coursecreator' => 'coursecreator',
  1505. 'editingteacher' => 'editingteacher',
  1506. 'teacher' => 'teacher',
  1507. 'student' => 'student',
  1508. 'guest' => 'guest',
  1509. 'user' => 'user',
  1510. 'frontpage' => 'frontpage'
  1511. );
  1512. }
  1513. /**
  1514. * Assign the defaults found in this capability definition to roles that have
  1515. * the corresponding legacy capabilities assigned to them.
  1516. *
  1517. * @param string $capability
  1518. * @param array $legacyperms an array in the format (example):
  1519. * 'guest' => CAP_PREVENT,
  1520. * 'student' => CAP_ALLOW,
  1521. * 'teacher' => CAP_ALLOW,
  1522. * 'editingteacher' => CAP_ALLOW,
  1523. * 'coursecreator' => CAP_ALLOW,
  1524. * 'manager' => CAP_ALLOW
  1525. * @return boolean success or failure.
  1526. */
  1527. function assign_legacy_capabilities($capability, $legacyperms) {
  1528. $archetypes = get_role_archetypes();
  1529. foreach ($legacyperms as $type => $perm) {
  1530. $systemcontext = get_context_instance(CONTEXT_SYSTEM);
  1531. if ($type === 'admin') {
  1532. debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
  1533. $type = 'manager';
  1534. }
  1535. if (!array_key_exists($type, $archetypes)) {
  1536. print_error('invalidlegacy', '', '', $type);
  1537. }
  1538. if ($roles = get_archetype_roles($type)) {
  1539. foreach ($roles as $role) {
  1540. // Assign a site level capability.
  1541. if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
  1542. return false;
  1543. }
  1544. }
  1545. }
  1546. }
  1547. return true;
  1548. }
  1549. /**
  1550. * @param object $capability a capability - a row from the capabilities table.
  1551. * @return boolean whether this capability is safe - that is, whether people with the
  1552. * safeoverrides capability should be allowed to change it.
  1553. */
  1554. function is_safe_capability($capability) {
  1555. return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
  1556. }
  1557. /**********************************
  1558. * Context Manipulation functions *
  1559. **********************************/
  1560. /**
  1561. * Context creation - internal implementation.
  1562. *
  1563. * Create a new context record for use by all roles-related stuff
  1564. * assumes that the caller has done the homework.
  1565. *
  1566. * DO NOT CALL THIS DIRECTLY, instead use {@link get_context_instance}!
  1567. *
  1568. * @param int $contextlevel
  1569. * @param int $instanceid
  1570. * @param int $strictness
  1571. * @return object newly created context
  1572. */
  1573. function create_context($contextlevel, $instanceid, $strictness = IGNORE_MISSING) {
  1574. global $CFG, $DB;
  1575. if ($contextlevel == CONTEXT_SYSTEM) {
  1576. return get_system_context();
  1577. }
  1578. $context = new stdClass();
  1579. $context->contextlevel = $contextlevel;
  1580. $context->instanceid = $instanceid;
  1581. // Define $context->path based on the parent
  1582. // context. In other words... Who is your daddy?
  1583. $basepath = '/' . SYSCONTEXTID;
  1584. $basedepth = 1;
  1585. $result = true;
  1586. $error_message = null;
  1587. switch ($contextlevel) {
  1588. case CONTEXT_COURSECAT:
  1589. $sql = "SELECT ctx.path, ctx.depth
  1590. FROM {context} ctx
  1591. JOIN {course_categories} cc
  1592. ON (cc.parent=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
  1593. WHERE cc.id=?";
  1594. $params = array($instanceid);
  1595. if ($p = $DB->get_record_sql($sql, $params)) {
  1596. $basepath = $p->path;
  1597. $basedepth = $p->depth;
  1598. } else if ($category = $DB->get_record('course_categories', array('id'=>$instanceid), '*', $strictness)) {
  1599. if (empty($category->parent)) {
  1600. // ok - this is a top category
  1601. } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $category->parent)) {
  1602. $basepath = $parent->path;
  1603. $basedepth = $parent->depth;
  1604. } else {
  1605. // wrong parent category - no big deal, this can be fixed later
  1606. $basepath = null;
  1607. $basedepth = 0;
  1608. }
  1609. } else {
  1610. // incorrect category id
  1611. $error_message = "incorrect course category id ($instanceid)";
  1612. $result = false;
  1613. }
  1614. break;
  1615. case CONTEXT_COURSE:
  1616. $sql = "SELECT ctx.path, ctx.depth
  1617. FROM {context} ctx
  1618. JOIN {course} c
  1619. ON (c.category=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSECAT.")
  1620. WHERE c.id=? AND c.id !=" . SITEID;
  1621. $params = array($instanceid);
  1622. if ($p = $DB->get_record_sql($sql, $params)) {
  1623. $basepath = $p->path;
  1624. $basedepth = $p->depth;
  1625. } else if ($course = $DB->get_record('course', array('id'=>$instanceid), '*', $strictness)) {
  1626. if ($course->id == SITEID) {
  1627. //ok - no parent category
  1628. } else if ($parent = get_context_instance(CONTEXT_COURSECAT, $course->category)) {
  1629. $basepath = $parent->path;
  1630. $basedepth = $parent->depth;
  1631. } else {
  1632. // wrong parent category of course - no big deal, this can be fixed later
  1633. $basepath = null;
  1634. $basedepth = 0;
  1635. }
  1636. } else if ($instanceid == SITEID) {
  1637. // no errors for missing site course during installation
  1638. return false;
  1639. } else {
  1640. // incorrect course id
  1641. $error_message = "incorrect course id ($instanceid)";
  1642. $result = false;
  1643. }
  1644. break;
  1645. case CONTEXT_MODULE:
  1646. $sql = "SELECT ctx.path, ctx.depth
  1647. FROM {context} ctx
  1648. JOIN {course_modules} cm
  1649. ON (cm.course=ctx.instanceid AND ctx.contextlevel=".CONTEXT_COURSE.")
  1650. WHERE cm.id=?";
  1651. $params = array($instanceid);
  1652. if ($p = $DB->get_record_sql($sql, $params)) {
  1653. $basepath = $p->path;
  1654. $basedepth = $p->depth;
  1655. } else if ($cm = $DB->get_record('course_modules', array('id'=>$instanceid), '*', $strictness)) {
  1656. if ($parent = get_context_instance(CONTEXT_COURSE, $cm->course, $strictness)) {
  1657. $basepath = $parent->path;
  1658. $basedepth = $parent->depth;
  1659. } else {
  1660. // course does not exist - modules can not exist without a course
  1661. $error_message = "course does not exist ($cm->course) - modules can not exist without a course";
  1662. $result = false;
  1663. }
  1664. } else {
  1665. // cm does not exist
  1666. $error_message = "cm with id $instanceid does not exist";
  1667. $result = false;
  1668. }
  1669. break;
  1670. case CONTEXT_BLOCK:
  1671. $sql = "SELECT ctx.path, ctx.depth
  1672. FROM {context} ctx
  1673. JOIN {block_instances} bi ON (bi.parentcontextid=ctx.id)
  1674. WHERE bi.id = ?";
  1675. $params = array($instanceid, CONTEXT_COURSE);
  1676. if ($p = $DB->get_record_sql($sql, $params, '*', $strictness)) {
  1677. $basepath = $p->path;
  1678. $basedepth = $p->depth;
  1679. } else {
  1680. // block does not exist
  1681. $error_message = 'block or parent context does not exist';
  1682. $result = false;
  1683. }
  1684. break;
  1685. case CONTEXT_USER:
  1686. // default to basepath
  1687. break;
  1688. }
  1689. // if grandparents unknown, maybe rebuild_context_path() will solve it later
  1690. if ($basedepth != 0) {
  1691. $context->depth = $basedepth+1;
  1692. }
  1693. if (!$result) {
  1694. debugging('Error: could not insert new context level "'.
  1695. s($contextlevel).'", instance "'.
  1696. s($instanceid).'". ' . $error_message);
  1697. return false;
  1698. }
  1699. $id = $DB->insert_record('context', $context);
  1700. // can't set the full path till we know the id!
  1701. if ($basedepth != 0 and !empty($basepath)) {
  1702. $DB->set_field('context', 'path', $basepath.'/'. $id, array('id'=>$id));
  1703. }
  1704. return get_context_instance_by_id($id);
  1705. }
  1706. /**
  1707. * Returns system context or null if can not be created yet.
  1708. *
  1709. * @param bool $cache use caching
  1710. * @return mixed system context or null
  1711. */
  1712. function get_system_context($cache = true) {
  1713. global $DB, $ACCESSLIB_PRIVATE;
  1714. if ($cache and defined('SYSCONTEXTID')) {
  1715. if (is_null($ACCESSLIB_PRIVATE->systemcontext)) {
  1716. $ACCESSLIB_PRIVATE->systemcontext = new stdClass();
  1717. $ACCESSLIB_PRIVATE->systemcontext->id = SYSCONTEXTID;
  1718. $ACCESSLIB_PRIVATE->systemcontext->contextlevel = CONTEXT_SYSTEM;
  1719. $ACCESSLIB_PRIVATE->systemcontext->instanceid = 0;
  1720. $ACCESSLIB_PRIVATE->systemcontext->path = '/'.SYSCONTEXTID;
  1721. $ACCESSLIB_PRIVATE->systemcontext->depth = 1;
  1722. }
  1723. return $ACCESSLIB_PRIVATE->systemcontext;
  1724. }
  1725. try {
  1726. $context = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM));
  1727. } catch (dml_exception $e) {
  1728. //table does not exist yet, sorry
  1729. return null;
  1730. }
  1731. if (!$context) {
  1732. $context = new stdClass();
  1733. $context->contextlevel = CONTEXT_SYSTEM;
  1734. $context->instanceid = 0;
  1735. $context->depth = 1;
  1736. $context->path = null; //not known before insert
  1737. try {
  1738. $context->id = $DB->insert_record('context', $context);
  1739. } catch (dml_exception $e) {
  1740. // can not create context yet, sorry
  1741. return null;
  1742. }
  1743. }
  1744. if (!isset($context->depth) or $context->depth != 1 or $context->instanceid != 0 or $context->path != '/'.$context->id) {
  1745. $context->instanceid = 0;
  1746. $context->path = '/'.$context->id;
  1747. $context->depth = 1;
  1748. $DB->update_record('context', $context);
  1749. }
  1750. if (!defined('SYSCONTEXTID')) {
  1751. define('SYSCONTEXTID', $context->id);
  1752. }
  1753. $ACCESSLIB_PRIVATE->systemcontext = $context;
  1754. return $ACCESSLIB_PRIVATE->systemcontext;
  1755. }
  1756. /**
  1757. * Remove a context record and any dependent entries,
  1758. * removes context from static context cache too
  1759. *
  1760. * @param int $level
  1761. * @param int $instanceid
  1762. * @param bool $deleterecord false means keep record for now
  1763. * @return bool returns true or throws an exception
  1764. */
  1765. function delete_context($contextlevel, $instanceid, $deleterecord = true) {
  1766. global $DB, $ACCESSLIB_PRIVATE, $CFG;
  1767. // do not use get_context_instance(), because the related object might not exist,
  1768. // or the context does not exist yet and it would be created now
  1769. if ($context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
  1770. // delete these first because they might fetch the context and try to recreate it!
  1771. blocks_delete_all_for_context($context->id);
  1772. filter_delete_all_for_context($context->id);
  1773. require_once($CFG->dirroot . '/comment/lib.php');
  1774. comment::delete_comments(array('contextid'=>$context->id));
  1775. require_once($CFG->dirroot.'/rating/lib.php');
  1776. $delopt = new stdclass();
  1777. $delopt->contextid = $context->id;
  1778. $rm = new rating_manager();
  1779. $rm->delete_ratings($delopt);
  1780. // delete all files attached to this context
  1781. $fs = get_file_storage();
  1782. $fs->delete_area_files($context->id);
  1783. // now delete stuff from role related tables, role_unassign_all
  1784. // and unenrol should be called earlier to do proper cleanup
  1785. $DB->delete_records('role_assignments', array('contextid'=>$context->id));
  1786. $DB->delete_records('role_capabilities', array('contextid'=>$context->id));
  1787. $DB->delete_records('role_names', array('contextid'=>$context->id));
  1788. // and finally it is time to delete the context record if requested
  1789. if ($deleterecord) {
  1790. $DB->delete_records('context', array('id'=>$context->id));
  1791. // purge static context cache if entry present
  1792. $ACCESSLIB_PRIVATE->contexcache->remove($context);
  1793. }
  1794. // do not mark dirty contexts if parents unknown
  1795. if (!is_null($context->path) and $context->depth > 0) {
  1796. mark_context_dirty($context->path);
  1797. }
  1798. }
  1799. return true;
  1800. }
  1801. /**
  1802. * Precreates all contexts including all parents
  1803. *
  1804. * @param int $contextlevel empty means all
  1805. * @param bool $buildpaths update paths and depths
  1806. * @return void
  1807. */
  1808. function create_contexts($contextlevel = null, $buildpaths = true) {
  1809. global $DB;
  1810. //make sure system context exists
  1811. $syscontext = get_system_context(false);
  1812. if (empty($contextlevel) or $contextlevel == CONTEXT_COURSECAT
  1813. or $contextlevel == CONTEXT_COURSE
  1814. or $contextlevel == CONTEXT_MODULE
  1815. or $contextlevel == CONTEXT_BLOCK) {
  1816. $sql = "INSERT INTO {context} (contextlevel, instanceid)
  1817. SELECT ".CONTEXT_COURSECAT.", cc.id
  1818. FROM {course}_categories cc
  1819. WHERE NOT EXISTS (SELECT 'x'
  1820. FROM {context} cx
  1821. WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
  1822. $DB->execute($sql);
  1823. }
  1824. if (empty($contextlevel) or $contextlevel == CONTEXT_COURSE
  1825. or $contextlevel == CONTEXT_MODULE
  1826. or $contextlevel == CONTEXT_BLOCK) {
  1827. $sql = "INSERT INTO {context} (contextlevel, instanceid)
  1828. SELECT ".CONTEXT_COURSE.", c.id
  1829. FROM {course} c
  1830. WHERE NOT EXISTS (SELECT 'x'
  1831. FROM {context} cx
  1832. WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
  1833. $DB->execute($sql);
  1834. }
  1835. if (empty($contextlevel) or $contextlevel == CONTEXT_MODULE
  1836. or $contextlevel == CONTEXT_BLOCK) {
  1837. $sql = "INSERT INTO {context} (contextlevel, instanceid)
  1838. SELECT ".CONTEXT_MODULE.", cm.id
  1839. FROM {course}_modules cm
  1840. WHERE NOT EXISTS (SELECT 'x'
  1841. FROM {context} cx
  1842. WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
  1843. $DB->execute($sql);
  1844. }
  1845. if (empty($contextlevel) or $contextlevel == CONTEXT_USER
  1846. or $contextlevel == CONTEXT_BLOCK) {
  1847. $sql = "INSERT INTO {context} (contextlevel, instanceid)
  1848. SELECT ".CONTEXT_USER.", u.id
  1849. FROM {user} u
  1850. WHERE u.deleted=0
  1851. AND NOT EXISTS (SELECT 'x'
  1852. FROM {context} cx
  1853. WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
  1854. $DB->execute($sql);
  1855. }
  1856. if (empty($contextlevel) or $contextlevel == CONTEXT_BLOCK) {
  1857. $sql = "INSERT INTO {context} (contextlevel, instanceid)
  1858. SELECT ".CONTEXT_BLOCK.", bi.id
  1859. FROM {block_instances} bi
  1860. WHERE NOT EXISTS (SELECT 'x'
  1861. FROM {context} cx
  1862. WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
  1863. $DB->execute($sql);
  1864. }
  1865. if ($buildpaths) {
  1866. build_context_path(false);
  1867. }
  1868. }
  1869. /**
  1870. * Remove stale context records
  1871. *
  1872. * @return bool
  1873. */
  1874. function cleanup_contexts() {
  1875. global $DB;
  1876. $sql = " SELECT c.contextlevel,
  1877. c.instanceid AS instanceid
  1878. FROM {context} c
  1879. LEFT OUTER JOIN {course}_categories t
  1880. ON c.instanceid = t.id
  1881. WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
  1882. UNION
  1883. SELECT c.contextlevel,
  1884. c.instanceid
  1885. FROM {context} c
  1886. LEFT OUTER JOIN {course} t
  1887. ON c.instanceid = t.id
  1888. WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
  1889. UNION
  1890. SELECT c.contextlevel,
  1891. c.instanceid
  1892. FROM {context} c
  1893. LEFT OUTER JOIN {course}_modules t
  1894. ON c.instanceid = t.id
  1895. WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
  1896. UNION
  1897. SELECT c.contextlevel,
  1898. c.instanceid
  1899. FROM {context} c
  1900. LEFT OUTER JOIN {user} t
  1901. ON c.instanceid = t.id
  1902. WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
  1903. UNION
  1904. SELECT c.contextlevel,
  1905. c.instanceid
  1906. FROM {context} c
  1907. LEFT OUTER JOIN {block_instances} t
  1908. ON c.instanceid = t.id
  1909. WHERE t.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
  1910. ";
  1911. // transactions used only for performance reasons here
  1912. $transaction = $DB->start_delegated_transaction();
  1913. $rs = $DB->get_recordset_sql($sql);
  1914. foreach ($rs as $ctx) {
  1915. delete_context($ctx->contextlevel, $ctx->instanceid);
  1916. }
  1917. $rs->close();
  1918. $transaction->allow_commit();
  1919. return true;
  1920. }
  1921. /**
  1922. * Preloads all contexts relating to a course: course, modules. Block contexts
  1923. * are no longer loaded here. The contexts for all the blocks on the current
  1924. * page are now efficiently loaded by {@link block_manager::load_blocks()}.
  1925. *
  1926. * @param int $courseid Course ID
  1927. * @return void
  1928. */
  1929. function preload_course_contexts($courseid) {
  1930. global $DB, $ACCESSLIB_PRIVATE;
  1931. // Users can call this multiple times without doing any harm
  1932. global $ACCESSLIB_PRIVATE;
  1933. if (array_key_exists($courseid, $ACCESSLIB_PRIVATE->preloadedcourses)) {
  1934. return;
  1935. }
  1936. $params = array($courseid, $courseid, $courseid);
  1937. $sql = "SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
  1938. FROM {course_modules} cm
  1939. JOIN {context} x ON x.instanceid=cm.id
  1940. WHERE cm.course=? AND x.contextlevel=".CONTEXT_MODULE."
  1941. UNION ALL
  1942. SELECT x.instanceid, x.id, x.contextlevel, x.path, x.depth
  1943. FROM {context} x
  1944. WHERE x.instanceid=? AND x.contextlevel=".CONTEXT_COURSE."";
  1945. $rs = $DB->get_recordset_sql($sql, $params);
  1946. foreach($rs as $context) {
  1947. $ACCESSLIB_PRIVATE->contexcache->add($context);
  1948. }
  1949. $rs->close();
  1950. $ACCESSLIB_PRIVATE->preloadedcourses[$courseid] = true;
  1951. }
  1952. /**
  1953. * Get the context instance as an object. This function will create the
  1954. * context instance if it does not exist yet.
  1955. *
  1956. * @todo Remove code branch from previous fix MDL-9016 which is no longer needed
  1957. *
  1958. * @param integer $level The context level, for example CONTEXT_COURSE, or CONTEXT_MODULE.
  1959. * @param integer $instance The instance id. For $level = CONTEXT_COURSE, this would be $course->id,
  1960. * for $level = CONTEXT_MODULE, this would be $cm->id. And so on. Defaults to 0
  1961. * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
  1962. * MUST_EXIST means throw exception if no record or multiple records found
  1963. * @return object The context object.
  1964. */
  1965. function get_context_instance($contextlevel, $instance = 0, $strictness = IGNORE_MISSING) {
  1966. global $DB, $ACCESSLIB_PRIVATE;
  1967. static $allowed_contexts = array(CONTEXT_SYSTEM, CONTEXT_USER, CONTEXT_COURSECAT, CONTEXT_COURSE, CONTEXT_MODULE, CONTEXT_BLOCK);
  1968. /// System context has special cache
  1969. if ($contextlevel == CONTEXT_SYSTEM) {
  1970. return get_system_context();
  1971. }
  1972. /// check allowed context levels
  1973. if (!in_array($contextlevel, $allowed_contexts)) {
  1974. // fatal error, code must be fixed - probably typo or switched parameters
  1975. print_error('invalidcourselevel');
  1976. }
  1977. // Various operations rely on context cache
  1978. $cache = $ACCESSLIB_PRIVATE->contexcache;
  1979. if (!is_array($instance)) {
  1980. /// Check the cache
  1981. $context = $cache->get($contextlevel, $instance);
  1982. if ($context) {
  1983. return $context;
  1984. }
  1985. /// Get it from the database, or create it
  1986. if (!$context = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instance))) {
  1987. $context = create_context($contextlevel, $instance, $strictness);
  1988. }
  1989. /// Only add to cache if context isn't empty.
  1990. if (!empty($context)) {
  1991. $cache->add($context);
  1992. }
  1993. return $context;
  1994. }
  1995. /// ok, somebody wants to load several contexts to save some db queries ;-)
  1996. $instances = $instance;
  1997. $result = array();
  1998. foreach ($instances as $key=>$instance) {
  1999. /// Check the cache first
  2000. if ($context = $cache->get($contextlevel, $instance)) { // Already cached
  2001. $result[$instance] = $context;
  2002. unset($instances[$key]);
  2003. continue;
  2004. }
  2005. }
  2006. if ($instances) {
  2007. list($instanceids, $params) = $DB->get_in_or_equal($instances, SQL_PARAMS_QM);
  2008. array_unshift($params, $contextlevel);
  2009. $sql = "SELECT instanceid, id, contextlevel, path, depth
  2010. FROM {context}
  2011. WHERE contextlevel=? AND instanceid $instanceids";
  2012. if (!$contexts = $DB->get_records_sql($sql, $params)) {
  2013. $contexts = array();
  2014. }
  2015. foreach ($instances as $instance) {
  2016. if (isset($contexts[$instance])) {
  2017. $context = $contexts[$instance];
  2018. } else {
  2019. $context = create_context($contextlevel, $instance);
  2020. }
  2021. if (!empty($context)) {
  2022. $cache->add($context);
  2023. }
  2024. $result[$instance] = $context;
  2025. }
  2026. }
  2027. return $result;
  2028. }
  2029. /**
  2030. * Get a context instance as an object, from a given context id.
  2031. *
  2032. * @param int $id context id
  2033. * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
  2034. * MUST_EXIST means throw exception if no record or multiple records found
  2035. * @return stdClass|bool the context object or false if not found.
  2036. */
  2037. function get_context_instance_by_id($id, $strictness = IGNORE_MISSING) {
  2038. global $DB, $ACCESSLIB_PRIVATE;
  2039. if ($id == SYSCONTEXTID) {
  2040. return get_system_context();
  2041. }
  2042. $cache = $ACCESSLIB_PRIVATE->contexcache;
  2043. if ($context = $cache->get_by_id($id)) {
  2044. return $context;
  2045. }
  2046. if ($context = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
  2047. $cache->add($context);
  2048. return $context;
  2049. }
  2050. return false;
  2051. }
  2052. /**
  2053. * Get the local override (if any) for a given capability in a role in a context
  2054. *
  2055. * @param int $roleid
  2056. * @param int $contextid
  2057. * @param string $capability
  2058. */
  2059. function get_local_override($roleid, $contextid, $capability) {
  2060. global $DB;
  2061. return $DB->get_record('role_capabilities', array('roleid'=>$roleid, 'capability'=>$capability, 'contextid'=>$contextid));
  2062. }
  2063. /**
  2064. * Returns context instance plus related course and cm instances
  2065. * @param int $contextid
  2066. * @return array of ($context, $course, $cm)
  2067. */
  2068. function get_context_info_array($contextid) {
  2069. global $DB;
  2070. $context = get_context_instance_by_id($contextid, MUST_EXIST);
  2071. $course = null;
  2072. $cm = null;
  2073. if ($context->contextlevel == CONTEXT_COURSE) {
  2074. $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
  2075. } else if ($context->contextlevel == CONTEXT_MODULE) {
  2076. $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
  2077. $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
  2078. } else if ($context->contextlevel == CONTEXT_BLOCK) {
  2079. $parentcontexts = get_parent_contexts($context, false);
  2080. $parent = reset($parentcontexts);
  2081. $parent = get_context_instance_by_id($parent);
  2082. if ($parent->contextlevel == CONTEXT_COURSE) {
  2083. $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
  2084. } else if ($parent->contextlevel == CONTEXT_MODULE) {
  2085. $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
  2086. $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
  2087. }
  2088. }
  2089. return array($context, $course, $cm);
  2090. }
  2091. /**
  2092. * Returns current course id or null if outside of course based on context parameter.
  2093. * @param object $context
  2094. * @return int|bool related course id or false
  2095. */
  2096. function get_courseid_from_context($context) {
  2097. if (empty($context->contextlevel)) {
  2098. debugging('Invalid context object specified in get_courseid_from_context() call');
  2099. return false;
  2100. }
  2101. if ($context->contextlevel == CONTEXT_COURSE) {
  2102. return $context->instanceid;
  2103. }
  2104. if ($context->contextlevel < CONTEXT_COURSE) {
  2105. return false;
  2106. }
  2107. if ($context->contextlevel == CONTEXT_MODULE) {
  2108. $parentcontexts = get_parent_contexts($context, false);
  2109. $parent = reset($parentcontexts);
  2110. $parent = get_context_instance_by_id($parent);
  2111. return $parent->instanceid;
  2112. }
  2113. if ($context->contextlevel == CONTEXT_BLOCK) {
  2114. $parentcontexts = get_parent_contexts($context, false);
  2115. $parent = reset($parentcontexts);
  2116. return get_courseid_from_context(get_context_instance_by_id($parent));
  2117. }
  2118. return false;
  2119. }
  2120. //////////////////////////////////////
  2121. // DB TABLE RELATED FUNCTIONS //
  2122. //////////////////////////////////////
  2123. /**
  2124. * function that creates a role
  2125. *
  2126. * @param string $name role name
  2127. * @param string $shortname role short name
  2128. * @param string $description role description
  2129. * @param string $archetype
  2130. * @return int id or dml_exception
  2131. */
  2132. function create_role($name, $shortname, $description, $archetype = '') {
  2133. global $DB;
  2134. if (strpos($archetype, 'moodle/legacy:') !== false) {
  2135. throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
  2136. }
  2137. // verify role archetype actually exists
  2138. $archetypes = get_role_archetypes();
  2139. if (empty($archetypes[$archetype])) {
  2140. $archetype = '';
  2141. }
  2142. // Get the system context.
  2143. $context = get_context_instance(CONTEXT_SYSTEM);
  2144. // Insert the role record.
  2145. $role = new stdClass();
  2146. $role->name = $name;
  2147. $role->shortname = $shortname;
  2148. $role->description = $description;
  2149. $role->archetype = $archetype;
  2150. //find free sortorder number
  2151. $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
  2152. if (empty($role->sortorder)) {
  2153. $role->sortorder = 1;
  2154. }
  2155. $id = $DB->insert_record('role', $role);
  2156. return $id;
  2157. }
  2158. /**
  2159. * Function that deletes a role and cleanups up after it
  2160. *
  2161. * @param int $roleid id of role to delete
  2162. * @return bool always true
  2163. */
  2164. function delete_role($roleid) {
  2165. global $CFG, $DB;
  2166. // first unssign all users
  2167. role_unassign_all(array('roleid'=>$roleid));
  2168. // cleanup all references to this role, ignore errors
  2169. $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
  2170. $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
  2171. $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
  2172. $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
  2173. $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
  2174. $DB->delete_records('role_names', array('roleid'=>$roleid));
  2175. $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
  2176. // finally delete the role itself
  2177. // get this before the name is gone for logging
  2178. $rolename = $DB->get_field('role', 'name', array('id'=>$roleid));
  2179. $DB->delete_records('role', array('id'=>$roleid));
  2180. add_to_log(SITEID, 'role', 'delete', 'admin/roles/action=delete&roleid='.$roleid, $rolename, '');
  2181. return true;
  2182. }
  2183. /**
  2184. * Function to write context specific overrides, or default capabilities.
  2185. *
  2186. * @param string $capability string name
  2187. * @param int $permission CAP_ constants
  2188. * @param int $roleid role id
  2189. * @param int $contextid context id
  2190. * @param bool $overwrite
  2191. * @return bool always true or exception
  2192. */
  2193. function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
  2194. global $USER, $DB;
  2195. if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
  2196. unassign_capability($capability, $roleid, $contextid);
  2197. return true;
  2198. }
  2199. $existing = $DB->get_record('role_capabilities', array('contextid'=>$contextid, 'roleid'=>$roleid, 'capability'=>$capability));
  2200. if ($existing and !$overwrite) { // We want to keep whatever is there already
  2201. return true;
  2202. }
  2203. $cap = new stdClass();
  2204. $cap->contextid = $contextid;
  2205. $cap->roleid = $roleid;
  2206. $cap->capability = $capability;
  2207. $cap->permission = $permission;
  2208. $cap->timemodified = time();
  2209. $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
  2210. if ($existing) {
  2211. $cap->id = $existing->id;
  2212. $DB->update_record('role_capabilities', $cap);
  2213. } else {
  2214. $c = $DB->get_record('context', array('id'=>$contextid));
  2215. $DB->insert_record('role_capabilities', $cap);
  2216. }
  2217. return true;
  2218. }
  2219. /**
  2220. * Unassign a capability from a role.
  2221. *
  2222. * @param string $capability the name of the capability
  2223. * @param int $roleid the role id
  2224. * @param int $contextid null means all contexts
  2225. * @return boolean success or failure
  2226. */
  2227. function unassign_capability($capability, $roleid, $contextid = null) {
  2228. global $DB;
  2229. if (!empty($contextid)) {
  2230. // delete from context rel, if this is the last override in this context
  2231. $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$contextid));
  2232. } else {
  2233. $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
  2234. }
  2235. return true;
  2236. }
  2237. /**
  2238. * Get the roles that have a given capability assigned to it
  2239. *
  2240. * This function does not resolve the actual permission of the capability.
  2241. * It just checks for permissions and overrides.
  2242. * Use get_roles_with_cap_in_context() if resolution is required.
  2243. *
  2244. * @param string $capability - capability name (string)
  2245. * @param string $permission - optional, the permission defined for this capability
  2246. * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
  2247. * @param stdClass $context, null means any
  2248. * @return array of role objects
  2249. */
  2250. function get_roles_with_capability($capability, $permission = null, $context = null) {
  2251. global $DB;
  2252. if ($context) {
  2253. $contexts = get_parent_contexts($context, true);
  2254. list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx000');
  2255. $contextsql = "AND rc.contextid $insql";
  2256. } else {
  2257. $params = array();
  2258. $contextsql = '';
  2259. }
  2260. if ($permission) {
  2261. $permissionsql = " AND rc.permission = :permission";
  2262. $params['permission'] = $permission;
  2263. } else {
  2264. $permissionsql = '';
  2265. }
  2266. $sql = "SELECT r.*
  2267. FROM {role} r
  2268. WHERE r.id IN (SELECT rc.roleid
  2269. FROM {role_capabilities} rc
  2270. WHERE rc.capability = :capname
  2271. $contextsql
  2272. $permissionsql)";
  2273. $params['capname'] = $capability;
  2274. return $DB->get_records_sql($sql, $params);
  2275. }
  2276. /**
  2277. * This function makes a role-assignment (a role for a user in a particular context)
  2278. *
  2279. * @param int $roleid the role of the id
  2280. * @param int $userid userid
  2281. * @param int $contextid id of the context
  2282. * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
  2283. * @prama int $itemid id of enrolment/auth plugin
  2284. * @param string $timemodified defaults to current time
  2285. * @return int new/existing id of the assignment
  2286. */
  2287. function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
  2288. global $USER, $CFG, $DB;
  2289. // first of all detect if somebody is using old style parameters
  2290. if ($contextid === 0 or is_numeric($component)) {
  2291. throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
  2292. }
  2293. // now validate all parameters
  2294. if (empty($roleid)) {
  2295. throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
  2296. }
  2297. if (empty($userid)) {
  2298. throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
  2299. }
  2300. if ($itemid) {
  2301. if (strpos($component, '_') === false) {
  2302. throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
  2303. }
  2304. } else {
  2305. $itemid = 0;
  2306. if ($component !== '' and strpos($component, '_') === false) {
  2307. throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
  2308. }
  2309. }
  2310. if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
  2311. throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
  2312. }
  2313. $context = get_context_instance_by_id($contextid, MUST_EXIST);
  2314. if (!$timemodified) {
  2315. $timemodified = time();
  2316. }
  2317. /// Check for existing entry
  2318. $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
  2319. if ($ras) {
  2320. // role already assigned - this should not happen
  2321. if (count($ras) > 1) {
  2322. //very weird - remove all duplicates!
  2323. $ra = array_shift($ras);
  2324. foreach ($ras as $r) {
  2325. $DB->delete_records('role_assignments', array('id'=>$r->id));
  2326. }
  2327. } else {
  2328. $ra = reset($ras);
  2329. }
  2330. // actually there is no need to update, reset anything or trigger any event, so just return
  2331. return $ra->id;
  2332. }
  2333. // Create a new entry
  2334. $ra = new stdClass();
  2335. $ra->roleid = $roleid;
  2336. $ra->contextid = $context->id;
  2337. $ra->userid = $userid;
  2338. $ra->component = $component;
  2339. $ra->itemid = $itemid;
  2340. $ra->timemodified = $timemodified;
  2341. $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
  2342. $ra->id = $DB->insert_record('role_assignments', $ra);
  2343. // mark context as dirty - again expensive, but needed
  2344. mark_context_dirty($context->path);
  2345. if (!empty($USER->id) && $USER->id == $userid) {
  2346. // If the user is the current user, then do full reload of capabilities too.
  2347. load_all_capabilities();
  2348. }
  2349. events_trigger('role_assigned', $ra);
  2350. return $ra->id;
  2351. }
  2352. /**
  2353. * Removes one role assignment
  2354. *
  2355. * @param int $roleid
  2356. * @param int $userid
  2357. * @param int $contextid
  2358. * @param string $component
  2359. * @param int $itemid
  2360. * @return void
  2361. */
  2362. function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
  2363. global $USER, $CFG, $DB;
  2364. // first make sure the params make sense
  2365. if ($roleid == 0 or $userid == 0 or $contextid == 0) {
  2366. throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
  2367. }
  2368. if ($itemid) {
  2369. if (strpos($component, '_') === false) {
  2370. throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
  2371. }
  2372. } else {
  2373. $itemid = 0;
  2374. if ($component !== '' and strpos($component, '_') === false) {
  2375. throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
  2376. }
  2377. }
  2378. role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
  2379. }
  2380. /**
  2381. * Removes multiple role assignments, parameters may contain:
  2382. * 'roleid', 'userid', 'contextid', 'component', 'enrolid'.
  2383. *
  2384. * @param array $params role assignment parameters
  2385. * @param bool $subcontexts unassign in subcontexts too
  2386. * @param bool $includmanual include manual role assignments too
  2387. * @return void
  2388. */
  2389. function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
  2390. global $USER, $CFG, $DB;
  2391. if (!$params) {
  2392. throw new coding_exception('Missing parameters in role_unsassign_all() call');
  2393. }
  2394. $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
  2395. foreach ($params as $key=>$value) {
  2396. if (!in_array($key, $allowed)) {
  2397. throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
  2398. }
  2399. }
  2400. if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
  2401. throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
  2402. }
  2403. if ($includemanual) {
  2404. if (!isset($params['component']) or $params['component'] === '') {
  2405. throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
  2406. }
  2407. }
  2408. if ($subcontexts) {
  2409. if (empty($params['contextid'])) {
  2410. throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
  2411. }
  2412. }
  2413. $ras = $DB->get_records('role_assignments', $params);
  2414. foreach($ras as $ra) {
  2415. $DB->delete_records('role_assignments', array('id'=>$ra->id));
  2416. if ($context = get_context_instance_by_id($ra->contextid)) {
  2417. // this is a bit expensive but necessary
  2418. mark_context_dirty($context->path);
  2419. /// If the user is the current user, then do full reload of capabilities too.
  2420. if (!empty($USER->id) && $USER->id == $ra->userid) {
  2421. load_all_capabilities();
  2422. }
  2423. }
  2424. events_trigger('role_unassigned', $ra);
  2425. }
  2426. unset($ras);
  2427. // process subcontexts
  2428. if ($subcontexts and $context = get_context_instance_by_id($params['contextid'])) {
  2429. $contexts = get_child_contexts($context);
  2430. $mparams = $params;
  2431. foreach($contexts as $context) {
  2432. $mparams['contextid'] = $context->id;
  2433. $ras = $DB->get_records('role_assignments', $mparams);
  2434. foreach($ras as $ra) {
  2435. $DB->delete_records('role_assignments', array('id'=>$ra->id));
  2436. // this is a bit expensive but necessary
  2437. mark_context_dirty($context->path);
  2438. /// If the user is the current user, then do full reload of capabilities too.
  2439. if (!empty($USER->id) && $USER->id == $ra->userid) {
  2440. load_all_capabilities();
  2441. }
  2442. events_trigger('role_unassigned', $ra);
  2443. }
  2444. }
  2445. }
  2446. // do this once more for all manual role assignments
  2447. if ($includemanual) {
  2448. $params['component'] = '';
  2449. role_unassign_all($params, $subcontexts, false);
  2450. }
  2451. }
  2452. /**
  2453. * Determines if a user is currently logged in
  2454. *
  2455. * @return bool
  2456. */
  2457. function isloggedin() {
  2458. global $USER;
  2459. return (!empty($USER->id));
  2460. }
  2461. /**
  2462. * Determines if a user is logged in as real guest user with username 'guest'.
  2463. *
  2464. * @param int|object $user mixed user object or id, $USER if not specified
  2465. * @return bool true if user is the real guest user, false if not logged in or other user
  2466. */
  2467. function isguestuser($user = null) {
  2468. global $USER, $DB, $CFG;
  2469. // make sure we have the user id cached in config table, because we are going to use it a lot
  2470. if (empty($CFG->siteguest)) {
  2471. if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
  2472. // guest does not exist yet, weird
  2473. return false;
  2474. }
  2475. set_config('siteguest', $guestid);
  2476. }
  2477. if ($user === null) {
  2478. $user = $USER;
  2479. }
  2480. if ($user === null) {
  2481. // happens when setting the $USER
  2482. return false;
  2483. } else if (is_numeric($user)) {
  2484. return ($CFG->siteguest == $user);
  2485. } else if (is_object($user)) {
  2486. if (empty($user->id)) {
  2487. return false; // not logged in means is not be guest
  2488. } else {
  2489. return ($CFG->siteguest == $user->id);
  2490. }
  2491. } else {
  2492. throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
  2493. }
  2494. }
  2495. /**
  2496. * Does user have a (temporary or real) guest access to course?
  2497. *
  2498. * @param stdClass $context
  2499. * @param stdClass|int $user
  2500. * @return bool
  2501. */
  2502. function is_guest($context, $user = null) {
  2503. global $USER;
  2504. // first find the course context
  2505. $coursecontext = get_course_context($context);
  2506. // make sure there is a real user specified
  2507. if ($user === null) {
  2508. $userid = !empty($USER->id) ? $USER->id : 0;
  2509. } else {
  2510. $userid = !empty($user->id) ? $user->id : $user;
  2511. }
  2512. if (isguestuser($userid)) {
  2513. // can not inspect or be enrolled
  2514. return true;
  2515. }
  2516. if (has_capability('moodle/course:view', $coursecontext, $user)) {
  2517. // viewing users appear out of nowhere, they are neither guests nor participants
  2518. return false;
  2519. }
  2520. // consider only real active enrolments here
  2521. if (is_enrolled($coursecontext, $user, '', true)) {
  2522. return false;
  2523. }
  2524. return true;
  2525. }
  2526. /**
  2527. * Returns true if the user has moodle/course:view capability in the course,
  2528. * this is intended for admins, managers (aka small admins), inspectors, etc.
  2529. *
  2530. * @param stdClass $context
  2531. * @param int|object $user, if null $USER is used
  2532. * @param string $withcapability extra capability name
  2533. * @return bool
  2534. */
  2535. function is_viewing($context, $user = null, $withcapability = '') {
  2536. // first find the course context
  2537. $coursecontext = get_course_context($context);
  2538. if (isguestuser($user)) {
  2539. // can not inspect
  2540. return false;
  2541. }
  2542. if (!has_capability('moodle/course:view', $coursecontext, $user)) {
  2543. // admins are allowed to inspect courses
  2544. return false;
  2545. }
  2546. if ($withcapability and !has_capability($withcapability, $context, $user)) {
  2547. // site admins always have the capability, but the enrolment above blocks
  2548. return false;
  2549. }
  2550. return true;
  2551. }
  2552. /**
  2553. * Returns true if user is enrolled (is participating) in course
  2554. * this is intended for students and teachers.
  2555. *
  2556. * @param object $context
  2557. * @param int|object $user, if null $USER is used, otherwise user object or id expected
  2558. * @param string $withcapability extra capability name
  2559. * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  2560. * @return bool
  2561. */
  2562. function is_enrolled($context, $user = null, $withcapability = '', $onlyactive = false) {
  2563. global $USER, $DB;
  2564. // first find the course context
  2565. $coursecontext = get_course_context($context);
  2566. // make sure there is a real user specified
  2567. if ($user === null) {
  2568. $userid = !empty($USER->id) ? $USER->id : 0;
  2569. } else {
  2570. $userid = !empty($user->id) ? $user->id : $user;
  2571. }
  2572. if (empty($userid)) {
  2573. // not-logged-in!
  2574. return false;
  2575. } else if (isguestuser($userid)) {
  2576. // guest account can not be enrolled anywhere
  2577. return false;
  2578. }
  2579. if ($coursecontext->instanceid == SITEID) {
  2580. // everybody participates on frontpage
  2581. } else {
  2582. if ($onlyactive) {
  2583. $sql = "SELECT ue.*
  2584. FROM {user_enrolments} ue
  2585. JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
  2586. JOIN {user} u ON u.id = ue.userid
  2587. WHERE ue.userid = :userid AND ue.status = :active AND e.status = :enabled AND u.deleted = 0";
  2588. $params = array('enabled'=>ENROL_INSTANCE_ENABLED, 'active'=>ENROL_USER_ACTIVE, 'userid'=>$userid, 'courseid'=>$coursecontext->instanceid);
  2589. // this result should be very small, better not do the complex time checks in sql for now ;-)
  2590. $enrolments = $DB->get_records_sql($sql, $params);
  2591. $now = time();
  2592. // make sure the enrol period is ok
  2593. $result = false;
  2594. foreach ($enrolments as $e) {
  2595. if ($e->timestart > $now) {
  2596. continue;
  2597. }
  2598. if ($e->timeend and $e->timeend < $now) {
  2599. continue;
  2600. }
  2601. $result = true;
  2602. break;
  2603. }
  2604. if (!$result) {
  2605. return false;
  2606. }
  2607. } else {
  2608. // any enrolment is good for us here, even outdated, disabled or inactive
  2609. $sql = "SELECT 'x'
  2610. FROM {user_enrolments} ue
  2611. JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :courseid)
  2612. JOIN {user} u ON u.id = ue.userid
  2613. WHERE ue.userid = :userid AND u.deleted = 0";
  2614. $params = array('userid'=>$userid, 'courseid'=>$coursecontext->instanceid);
  2615. if (!$DB->record_exists_sql($sql, $params)) {
  2616. return false;
  2617. }
  2618. }
  2619. }
  2620. if ($withcapability and !has_capability($withcapability, $context, $userid)) {
  2621. return false;
  2622. }
  2623. return true;
  2624. }
  2625. /**
  2626. * Returns true if the user is able to access the course.
  2627. *
  2628. * This function is in no way, shape, or form a substitute for require_login.
  2629. * It should only be used in circumstances where it is not possible to call require_login
  2630. * such as the navigation.
  2631. *
  2632. * This function checks many of the methods of access to a course such as the view
  2633. * capability, enrollments, and guest access. It also makes use of the cache
  2634. * generated by require_login for guest access.
  2635. *
  2636. * The flags within the $USER object that are used here should NEVER be used outside
  2637. * of this function can_access_course and require_login. Doing so WILL break future
  2638. * versions.
  2639. *
  2640. * @global moodle_database $DB
  2641. * @param stdClass $context
  2642. * @param stdClass|null $user
  2643. * @param string $withcapability Check for this capability as well.
  2644. * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  2645. * @param boolean $trustcache If set to false guest access will always be checked
  2646. * against the enrolment plugins from the course, rather
  2647. * than the cache generated by require_login.
  2648. * @return boolean Returns true if the user is able to access the course
  2649. */
  2650. function can_access_course($context, $user = null, $withcapability = '', $onlyactive = false, $trustcache = true) {
  2651. global $DB, $USER;
  2652. $coursecontext = get_course_context($context);
  2653. $courseid = $coursecontext->instanceid;
  2654. // First check the obvious, is the user viewing or is the user enrolled.
  2655. if (is_viewing($coursecontext, $user, $withcapability) || is_enrolled($coursecontext, $user, $withcapability, $onlyactive)) {
  2656. // How easy was that!
  2657. return true;
  2658. }
  2659. $access = false;
  2660. if (!isset($USER->enrol)) {
  2661. // Cache hasn't been generated yet so we can't trust it
  2662. $trustcache = false;
  2663. /**
  2664. * These flags within the $USER object should NEVER be used outside of this
  2665. * function can_access_course and the function require_login.
  2666. * Doing so WILL break future versions!!!!
  2667. */
  2668. $USER->enrol = array();
  2669. $USER->enrol['enrolled'] = array();
  2670. $USER->enrol['tempguest'] = array();
  2671. }
  2672. // If we don't trust the cache we need to check with the courses enrolment
  2673. // plugin instances to see if the user can access the course as a guest.
  2674. if (!$trustcache) {
  2675. // Ok, off to the database we go!
  2676. $instances = $DB->get_records('enrol', array('courseid'=>$courseid, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
  2677. $enrols = enrol_get_plugins(true);
  2678. foreach($instances as $instance) {
  2679. if (!isset($enrols[$instance->enrol])) {
  2680. continue;
  2681. }
  2682. $until = $enrols[$instance->enrol]->try_guestaccess($instance);
  2683. if ($until !== false) {
  2684. // Never use me anywhere but here and require_login
  2685. $USER->enrol['tempguest'][$courseid] = $until;
  2686. $access = true;
  2687. break;
  2688. }
  2689. }
  2690. }
  2691. // If we don't already have access (from above) check the cache and see whether
  2692. // there is record of it in there.
  2693. if (!$access && isset($USER->enrol['tempguest'][$courseid])) {
  2694. // Never use me anywhere but here and require_login
  2695. if ($USER->enrol['tempguest'][$courseid] == 0) {
  2696. $access = true;
  2697. } else if ($USER->enrol['tempguest'][$courseid] > time()) {
  2698. $access = true;
  2699. } else {
  2700. //expired
  2701. unset($USER->enrol['tempguest'][$courseid]);
  2702. }
  2703. }
  2704. return $access;
  2705. }
  2706. /**
  2707. * Returns array with sql code and parameters returning all ids
  2708. * of users enrolled into course.
  2709. *
  2710. * This function is using 'eu[0-9]+_' prefix for table names and parameters.
  2711. *
  2712. * @param object $context
  2713. * @param string $withcapability
  2714. * @param int $groupid 0 means ignore groups, any other value limits the result by group id
  2715. * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  2716. * @return array list($sql, $params)
  2717. */
  2718. function get_enrolled_sql($context, $withcapability = '', $groupid = 0, $onlyactive = false) {
  2719. global $DB, $CFG;
  2720. // use unique prefix just in case somebody makes some SQL magic with the result
  2721. static $i = 0;
  2722. $i++;
  2723. $prefix = 'eu'.$i.'_';
  2724. // first find the course context
  2725. $coursecontext = get_course_context($context);
  2726. $isfrontpage = ($coursecontext->instanceid == SITEID);
  2727. $joins = array();
  2728. $wheres = array();
  2729. $params = array();
  2730. list($contextids, $contextpaths) = get_context_info_list($context);
  2731. // get all relevant capability info for all roles
  2732. if ($withcapability) {
  2733. list($incontexts, $cparams) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'ctx00');
  2734. $cparams['cap'] = $withcapability;
  2735. $defs = array();
  2736. $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.path
  2737. FROM {role_capabilities} rc
  2738. JOIN {context} ctx on rc.contextid = ctx.id
  2739. WHERE rc.contextid $incontexts AND rc.capability = :cap";
  2740. $rcs = $DB->get_records_sql($sql, $cparams);
  2741. foreach ($rcs as $rc) {
  2742. $defs[$rc->path][$rc->roleid] = $rc->permission;
  2743. }
  2744. $access = array();
  2745. if (!empty($defs)) {
  2746. foreach ($contextpaths as $path) {
  2747. if (empty($defs[$path])) {
  2748. continue;
  2749. }
  2750. foreach($defs[$path] as $roleid => $perm) {
  2751. if ($perm == CAP_PROHIBIT) {
  2752. $access[$roleid] = CAP_PROHIBIT;
  2753. continue;
  2754. }
  2755. if (!isset($access[$roleid])) {
  2756. $access[$roleid] = (int)$perm;
  2757. }
  2758. }
  2759. }
  2760. }
  2761. unset($defs);
  2762. // make lists of roles that are needed and prohibited
  2763. $needed = array(); // one of these is enough
  2764. $prohibited = array(); // must not have any of these
  2765. foreach ($access as $roleid => $perm) {
  2766. if ($perm == CAP_PROHIBIT) {
  2767. unset($needed[$roleid]);
  2768. $prohibited[$roleid] = true;
  2769. } else if ($perm == CAP_ALLOW and empty($prohibited[$roleid])) {
  2770. $needed[$roleid] = true;
  2771. }
  2772. }
  2773. $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : null;
  2774. $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : null;
  2775. $nobody = false;
  2776. if ($isfrontpage) {
  2777. if (!empty($prohibited[$defaultuserroleid]) or !empty($prohibited[$defaultfrontpageroleid])) {
  2778. $nobody = true;
  2779. } else if (!empty($needed[$defaultuserroleid]) or !empty($needed[$defaultfrontpageroleid])) {
  2780. // everybody not having prohibit has the capability
  2781. $needed = array();
  2782. } else if (empty($needed)) {
  2783. $nobody = true;
  2784. }
  2785. } else {
  2786. if (!empty($prohibited[$defaultuserroleid])) {
  2787. $nobody = true;
  2788. } else if (!empty($needed[$defaultuserroleid])) {
  2789. // everybody not having prohibit has the capability
  2790. $needed = array();
  2791. } else if (empty($needed)) {
  2792. $nobody = true;
  2793. }
  2794. }
  2795. if ($nobody) {
  2796. // nobody can match so return some SQL that does not return any results
  2797. $wheres[] = "1 = 2";
  2798. } else {
  2799. if ($needed) {
  2800. $ctxids = implode(',', $contextids);
  2801. $roleids = implode(',', array_keys($needed));
  2802. $joins[] = "JOIN {role_assignments} {$prefix}ra3 ON ({$prefix}ra3.userid = {$prefix}u.id AND {$prefix}ra3.roleid IN ($roleids) AND {$prefix}ra3.contextid IN ($ctxids))";
  2803. }
  2804. if ($prohibited) {
  2805. $ctxids = implode(',', $contextids);
  2806. $roleids = implode(',', array_keys($prohibited));
  2807. $joins[] = "LEFT JOIN {role_assignments} {$prefix}ra4 ON ({$prefix}ra4.userid = {$prefix}u.id AND {$prefix}ra4.roleid IN ($roleids) AND {$prefix}ra4.contextid IN ($ctxids))";
  2808. $wheres[] = "{$prefix}ra4.id IS NULL";
  2809. }
  2810. if ($groupid) {
  2811. $joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}u.id AND {$prefix}gm.groupid = :{$prefix}gmid)";
  2812. $params["{$prefix}gmid"] = $groupid;
  2813. }
  2814. }
  2815. } else {
  2816. if ($groupid) {
  2817. $joins[] = "JOIN {groups_members} {$prefix}gm ON ({$prefix}gm.userid = {$prefix}u.id AND {$prefix}gm.groupid = :{$prefix}gmid)";
  2818. $params["{$prefix}gmid"] = $groupid;
  2819. }
  2820. }
  2821. $wheres[] = "{$prefix}u.deleted = 0 AND {$prefix}u.id <> :{$prefix}guestid";
  2822. $params["{$prefix}guestid"] = $CFG->siteguest;
  2823. if ($isfrontpage) {
  2824. // all users are "enrolled" on the frontpage
  2825. } else {
  2826. $joins[] = "JOIN {user_enrolments} {$prefix}ue ON {$prefix}ue.userid = {$prefix}u.id";
  2827. $joins[] = "JOIN {enrol} {$prefix}e ON ({$prefix}e.id = {$prefix}ue.enrolid AND {$prefix}e.courseid = :{$prefix}courseid)";
  2828. $params[$prefix.'courseid'] = $coursecontext->instanceid;
  2829. if ($onlyactive) {
  2830. $wheres[] = "{$prefix}ue.status = :{$prefix}active AND {$prefix}e.status = :{$prefix}enabled";
  2831. $wheres[] = "{$prefix}ue.timestart < :{$prefix}now1 AND ({$prefix}ue.timeend = 0 OR {$prefix}ue.timeend > :{$prefix}now2)";
  2832. $now = round(time(), -2); // rounding helps caching in DB
  2833. $params = array_merge($params, array($prefix.'enabled'=>ENROL_INSTANCE_ENABLED,
  2834. $prefix.'active'=>ENROL_USER_ACTIVE,
  2835. $prefix.'now1'=>$now, $prefix.'now2'=>$now));
  2836. }
  2837. }
  2838. $joins = implode("\n", $joins);
  2839. $wheres = "WHERE ".implode(" AND ", $wheres);
  2840. $sql = "SELECT DISTINCT {$prefix}u.id
  2841. FROM {user} {$prefix}u
  2842. $joins
  2843. $wheres";
  2844. return array($sql, $params);
  2845. }
  2846. /**
  2847. * Returns list of users enrolled into course.
  2848. * @param object $context
  2849. * @param string $withcapability
  2850. * @param int $groupid 0 means ignore groups, any other value limits the result by group id
  2851. * @param string $userfields requested user record fields
  2852. * @param string $orderby
  2853. * @param int $limitfrom return a subset of records, starting at this point (optional, required if $limitnum is set).
  2854. * @param int $limitnum return a subset comprising this many records (optional, required if $limitfrom is set).
  2855. * @return array of user records
  2856. */
  2857. function get_enrolled_users($context, $withcapability = '', $groupid = 0, $userfields = 'u.*', $orderby = '', $limitfrom = 0, $limitnum = 0) {
  2858. global $DB;
  2859. list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid);
  2860. $sql = "SELECT $userfields
  2861. FROM {user} u
  2862. JOIN ($esql) je ON je.id = u.id
  2863. WHERE u.deleted = 0";
  2864. if ($orderby) {
  2865. $sql = "$sql ORDER BY $orderby";
  2866. } else {
  2867. $sql = "$sql ORDER BY u.lastname ASC, u.firstname ASC";
  2868. }
  2869. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  2870. }
  2871. /**
  2872. * Counts list of users enrolled into course (as per above function)
  2873. * @param object $context
  2874. * @param string $withcapability
  2875. * @param int $groupid 0 means ignore groups, any other value limits the result by group id
  2876. * @return array of user records
  2877. */
  2878. function count_enrolled_users($context, $withcapability = '', $groupid = 0) {
  2879. global $DB;
  2880. list($esql, $params) = get_enrolled_sql($context, $withcapability, $groupid);
  2881. $sql = "SELECT count(u.id)
  2882. FROM {user} u
  2883. JOIN ($esql) je ON je.id = u.id
  2884. WHERE u.deleted = 0";
  2885. return $DB->count_records_sql($sql, $params);
  2886. }
  2887. /**
  2888. * Loads the capability definitions for the component (from file).
  2889. *
  2890. * Loads the capability definitions for the component (from file). If no
  2891. * capabilities are defined for the component, we simply return an empty array.
  2892. *
  2893. * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
  2894. * @return array array of capabilities
  2895. */
  2896. function load_capability_def($component) {
  2897. $defpath = get_component_directory($component).'/db/access.php';
  2898. $capabilities = array();
  2899. if (file_exists($defpath)) {
  2900. require($defpath);
  2901. if (!empty(${$component.'_capabilities'})) {
  2902. // BC capability array name
  2903. // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
  2904. debugging('componentname_capabilities array is deprecated, please use capabilities array only in access.php files');
  2905. $capabilities = ${$component.'_capabilities'};
  2906. }
  2907. }
  2908. return $capabilities;
  2909. }
  2910. /**
  2911. * Gets the capabilities that have been cached in the database for this component.
  2912. * @param string $component - examples: 'moodle', 'mod_forum'
  2913. * @return array array of capabilities
  2914. */
  2915. function get_cached_capabilities($component = 'moodle') {
  2916. global $DB;
  2917. return $DB->get_records('capabilities', array('component'=>$component));
  2918. }
  2919. /**
  2920. * Returns default capabilities for given role archetype.
  2921. * @param string $archetype role archetype
  2922. * @return array
  2923. */
  2924. function get_default_capabilities($archetype) {
  2925. global $DB;
  2926. if (!$archetype) {
  2927. return array();
  2928. }
  2929. $alldefs = array();
  2930. $defaults = array();
  2931. $components = array();
  2932. $allcaps = $DB->get_records('capabilities');
  2933. foreach ($allcaps as $cap) {
  2934. if (!in_array($cap->component, $components)) {
  2935. $components[] = $cap->component;
  2936. $alldefs = array_merge($alldefs, load_capability_def($cap->component));
  2937. }
  2938. }
  2939. foreach($alldefs as $name=>$def) {
  2940. // Use array 'archetypes if available. Only if not specified, use 'legacy'.
  2941. if (isset($def['archetypes'])) {
  2942. if (isset($def['archetypes'][$archetype])) {
  2943. $defaults[$name] = $def['archetypes'][$archetype];
  2944. }
  2945. // 'legacy' is for backward compatibility with 1.9 access.php
  2946. } else {
  2947. if (isset($def['legacy'][$archetype])) {
  2948. $defaults[$name] = $def['legacy'][$archetype];
  2949. }
  2950. }
  2951. }
  2952. return $defaults;
  2953. }
  2954. /**
  2955. * Reset role capabilities to default according to selected role archetype.
  2956. * If no archetype selected, removes all capabilities.
  2957. * @param int $roleid
  2958. * @return void
  2959. */
  2960. function reset_role_capabilities($roleid) {
  2961. global $DB;
  2962. $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
  2963. $defaultcaps = get_default_capabilities($role->archetype);
  2964. $sitecontext = get_context_instance(CONTEXT_SYSTEM);
  2965. $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
  2966. foreach($defaultcaps as $cap=>$permission) {
  2967. assign_capability($cap, $permission, $roleid, $sitecontext->id);
  2968. }
  2969. }
  2970. /**
  2971. * Updates the capabilities table with the component capability definitions.
  2972. * If no parameters are given, the function updates the core moodle
  2973. * capabilities.
  2974. *
  2975. * Note that the absence of the db/access.php capabilities definition file
  2976. * will cause any stored capabilities for the component to be removed from
  2977. * the database.
  2978. *
  2979. * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
  2980. * @return boolean true if success, exception in case of any problems
  2981. */
  2982. function update_capabilities($component = 'moodle') {
  2983. global $DB, $OUTPUT, $ACCESSLIB_PRIVATE;
  2984. $storedcaps = array();
  2985. $filecaps = load_capability_def($component);
  2986. $cachedcaps = get_cached_capabilities($component);
  2987. if ($cachedcaps) {
  2988. foreach ($cachedcaps as $cachedcap) {
  2989. array_push($storedcaps, $cachedcap->name);
  2990. // update risk bitmasks and context levels in existing capabilities if needed
  2991. if (array_key_exists($cachedcap->name, $filecaps)) {
  2992. if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
  2993. $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
  2994. }
  2995. if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
  2996. $updatecap = new stdClass();
  2997. $updatecap->id = $cachedcap->id;
  2998. $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
  2999. $DB->update_record('capabilities', $updatecap);
  3000. }
  3001. if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
  3002. $updatecap = new stdClass();
  3003. $updatecap->id = $cachedcap->id;
  3004. $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
  3005. $DB->update_record('capabilities', $updatecap);
  3006. }
  3007. if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
  3008. $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
  3009. }
  3010. if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
  3011. $updatecap = new stdClass();
  3012. $updatecap->id = $cachedcap->id;
  3013. $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
  3014. $DB->update_record('capabilities', $updatecap);
  3015. }
  3016. }
  3017. }
  3018. }
  3019. // Are there new capabilities in the file definition?
  3020. $newcaps = array();
  3021. foreach ($filecaps as $filecap => $def) {
  3022. if (!$storedcaps ||
  3023. ($storedcaps && in_array($filecap, $storedcaps) === false)) {
  3024. if (!array_key_exists('riskbitmask', $def)) {
  3025. $def['riskbitmask'] = 0; // no risk if not specified
  3026. }
  3027. $newcaps[$filecap] = $def;
  3028. }
  3029. }
  3030. // Add new capabilities to the stored definition.
  3031. foreach ($newcaps as $capname => $capdef) {
  3032. $capability = new stdClass();
  3033. $capability->name = $capname;
  3034. $capability->captype = $capdef['captype'];
  3035. $capability->contextlevel = $capdef['contextlevel'];
  3036. $capability->component = $component;
  3037. $capability->riskbitmask = $capdef['riskbitmask'];
  3038. $DB->insert_record('capabilities', $capability, false);
  3039. if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $storedcaps)){
  3040. if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
  3041. foreach ($rolecapabilities as $rolecapability){
  3042. //assign_capability will update rather than insert if capability exists
  3043. if (!assign_capability($capname, $rolecapability->permission,
  3044. $rolecapability->roleid, $rolecapability->contextid, true)){
  3045. echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
  3046. }
  3047. }
  3048. }
  3049. // we ignore archetype key if we have cloned permissions
  3050. } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
  3051. assign_legacy_capabilities($capname, $capdef['archetypes']);
  3052. // 'legacy' is for backward compatibility with 1.9 access.php
  3053. } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
  3054. assign_legacy_capabilities($capname, $capdef['legacy']);
  3055. }
  3056. }
  3057. // Are there any capabilities that have been removed from the file
  3058. // definition that we need to delete from the stored capabilities and
  3059. // role assignments?
  3060. capabilities_cleanup($component, $filecaps);
  3061. // reset static caches
  3062. $ACCESSLIB_PRIVATE->capabilities = null;
  3063. return true;
  3064. }
  3065. /**
  3066. * Deletes cached capabilities that are no longer needed by the component.
  3067. * Also unassigns these capabilities from any roles that have them.
  3068. *
  3069. * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
  3070. * @param array $newcapdef array of the new capability definitions that will be
  3071. * compared with the cached capabilities
  3072. * @return int number of deprecated capabilities that have been removed
  3073. */
  3074. function capabilities_cleanup($component, $newcapdef = null) {
  3075. global $DB;
  3076. $removedcount = 0;
  3077. if ($cachedcaps = get_cached_capabilities($component)) {
  3078. foreach ($cachedcaps as $cachedcap) {
  3079. if (empty($newcapdef) ||
  3080. array_key_exists($cachedcap->name, $newcapdef) === false) {
  3081. // Remove from capabilities cache.
  3082. $DB->delete_records('capabilities', array('name'=>$cachedcap->name));
  3083. $removedcount++;
  3084. // Delete from roles.
  3085. if ($roles = get_roles_with_capability($cachedcap->name)) {
  3086. foreach($roles as $role) {
  3087. if (!unassign_capability($cachedcap->name, $role->id)) {
  3088. print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
  3089. }
  3090. }
  3091. }
  3092. } // End if.
  3093. }
  3094. }
  3095. return $removedcount;
  3096. }
  3097. //////////////////
  3098. // UI FUNCTIONS //
  3099. //////////////////
  3100. /**
  3101. * @param integer $contextlevel $context->context level. One of the CONTEXT_... constants.
  3102. * @return string the name for this type of context.
  3103. */
  3104. function get_contextlevel_name($contextlevel) {
  3105. static $strcontextlevels = null;
  3106. if (is_null($strcontextlevels)) {
  3107. $strcontextlevels = array(
  3108. CONTEXT_SYSTEM => get_string('coresystem'),
  3109. CONTEXT_USER => get_string('user'),
  3110. CONTEXT_COURSECAT => get_string('category'),
  3111. CONTEXT_COURSE => get_string('course'),
  3112. CONTEXT_MODULE => get_string('activitymodule'),
  3113. CONTEXT_BLOCK => get_string('block')
  3114. );
  3115. }
  3116. return $strcontextlevels[$contextlevel];
  3117. }
  3118. /**
  3119. * Prints human readable context identifier.
  3120. *
  3121. * @param object $context the context.
  3122. * @param boolean $withprefix whether to prefix the name of the context with the
  3123. * type of context, e.g. User, Course, Forum, etc.
  3124. * @param boolean $short whether to user the short name of the thing. Only applies
  3125. * to course contexts
  3126. * @return string the human readable context name.
  3127. */
  3128. function print_context_name($context, $withprefix = true, $short = false) {
  3129. global $DB;
  3130. $name = '';
  3131. switch ($context->contextlevel) {
  3132. case CONTEXT_SYSTEM:
  3133. $name = get_string('coresystem');
  3134. break;
  3135. case CONTEXT_USER:
  3136. if ($user = $DB->get_record('user', array('id'=>$context->instanceid))) {
  3137. if ($withprefix){
  3138. $name = get_string('user').': ';
  3139. }
  3140. $name .= fullname($user);
  3141. }
  3142. break;
  3143. case CONTEXT_COURSECAT:
  3144. if ($category = $DB->get_record('course_categories', array('id'=>$context->instanceid))) {
  3145. if ($withprefix){
  3146. $name = get_string('category').': ';
  3147. }
  3148. $name .=format_string($category->name);
  3149. }
  3150. break;
  3151. case CONTEXT_COURSE:
  3152. if ($context->instanceid == SITEID) {
  3153. $name = get_string('frontpage', 'admin');
  3154. } else {
  3155. if ($course = $DB->get_record('course', array('id'=>$context->instanceid))) {
  3156. if ($withprefix){
  3157. $name = get_string('course').': ';
  3158. }
  3159. if ($short){
  3160. $name .= format_string($course->shortname);
  3161. } else {
  3162. $name .= format_string($course->fullname);
  3163. }
  3164. }
  3165. }
  3166. break;
  3167. case CONTEXT_MODULE:
  3168. if ($cm = $DB->get_record_sql('SELECT cm.*, md.name AS modname FROM {course_modules} cm ' .
  3169. 'JOIN {modules} md ON md.id = cm.module WHERE cm.id = ?', array($context->instanceid))) {
  3170. if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
  3171. if ($withprefix){
  3172. $name = get_string('modulename', $cm->modname).': ';
  3173. }
  3174. $name .= $mod->name;
  3175. }
  3176. }
  3177. break;
  3178. case CONTEXT_BLOCK:
  3179. if ($blockinstance = $DB->get_record('block_instances', array('id'=>$context->instanceid))) {
  3180. global $CFG;
  3181. require_once("$CFG->dirroot/blocks/moodleblock.class.php");
  3182. require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php");
  3183. $blockname = "block_$blockinstance->blockname";
  3184. if ($blockobject = new $blockname()) {
  3185. if ($withprefix){
  3186. $name = get_string('block').': ';
  3187. }
  3188. $name .= $blockobject->title;
  3189. }
  3190. }
  3191. break;
  3192. default:
  3193. print_error('unknowncontext');
  3194. return false;
  3195. }
  3196. return $name;
  3197. }
  3198. /**
  3199. * Get a URL for a context, if there is a natural one. For example, for
  3200. * CONTEXT_COURSE, this is the course page. For CONTEXT_USER it is the
  3201. * user profile page.
  3202. *
  3203. * @param object $context the context.
  3204. * @return moodle_url
  3205. */
  3206. function get_context_url($context) {
  3207. global $COURSE, $DB;
  3208. switch ($context->contextlevel) {
  3209. case CONTEXT_USER:
  3210. if ($COURSE->id == SITEID) {
  3211. $url = new moodle_url('/user/profile.php', array('id'=>$context->instanceid));
  3212. } else {
  3213. $url = new moodle_url('/user/view.php', array('id'=>$context->instanceid, 'courseid'=>$COURSE->id));
  3214. }
  3215. return $url;;
  3216. case CONTEXT_COURSECAT: // Coursecat -> coursecat or site
  3217. return new moodle_url('/course/category.php', array('id'=>$context->instanceid));
  3218. case CONTEXT_COURSE: // 1 to 1 to course cat
  3219. if ($context->instanceid != SITEID) {
  3220. return new moodle_url('/course/view.php', array('id'=>$context->instanceid));
  3221. }
  3222. break;
  3223. case CONTEXT_MODULE: // 1 to 1 to course
  3224. if ($modname = $DB->get_field_sql('SELECT md.name AS modname FROM {course_modules} cm ' .
  3225. 'JOIN {modules} md ON md.id = cm.module WHERE cm.id = ?', array($context->instanceid))) {
  3226. return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$context->instanceid));
  3227. }
  3228. break;
  3229. case CONTEXT_BLOCK:
  3230. $parentcontexts = get_parent_contexts($context, false);
  3231. $parent = reset($parentcontexts);
  3232. $parent = get_context_instance_by_id($parent);
  3233. return get_context_url($parent);
  3234. }
  3235. return new moodle_url('/');
  3236. }
  3237. /**
  3238. * Returns an array of all the known types of risk
  3239. * The array keys can be used, for example as CSS class names, or in calls to
  3240. * print_risk_icon. The values are the corresponding RISK_ constants.
  3241. *
  3242. * @return array all the known types of risk.
  3243. */
  3244. function get_all_risks() {
  3245. return array(
  3246. 'riskmanagetrust' => RISK_MANAGETRUST,
  3247. 'riskconfig' => RISK_CONFIG,
  3248. 'riskxss' => RISK_XSS,
  3249. 'riskpersonal' => RISK_PERSONAL,
  3250. 'riskspam' => RISK_SPAM,
  3251. 'riskdataloss' => RISK_DATALOSS,
  3252. );
  3253. }
  3254. /**
  3255. * Return a link to moodle docs for a given capability name
  3256. *
  3257. * @param object $capability a capability - a row from the mdl_capabilities table.
  3258. * @return string the human-readable capability name as a link to Moodle Docs.
  3259. */
  3260. function get_capability_docs_link($capability) {
  3261. global $CFG;
  3262. $url = get_docs_url('Capabilities/' . $capability->name);
  3263. return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
  3264. }
  3265. /**
  3266. * Extracts the relevant capabilities given a contextid.
  3267. * All case based, example an instance of forum context.
  3268. * Will fetch all forum related capabilities, while course contexts
  3269. * Will fetch all capabilities
  3270. *
  3271. * capabilities
  3272. * `name` varchar(150) NOT NULL,
  3273. * `captype` varchar(50) NOT NULL,
  3274. * `contextlevel` int(10) NOT NULL,
  3275. * `component` varchar(100) NOT NULL,
  3276. *
  3277. * @param object context
  3278. * @return array
  3279. */
  3280. function fetch_context_capabilities($context) {
  3281. global $DB, $CFG;
  3282. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  3283. $params = array();
  3284. switch ($context->contextlevel) {
  3285. case CONTEXT_SYSTEM: // all
  3286. $SQL = "SELECT *
  3287. FROM {capabilities}";
  3288. break;
  3289. case CONTEXT_USER:
  3290. $extracaps = array('moodle/grade:viewall');
  3291. list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap0');
  3292. $SQL = "SELECT *
  3293. FROM {capabilities}
  3294. WHERE contextlevel = ".CONTEXT_USER."
  3295. OR name $extra";
  3296. break;
  3297. case CONTEXT_COURSECAT: // course category context and bellow
  3298. $SQL = "SELECT *
  3299. FROM {capabilities}
  3300. WHERE contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
  3301. break;
  3302. case CONTEXT_COURSE: // course context and bellow
  3303. $SQL = "SELECT *
  3304. FROM {capabilities}
  3305. WHERE contextlevel IN (".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
  3306. break;
  3307. case CONTEXT_MODULE: // mod caps
  3308. $cm = $DB->get_record('course_modules', array('id'=>$context->instanceid));
  3309. $module = $DB->get_record('modules', array('id'=>$cm->module));
  3310. $subcaps = array();
  3311. $subpluginsfile = "$CFG->dirroot/mod/$module->name/db/subplugins.php";
  3312. if (file_exists($subpluginsfile)) {
  3313. $subplugins = array(); // should be redefined in the file
  3314. include($subpluginsfile);
  3315. if (!empty($subplugins)) {
  3316. foreach (array_keys($subplugins) as $subplugintype) {
  3317. foreach (array_keys(get_plugin_list($subplugintype)) as $subpluginname) {
  3318. $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
  3319. }
  3320. }
  3321. }
  3322. }
  3323. $modfile = "$CFG->dirroot/mod/$module->name/lib.php";
  3324. if (file_exists($modfile)) {
  3325. include_once($modfile);
  3326. $modfunction = $module->name.'_get_extra_capabilities';
  3327. if (function_exists($modfunction)) {
  3328. $extracaps = $modfunction();
  3329. }
  3330. }
  3331. if (empty($extracaps)) {
  3332. $extracaps = array();
  3333. }
  3334. $extracaps = array_merge($subcaps, $extracaps);
  3335. // All modules allow viewhiddenactivities. This is so you can hide
  3336. // the module then override to allow specific roles to see it.
  3337. // The actual check is in course page so not module-specific
  3338. $extracaps[]="moodle/course:viewhiddenactivities";
  3339. list($extra, $params) = $DB->get_in_or_equal(
  3340. $extracaps, SQL_PARAMS_NAMED, 'cap0');
  3341. $extra = "OR name $extra";
  3342. $SQL = "SELECT *
  3343. FROM {capabilities}
  3344. WHERE (contextlevel = ".CONTEXT_MODULE."
  3345. AND component = :component)
  3346. $extra";
  3347. $params['component'] = "mod_$module->name";
  3348. break;
  3349. case CONTEXT_BLOCK: // block caps
  3350. $bi = $DB->get_record('block_instances', array('id' => $context->instanceid));
  3351. $extra = '';
  3352. $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities');
  3353. if ($extracaps) {
  3354. list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap0');
  3355. $extra = "OR name $extra";
  3356. }
  3357. $SQL = "SELECT *
  3358. FROM {capabilities}
  3359. WHERE (contextlevel = ".CONTEXT_BLOCK."
  3360. AND component = :component)
  3361. $extra";
  3362. $params['component'] = 'block_' . $bi->blockname;
  3363. break;
  3364. default:
  3365. return false;
  3366. }
  3367. if (!$records = $DB->get_records_sql($SQL.' '.$sort, $params)) {
  3368. $records = array();
  3369. }
  3370. return $records;
  3371. }
  3372. /**
  3373. * This function pulls out all the resolved capabilities (overrides and
  3374. * defaults) of a role used in capability overrides in contexts at a given
  3375. * context.
  3376. *
  3377. * @param obj $context
  3378. * @param int $roleid
  3379. * @param string $cap capability, optional, defaults to ''
  3380. * @return array of capabilities
  3381. */
  3382. function role_context_capabilities($roleid, $context, $cap = '') {
  3383. global $DB;
  3384. $contexts = get_parent_contexts($context);
  3385. $contexts[] = $context->id;
  3386. $contexts = '('.implode(',', $contexts).')';
  3387. $params = array($roleid);
  3388. if ($cap) {
  3389. $search = " AND rc.capability = ? ";
  3390. $params[] = $cap;
  3391. } else {
  3392. $search = '';
  3393. }
  3394. $sql = "SELECT rc.*
  3395. FROM {role_capabilities} rc, {context} c
  3396. WHERE rc.contextid in $contexts
  3397. AND rc.roleid = ?
  3398. AND rc.contextid = c.id $search
  3399. ORDER BY c.contextlevel DESC, rc.capability DESC";
  3400. $capabilities = array();
  3401. if ($records = $DB->get_records_sql($sql, $params)) {
  3402. // We are traversing via reverse order.
  3403. foreach ($records as $record) {
  3404. // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
  3405. if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
  3406. $capabilities[$record->capability] = $record->permission;
  3407. }
  3408. }
  3409. }
  3410. return $capabilities;
  3411. }
  3412. /**
  3413. * Recursive function which, given a context, find all parent context ids,
  3414. * and return the array in reverse order, i.e. parent first, then grand
  3415. * parent, etc.
  3416. *
  3417. * @param object $context
  3418. * @param bool $capability optional, defaults to false
  3419. * @return array
  3420. */
  3421. function get_parent_contexts($context, $includeself = false) {
  3422. if ($context->path == '') {
  3423. return array();
  3424. }
  3425. $parentcontexts = substr($context->path, 1); // kill leading slash
  3426. $parentcontexts = explode('/', $parentcontexts);
  3427. if (!$includeself) {
  3428. array_pop($parentcontexts); // and remove its own id
  3429. }
  3430. return array_reverse($parentcontexts);
  3431. }
  3432. /**
  3433. * Return the id of the parent of this context, or false if there is no parent (only happens if this
  3434. * is the site context.)
  3435. *
  3436. * @param object $context
  3437. * @return integer the id of the parent context.
  3438. */
  3439. function get_parent_contextid($context) {
  3440. $parentcontexts = get_parent_contexts($context);
  3441. if (count($parentcontexts) == 0) {
  3442. return false;
  3443. }
  3444. return array_shift($parentcontexts);
  3445. }
  3446. /**
  3447. * Constructs array with contextids as first parameter and context paths,
  3448. * in both cases bottom top including self.
  3449. *
  3450. * @param object $context
  3451. * @return array
  3452. */
  3453. function get_context_info_list($context) {
  3454. $contextids = explode('/', ltrim($context->path, '/'));
  3455. $contextpaths = array();
  3456. $contextids2 = $contextids;
  3457. while ($contextids2) {
  3458. $contextpaths[] = '/' . implode('/', $contextids2);
  3459. array_pop($contextids2);
  3460. }
  3461. return array($contextids, $contextpaths);
  3462. }
  3463. /**
  3464. * Find course context
  3465. * @param object $context - course or lower context
  3466. * @return object context of the enclosing course, throws exception when related course context can not be found
  3467. */
  3468. function get_course_context($context) {
  3469. if (empty($context->contextlevel)) {
  3470. throw new coding_exception('Invalid context parameter.');
  3471. } if ($context->contextlevel == CONTEXT_COURSE) {
  3472. return $context;
  3473. } else if ($context->contextlevel == CONTEXT_MODULE) {
  3474. return get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST));
  3475. } else if ($context->contextlevel == CONTEXT_BLOCK) {
  3476. $parentcontext = get_context_instance_by_id(get_parent_contextid($context, MUST_EXIST));
  3477. if ($parentcontext->contextlevel == CONTEXT_COURSE) {
  3478. return $parentcontext;
  3479. } else if ($parentcontext->contextlevel == CONTEXT_MODULE) {
  3480. return get_context_instance_by_id(get_parent_contextid($parentcontext, MUST_EXIST));
  3481. } else {
  3482. throw new coding_exception('Invalid level of block context parameter.');
  3483. }
  3484. }
  3485. throw new coding_exception('Invalid context level of parameter.');
  3486. }
  3487. /**
  3488. * Check if context is the front page context or a context inside it
  3489. *
  3490. * Returns true if this context is the front page context, or a context inside it,
  3491. * otherwise false.
  3492. *
  3493. * @param object $context a context object.
  3494. * @return bool
  3495. */
  3496. function is_inside_frontpage($context) {
  3497. $frontpagecontext = get_context_instance(CONTEXT_COURSE, SITEID);
  3498. return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
  3499. }
  3500. /**
  3501. * Runs get_records select on context table and returns the result
  3502. * Does get_records_select on the context table, and returns the results ordered
  3503. * by contextlevel, and then the natural sort order within each level.
  3504. * for the purpose of $select, you need to know that the context table has been
  3505. * aliased to ctx, so for example, you can call get_sorted_contexts('ctx.depth = 3');
  3506. *
  3507. * @param string $select the contents of the WHERE clause. Remember to do ctx.fieldname.
  3508. * @param array $params any parameters required by $select.
  3509. * @return array the requested context records.
  3510. */
  3511. function get_sorted_contexts($select, $params = array()) {
  3512. global $DB;
  3513. if ($select) {
  3514. $select = 'WHERE ' . $select;
  3515. }
  3516. return $DB->get_records_sql("
  3517. SELECT ctx.*
  3518. FROM {context} ctx
  3519. LEFT JOIN {user} u ON ctx.contextlevel = " . CONTEXT_USER . " AND u.id = ctx.instanceid
  3520. LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid
  3521. LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid
  3522. LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid
  3523. LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid
  3524. $select
  3525. ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), u.lastname, u.firstname, cm.id
  3526. ", $params);
  3527. }
  3528. /**
  3529. * Recursive function which, given a context, find all its children context ids.
  3530. *
  3531. * When called for a course context, it will return the modules and blocks
  3532. * displayed in the course page.
  3533. *
  3534. * For course category contexts it will return categories and courses. It will
  3535. * NOT recurse into courses, nor return blocks on the category pages. If you
  3536. * want to do that, call it on the returned courses.
  3537. *
  3538. * If called on a course context it _will_ populate the cache with the appropriate
  3539. * contexts ;-)
  3540. *
  3541. * @param object $context.
  3542. * @return array Array of child records
  3543. */
  3544. function get_child_contexts($context) {
  3545. global $DB, $ACCESSLIB_PRIVATE;
  3546. // We *MUST* populate the context_cache as the callers
  3547. // will probably ask for the full record anyway soon after
  3548. // soon after calling us ;-)
  3549. $array = array();
  3550. $cache = $ACCESSLIB_PRIVATE->contexcache;
  3551. switch ($context->contextlevel) {
  3552. case CONTEXT_BLOCK:
  3553. // No children.
  3554. break;
  3555. case CONTEXT_MODULE:
  3556. // Find
  3557. // - blocks under this context path.
  3558. $sql = " SELECT ctx.*
  3559. FROM {context} ctx
  3560. WHERE ctx.path LIKE ?
  3561. AND ctx.contextlevel = ".CONTEXT_BLOCK;
  3562. $params = array("{$context->path}/%", $context->instanceid);
  3563. $records = $DB->get_recordset_sql($sql, $params);
  3564. foreach ($records as $rec) {
  3565. $cache->add($rec);
  3566. $array[$rec->id] = $rec;
  3567. }
  3568. break;
  3569. case CONTEXT_COURSE:
  3570. // Find
  3571. // - modules and blocks under this context path.
  3572. $sql = " SELECT ctx.*
  3573. FROM {context} ctx
  3574. WHERE ctx.path LIKE ?
  3575. AND ctx.contextlevel IN (".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
  3576. $params = array("{$context->path}/%", $context->instanceid);
  3577. $records = $DB->get_recordset_sql($sql, $params);
  3578. foreach ($records as $rec) {
  3579. $cache->add($rec);
  3580. $array[$rec->id] = $rec;
  3581. }
  3582. break;
  3583. case CONTEXT_COURSECAT:
  3584. // Find
  3585. // - categories
  3586. // - courses
  3587. $sql = " SELECT ctx.*
  3588. FROM {context} ctx
  3589. WHERE ctx.path LIKE ?
  3590. AND ctx.contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.")";
  3591. $params = array("{$context->path}/%");
  3592. $records = $DB->get_recordset_sql($sql, $params);
  3593. foreach ($records as $rec) {
  3594. $cache->add($rec);
  3595. $array[$rec->id] = $rec;
  3596. }
  3597. break;
  3598. case CONTEXT_USER:
  3599. // Find
  3600. // - blocks under this context path.
  3601. $sql = " SELECT ctx.*
  3602. FROM {context} ctx
  3603. WHERE ctx.path LIKE ?
  3604. AND ctx.contextlevel = ".CONTEXT_BLOCK;
  3605. $params = array("{$context->path}/%", $context->instanceid);
  3606. $records = $DB->get_recordset_sql($sql, $params);
  3607. foreach ($records as $rec) {
  3608. $cache->add($rec);
  3609. $array[$rec->id] = $rec;
  3610. }
  3611. break;
  3612. case CONTEXT_SYSTEM:
  3613. // Just get all the contexts except for CONTEXT_SYSTEM level
  3614. // and hope we don't OOM in the process - don't cache
  3615. $sql = "SELECT c.*
  3616. FROM {context} c
  3617. WHERE contextlevel != ".CONTEXT_SYSTEM;
  3618. $records = $DB->get_records_sql($sql);
  3619. foreach ($records as $rec) {
  3620. $array[$rec->id] = $rec;
  3621. }
  3622. break;
  3623. default:
  3624. print_error('unknowcontext', '', '', $context->contextlevel);
  3625. return false;
  3626. }
  3627. return $array;
  3628. }
  3629. /**
  3630. * Gets a string for sql calls, searching for stuff in this context or above
  3631. *
  3632. * @param object $context
  3633. * @return string
  3634. */
  3635. function get_related_contexts_string($context) {
  3636. if ($parents = get_parent_contexts($context)) {
  3637. return (' IN ('.$context->id.','.implode(',', $parents).')');
  3638. } else {
  3639. return (' ='.$context->id);
  3640. }
  3641. }
  3642. /**
  3643. * Returns capability information (cached)
  3644. *
  3645. * @param string $capabilityname
  3646. * @return object or null if capability not found
  3647. */
  3648. function get_capability_info($capabilityname) {
  3649. global $ACCESSLIB_PRIVATE, $DB; // one request per page only
  3650. // TODO: cache this in shared memory if available, use new $CFG->roledefrev for version check
  3651. if (empty($ACCESSLIB_PRIVATE->capabilities)) {
  3652. $ACCESSLIB_PRIVATE->capabilities = array();
  3653. $caps = $DB->get_records('capabilities', array(), 'id, name, captype, riskbitmask');
  3654. foreach ($caps as $cap) {
  3655. $capname = $cap->name;
  3656. unset($cap->id);
  3657. unset($cap->name);
  3658. $ACCESSLIB_PRIVATE->capabilities[$capname] = $cap;
  3659. }
  3660. }
  3661. return isset($ACCESSLIB_PRIVATE->capabilities[$capabilityname]) ? $ACCESSLIB_PRIVATE->capabilities[$capabilityname] : null;
  3662. }
  3663. /**
  3664. * Returns the human-readable, translated version of the capability.
  3665. * Basically a big switch statement.
  3666. *
  3667. * @param string $capabilityname e.g. mod/choice:readresponses
  3668. * @return string
  3669. */
  3670. function get_capability_string($capabilityname) {
  3671. // Typical capability name is 'plugintype/pluginname:capabilityname'
  3672. list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
  3673. if ($type === 'moodle') {
  3674. $component = 'core_role';
  3675. } else if ($type === 'quizreport') {
  3676. //ugly hack!!
  3677. $component = 'quiz_'.$name;
  3678. } else {
  3679. $component = $type.'_'.$name;
  3680. }
  3681. $stringname = $name.':'.$capname;
  3682. if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
  3683. return get_string($stringname, $component);
  3684. }
  3685. $dir = get_component_directory($component);
  3686. if (!file_exists($dir)) {
  3687. // plugin broken or does not exist, do not bother with printing of debug message
  3688. return $capabilityname.' ???';
  3689. }
  3690. // something is wrong in plugin, better print debug
  3691. return get_string($stringname, $component);
  3692. }
  3693. /**
  3694. * This gets the mod/block/course/core etc strings.
  3695. *
  3696. * @param string $component
  3697. * @param int $contextlevel
  3698. * @return string|bool String is success, false if failed
  3699. */
  3700. function get_component_string($component, $contextlevel) {
  3701. if ($component === 'moodle' or $component === 'core') {
  3702. switch ($contextlevel) {
  3703. case CONTEXT_SYSTEM: return get_string('coresystem');
  3704. case CONTEXT_USER: return get_string('users');
  3705. case CONTEXT_COURSECAT: return get_string('categories');
  3706. case CONTEXT_COURSE: return get_string('course');
  3707. case CONTEXT_MODULE: return get_string('activities');
  3708. case CONTEXT_BLOCK: return get_string('block');
  3709. default: print_error('unknowncontext');
  3710. }
  3711. }
  3712. list($type, $name) = normalize_component($component);
  3713. $dir = get_plugin_directory($type, $name);
  3714. if (!file_exists($dir)) {
  3715. // plugin not installed, bad luck, there is no way to find the name
  3716. return $component.' ???';
  3717. }
  3718. switch ($type) {
  3719. // TODO this is really hacky
  3720. case 'quiz': return get_string($name.':componentname', $component);// insane hack!!!
  3721. case 'repository': return get_string('repository', 'repository').': '.get_string('pluginname', $component);
  3722. case 'gradeimport': return get_string('gradeimport', 'grades').': '.get_string('pluginname', $component);
  3723. case 'gradeexport': return get_string('gradeexport', 'grades').': '.get_string('pluginname', $component);
  3724. case 'gradereport': return get_string('gradereport', 'grades').': '.get_string('pluginname', $component);
  3725. case 'webservice': return get_string('webservice', 'webservice').': '.get_string('pluginname', $component);
  3726. case 'block': return get_string('block').': '.get_string('pluginname', basename($component));
  3727. case 'mod':
  3728. if (get_string_manager()->string_exists('pluginname', $component)) {
  3729. return get_string('activity').': '.get_string('pluginname', $component);
  3730. } else {
  3731. return get_string('activity').': '.get_string('modulename', $component);
  3732. }
  3733. default: return get_string('pluginname', $component);
  3734. }
  3735. }
  3736. /**
  3737. * Gets the list of roles assigned to this context and up (parents)
  3738. * from the list of roles that are visible on user profile page
  3739. * and participants page.
  3740. *
  3741. * @param object $context
  3742. * @return array
  3743. */
  3744. function get_profile_roles($context) {
  3745. global $CFG, $DB;
  3746. if (empty($CFG->profileroles)) {
  3747. return array();
  3748. }
  3749. $allowed = explode(',', $CFG->profileroles);
  3750. list($rallowed, $params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED);
  3751. $contextlist = get_related_contexts_string($context);
  3752. $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder
  3753. FROM {role_assignments} ra, {role} r
  3754. WHERE r.id = ra.roleid
  3755. AND ra.contextid $contextlist
  3756. AND r.id $rallowed
  3757. ORDER BY r.sortorder ASC";
  3758. return $DB->get_records_sql($sql, $params);
  3759. }
  3760. /**
  3761. * Gets the list of roles assigned to this context and up (parents)
  3762. *
  3763. * @param object $context
  3764. * @return array
  3765. */
  3766. function get_roles_used_in_context($context) {
  3767. global $DB;
  3768. $contextlist = get_related_contexts_string($context);
  3769. $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder
  3770. FROM {role_assignments} ra, {role} r
  3771. WHERE r.id = ra.roleid
  3772. AND ra.contextid $contextlist
  3773. ORDER BY r.sortorder ASC";
  3774. return $DB->get_records_sql($sql);
  3775. }
  3776. /**
  3777. * This function is used to print roles column in user profile page.
  3778. * It is using the CFG->profileroles to limit the list to only interesting roles.
  3779. * (The permission tab has full details of user role assignments.)
  3780. *
  3781. * @param int $userid
  3782. * @param int $courseid
  3783. * @return string
  3784. */
  3785. function get_user_roles_in_course($userid, $courseid) {
  3786. global $CFG, $DB,$USER;
  3787. if (empty($CFG->profileroles)) {
  3788. return '';
  3789. }
  3790. if ($courseid == SITEID) {
  3791. $context = get_context_instance(CONTEXT_SYSTEM);
  3792. } else {
  3793. $context = get_context_instance(CONTEXT_COURSE, $courseid);
  3794. }
  3795. if (empty($CFG->profileroles)) {
  3796. return array();
  3797. }
  3798. $allowed = explode(',', $CFG->profileroles);
  3799. list($rallowed, $params) = $DB->get_in_or_equal($allowed, SQL_PARAMS_NAMED);
  3800. $contextlist = get_related_contexts_string($context);
  3801. $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder
  3802. FROM {role_assignments} ra, {role} r
  3803. WHERE r.id = ra.roleid
  3804. AND ra.contextid $contextlist
  3805. AND r.id $rallowed
  3806. AND ra.userid = :userid
  3807. ORDER BY r.sortorder ASC";
  3808. $params['userid'] = $userid;
  3809. $rolestring = '';
  3810. if ($roles = $DB->get_records_sql($sql, $params)) {
  3811. foreach ($roles as $userrole) {
  3812. $rolenames[$userrole->id] = $userrole->name;
  3813. }
  3814. $rolenames = role_fix_names($rolenames, $context); // Substitute aliases
  3815. foreach ($rolenames as $roleid => $rolename) {
  3816. $rolenames[$roleid] = '<a href="'.$CFG->wwwroot.'/user/index.php?contextid='.$context->id.'&amp;roleid='.$roleid.'">'.$rolename.'</a>';
  3817. }
  3818. $rolestring = implode(',', $rolenames);
  3819. }
  3820. return $rolestring;
  3821. }
  3822. /**
  3823. * Checks if a user can assign users to a particular role in this context
  3824. *
  3825. * @param object $context
  3826. * @param int $targetroleid - the id of the role you want to assign users to
  3827. * @return boolean
  3828. */
  3829. function user_can_assign($context, $targetroleid) {
  3830. global $DB;
  3831. // first check if user has override capability
  3832. // if not return false;
  3833. if (!has_capability('moodle/role:assign', $context)) {
  3834. return false;
  3835. }
  3836. // pull out all active roles of this user from this context(or above)
  3837. if ($userroles = get_user_roles($context)) {
  3838. foreach ($userroles as $userrole) {
  3839. // if any in the role_allow_override table, then it's ok
  3840. if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
  3841. return true;
  3842. }
  3843. }
  3844. }
  3845. return false;
  3846. }
  3847. /**
  3848. * Returns all site roles in correct sort order.
  3849. *
  3850. * @return array
  3851. */
  3852. function get_all_roles() {
  3853. global $DB;
  3854. return $DB->get_records('role', null, 'sortorder ASC');
  3855. }
  3856. /**
  3857. * Returns roles of a specified archetype
  3858. * @param string $archetype
  3859. * @return array of full role records
  3860. */
  3861. function get_archetype_roles($archetype) {
  3862. global $DB;
  3863. return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
  3864. }
  3865. /**
  3866. * Gets all the user roles assigned in this context, or higher contexts
  3867. * this is mainly used when checking if a user can assign a role, or overriding a role
  3868. * i.e. we need to know what this user holds, in order to verify against allow_assign and
  3869. * allow_override tables
  3870. *
  3871. * @param object $context
  3872. * @param int $userid
  3873. * @param bool $checkparentcontexts defaults to true
  3874. * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
  3875. * @return array
  3876. */
  3877. function get_user_roles($context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
  3878. global $USER, $DB;
  3879. if (empty($userid)) {
  3880. if (empty($USER->id)) {
  3881. return array();
  3882. }
  3883. $userid = $USER->id;
  3884. }
  3885. if ($checkparentcontexts) {
  3886. $contextids = get_parent_contexts($context);
  3887. } else {
  3888. $contextids = array();
  3889. }
  3890. $contextids[] = $context->id;
  3891. list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
  3892. array_unshift($params, $userid);
  3893. $sql = "SELECT ra.*, r.name, r.shortname
  3894. FROM {role_assignments} ra, {role} r, {context} c
  3895. WHERE ra.userid = ?
  3896. AND ra.roleid = r.id
  3897. AND ra.contextid = c.id
  3898. AND ra.contextid $contextids
  3899. ORDER BY $order";
  3900. return $DB->get_records_sql($sql ,$params);
  3901. }
  3902. /**
  3903. * Creates a record in the role_allow_override table
  3904. *
  3905. * @param int $sroleid source roleid
  3906. * @param int $troleid target roleid
  3907. * @return void
  3908. */
  3909. function allow_override($sroleid, $troleid) {
  3910. global $DB;
  3911. $record = new stdClass();
  3912. $record->roleid = $sroleid;
  3913. $record->allowoverride = $troleid;
  3914. $DB->insert_record('role_allow_override', $record);
  3915. }
  3916. /**
  3917. * Creates a record in the role_allow_assign table
  3918. *
  3919. * @param int $sroleid source roleid
  3920. * @param int $troleid target roleid
  3921. * @return void
  3922. */
  3923. function allow_assign($fromroleid, $targetroleid) {
  3924. global $DB;
  3925. $record = new stdClass();
  3926. $record->roleid = $fromroleid;
  3927. $record->allowassign = $targetroleid;
  3928. $DB->insert_record('role_allow_assign', $record);
  3929. }
  3930. /**
  3931. * Creates a record in the role_allow_switch table
  3932. *
  3933. * @param int $sroleid source roleid
  3934. * @param int $troleid target roleid
  3935. * @return void
  3936. */
  3937. function allow_switch($fromroleid, $targetroleid) {
  3938. global $DB;
  3939. $record = new stdClass();
  3940. $record->roleid = $fromroleid;
  3941. $record->allowswitch = $targetroleid;
  3942. $DB->insert_record('role_allow_switch', $record);
  3943. }
  3944. /**
  3945. * Gets a list of roles that this user can assign in this context
  3946. *
  3947. * @param object $context the context.
  3948. * @param int $rolenamedisplay the type of role name to display. One of the
  3949. * ROLENAME_X constants. Default ROLENAME_ALIAS.
  3950. * @param bool $withusercounts if true, count the number of users with each role.
  3951. * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
  3952. * @return array if $withusercounts is false, then an array $roleid => $rolename.
  3953. * if $withusercounts is true, returns a list of three arrays,
  3954. * $rolenames, $rolecounts, and $nameswithcounts.
  3955. */
  3956. function get_assignable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
  3957. global $USER, $DB;
  3958. // make sure there is a real user specified
  3959. if ($user === null) {
  3960. $userid = !empty($USER->id) ? $USER->id : 0;
  3961. } else {
  3962. $userid = !empty($user->id) ? $user->id : $user;
  3963. }
  3964. if (!has_capability('moodle/role:assign', $context, $userid)) {
  3965. if ($withusercounts) {
  3966. return array(array(), array(), array());
  3967. } else {
  3968. return array();
  3969. }
  3970. }
  3971. $parents = get_parent_contexts($context, true);
  3972. $contexts = implode(',' , $parents);
  3973. $params = array();
  3974. $extrafields = '';
  3975. if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT or $rolenamedisplay == ROLENAME_SHORT) {
  3976. $extrafields .= ', r.shortname';
  3977. }
  3978. if ($withusercounts) {
  3979. $extrafields = ', (SELECT count(u.id)
  3980. FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
  3981. WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
  3982. ) AS usercount';
  3983. $params['conid'] = $context->id;
  3984. }
  3985. if (is_siteadmin($userid)) {
  3986. // show all roles allowed in this context to admins
  3987. $assignrestriction = "";
  3988. } else {
  3989. $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
  3990. FROM {role_allow_assign} raa
  3991. JOIN {role_assignments} ra ON ra.roleid = raa.roleid
  3992. WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
  3993. ) ar ON ar.id = r.id";
  3994. $params['userid'] = $userid;
  3995. }
  3996. $params['contextlevel'] = $context->contextlevel;
  3997. $sql = "SELECT r.id, r.name $extrafields
  3998. FROM {role} r
  3999. $assignrestriction
  4000. JOIN {role_context_levels} rcl ON r.id = rcl.roleid
  4001. WHERE rcl.contextlevel = :contextlevel
  4002. ORDER BY r.sortorder ASC";
  4003. $roles = $DB->get_records_sql($sql, $params);
  4004. $rolenames = array();
  4005. foreach ($roles as $role) {
  4006. if ($rolenamedisplay == ROLENAME_SHORT) {
  4007. $rolenames[$role->id] = $role->shortname;
  4008. continue;
  4009. }
  4010. $rolenames[$role->id] = $role->name;
  4011. if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
  4012. $rolenames[$role->id] .= ' (' . $role->shortname . ')';
  4013. }
  4014. }
  4015. if ($rolenamedisplay != ROLENAME_ORIGINALANDSHORT and $rolenamedisplay != ROLENAME_SHORT) {
  4016. $rolenames = role_fix_names($rolenames, $context, $rolenamedisplay);
  4017. }
  4018. if (!$withusercounts) {
  4019. return $rolenames;
  4020. }
  4021. $rolecounts = array();
  4022. $nameswithcounts = array();
  4023. foreach ($roles as $role) {
  4024. $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
  4025. $rolecounts[$role->id] = $roles[$role->id]->usercount;
  4026. }
  4027. return array($rolenames, $rolecounts, $nameswithcounts);
  4028. }
  4029. /**
  4030. * Gets a list of roles that this user can switch to in a context
  4031. *
  4032. * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
  4033. * This function just process the contents of the role_allow_switch table. You also need to
  4034. * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
  4035. *
  4036. * @param object $context a context.
  4037. * @return array an array $roleid => $rolename.
  4038. */
  4039. function get_switchable_roles($context) {
  4040. global $USER, $DB;
  4041. $systemcontext = get_context_instance(CONTEXT_SYSTEM);
  4042. $params = array();
  4043. $extrajoins = '';
  4044. $extrawhere = '';
  4045. if (!is_siteadmin()) {
  4046. // Admins are allowed to switch to any role with.
  4047. // Others are subject to the additional constraint that the switch-to role must be allowed by
  4048. // 'role_allow_switch' for some role they have assigned in this context or any parent.
  4049. $parents = get_parent_contexts($context);
  4050. $parents[] = $context->id;
  4051. $contexts = implode(',' , $parents);
  4052. $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
  4053. JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
  4054. $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
  4055. $params['userid'] = $USER->id;
  4056. }
  4057. $query = "
  4058. SELECT r.id, r.name
  4059. FROM (SELECT DISTINCT rc.roleid
  4060. FROM {role_capabilities} rc
  4061. $extrajoins
  4062. $extrawhere) idlist
  4063. JOIN {role} r ON r.id = idlist.roleid
  4064. ORDER BY r.sortorder";
  4065. $rolenames = $DB->get_records_sql_menu($query, $params);
  4066. return role_fix_names($rolenames, $context, ROLENAME_ALIAS);
  4067. }
  4068. /**
  4069. * Gets a list of roles that this user can override in this context.
  4070. *
  4071. * @param object $context the context.
  4072. * @param int $rolenamedisplay the type of role name to display. One of the
  4073. * ROLENAME_X constants. Default ROLENAME_ALIAS.
  4074. * @param bool $withcounts if true, count the number of overrides that are set for each role.
  4075. * @return array if $withcounts is false, then an array $roleid => $rolename.
  4076. * if $withusercounts is true, returns a list of three arrays,
  4077. * $rolenames, $rolecounts, and $nameswithcounts.
  4078. */
  4079. function get_overridable_roles($context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
  4080. global $USER, $DB;
  4081. if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
  4082. if ($withcounts) {
  4083. return array(array(), array(), array());
  4084. } else {
  4085. return array();
  4086. }
  4087. }
  4088. $parents = get_parent_contexts($context);
  4089. $parents[] = $context->id;
  4090. $contexts = implode(',' , $parents);
  4091. $params = array();
  4092. $extrafields = '';
  4093. if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
  4094. $extrafields .= ', ro.shortname';
  4095. }
  4096. $params['userid'] = $USER->id;
  4097. if ($withcounts) {
  4098. $extrafields = ', (SELECT count(rc.id) FROM {role_capabilities} rc
  4099. WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
  4100. $params['conid'] = $context->id;
  4101. }
  4102. if (is_siteadmin()) {
  4103. // show all roles to admins
  4104. $roles = $DB->get_records_sql("
  4105. SELECT ro.id, ro.name$extrafields
  4106. FROM {role} ro
  4107. ORDER BY ro.sortorder ASC", $params);
  4108. } else {
  4109. $roles = $DB->get_records_sql("
  4110. SELECT ro.id, ro.name$extrafields
  4111. FROM {role} ro
  4112. JOIN (SELECT DISTINCT r.id
  4113. FROM {role} r
  4114. JOIN {role_allow_override} rao ON r.id = rao.allowoverride
  4115. JOIN {role_assignments} ra ON rao.roleid = ra.roleid
  4116. WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
  4117. ) inline_view ON ro.id = inline_view.id
  4118. ORDER BY ro.sortorder ASC", $params);
  4119. }
  4120. $rolenames = array();
  4121. foreach ($roles as $role) {
  4122. $rolenames[$role->id] = $role->name;
  4123. if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
  4124. $rolenames[$role->id] .= ' (' . $role->shortname . ')';
  4125. }
  4126. }
  4127. if ($rolenamedisplay != ROLENAME_ORIGINALANDSHORT) {
  4128. $rolenames = role_fix_names($rolenames, $context, $rolenamedisplay);
  4129. }
  4130. if (!$withcounts) {
  4131. return $rolenames;
  4132. }
  4133. $rolecounts = array();
  4134. $nameswithcounts = array();
  4135. foreach ($roles as $role) {
  4136. $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
  4137. $rolecounts[$role->id] = $roles[$role->id]->overridecount;
  4138. }
  4139. return array($rolenames, $rolecounts, $nameswithcounts);
  4140. }
  4141. /**
  4142. * Create a role menu suitable for default role selection in enrol plugins.
  4143. * @param object $context
  4144. * @param int $addroleid current or default role - always added to list
  4145. * @return array roleid=>localised role name
  4146. */
  4147. function get_default_enrol_roles($context, $addroleid = null) {
  4148. global $DB;
  4149. $params = array('contextlevel'=>CONTEXT_COURSE);
  4150. if ($addroleid) {
  4151. $addrole = "OR r.id = :addroleid";
  4152. $params['addroleid'] = $addroleid;
  4153. } else {
  4154. $addrole = "";
  4155. }
  4156. $sql = "SELECT r.id, r.name
  4157. FROM {role} r
  4158. LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
  4159. WHERE rcl.id IS NOT NULL $addrole
  4160. ORDER BY sortorder DESC";
  4161. $roles = $DB->get_records_sql_menu($sql, $params);
  4162. $roles = role_fix_names($roles, $context, ROLENAME_BOTH);
  4163. return $roles;
  4164. }
  4165. /**
  4166. * @param integer $roleid the id of a role.
  4167. * @return array list of the context levels at which this role may be assigned.
  4168. */
  4169. function get_role_contextlevels($roleid) {
  4170. global $DB;
  4171. return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
  4172. 'contextlevel', 'id,contextlevel');
  4173. }
  4174. /**
  4175. * @param integer $contextlevel a contextlevel.
  4176. * @return array list of role ids that are assignable at this context level.
  4177. */
  4178. function get_roles_for_contextlevels($contextlevel) {
  4179. global $DB;
  4180. return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
  4181. '', 'id,roleid');
  4182. }
  4183. /**
  4184. * @param string $rolearchetype one of the role archetypes - that is, one of the keys
  4185. * from the array returned by get_role_archetypes();
  4186. * @return array list of the context levels at which this type of role may be assigned by default.
  4187. */
  4188. function get_default_contextlevels($rolearchetype) {
  4189. static $defaults = array(
  4190. 'manager' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
  4191. 'coursecreator' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
  4192. 'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
  4193. 'teacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
  4194. 'student' => array(CONTEXT_COURSE, CONTEXT_MODULE),
  4195. 'guest' => array(),
  4196. 'user' => array(),
  4197. 'frontpage' => array());
  4198. if (isset($defaults[$rolearchetype])) {
  4199. return $defaults[$rolearchetype];
  4200. } else {
  4201. return array();
  4202. }
  4203. }
  4204. /**
  4205. * Set the context levels at which a particular role can be assigned.
  4206. * Throws exceptions in case of error.
  4207. *
  4208. * @param integer $roleid the id of a role.
  4209. * @param array $contextlevels the context levels at which this role should be assignable,
  4210. * duplicate levels are removed.
  4211. * @return void
  4212. */
  4213. function set_role_contextlevels($roleid, array $contextlevels) {
  4214. global $DB;
  4215. $DB->delete_records('role_context_levels', array('roleid' => $roleid));
  4216. $rcl = new stdClass();
  4217. $rcl->roleid = $roleid;
  4218. $contextlevels = array_unique($contextlevels);
  4219. foreach ($contextlevels as $level) {
  4220. $rcl->contextlevel = $level;
  4221. $DB->insert_record('role_context_levels', $rcl, false, true);
  4222. }
  4223. }
  4224. /**
  4225. * Who has this capability in this context?
  4226. *
  4227. * This can be a very expensive call - use sparingly and keep
  4228. * the results if you are going to need them again soon.
  4229. *
  4230. * Note if $fields is empty this function attempts to get u.*
  4231. * which can get rather large - and has a serious perf impact
  4232. * on some DBs.
  4233. *
  4234. * @param object $context
  4235. * @param string|array $capability - capability name(s)
  4236. * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
  4237. * @param string $sort - the sort order. Default is lastaccess time.
  4238. * @param mixed $limitfrom - number of records to skip (offset)
  4239. * @param mixed $limitnum - number of records to fetch
  4240. * @param string|array $groups - single group or array of groups - only return
  4241. * users who are in one of these group(s).
  4242. * @param string|array $exceptions - list of users to exclude, comma separated or array
  4243. * @param bool $doanything_ignored not used any more, admin accounts are never returned
  4244. * @param bool $view_ignored - use get_enrolled_sql() instead
  4245. * @param bool $useviewallgroups if $groups is set the return users who
  4246. * have capability both $capability and moodle/site:accessallgroups
  4247. * in this context, as well as users who have $capability and who are
  4248. * in $groups.
  4249. * @return mixed
  4250. */
  4251. function get_users_by_capability($context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
  4252. $groups = '', $exceptions = '', $doanything_ignored = null, $view_ignored = null, $useviewallgroups = false) {
  4253. global $CFG, $DB;
  4254. if (empty($context->id)) {
  4255. throw new coding_exception('Invalid context specified');
  4256. }
  4257. $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : null;
  4258. $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : null;
  4259. $ctxids = trim($context->path, '/');
  4260. $ctxids = str_replace('/', ',', $ctxids);
  4261. // Context is the frontpage
  4262. $iscoursepage = false; // coursepage other than fp
  4263. $isfrontpage = false;
  4264. if ($context->contextlevel == CONTEXT_COURSE) {
  4265. if ($context->instanceid == SITEID) {
  4266. $isfrontpage = true;
  4267. } else {
  4268. $iscoursepage = true;
  4269. }
  4270. }
  4271. $isfrontpage = ($isfrontpage || is_inside_frontpage($context));
  4272. $caps = (array)$capability;
  4273. // construct list of context paths bottom-->top
  4274. list($contextids, $paths) = get_context_info_list($context);
  4275. // we need to find out all roles that have these capabilities either in definition or in overrides
  4276. $defs = array();
  4277. list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con000');
  4278. list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, 'cap000');
  4279. $params = array_merge($params, $params2);
  4280. $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
  4281. FROM {role_capabilities} rc
  4282. JOIN {context} ctx on rc.contextid = ctx.id
  4283. WHERE rc.contextid $incontexts AND rc.capability $incaps";
  4284. $rcs = $DB->get_records_sql($sql, $params);
  4285. foreach ($rcs as $rc) {
  4286. $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
  4287. }
  4288. // go through the permissions bottom-->top direction to evaluate the current permission,
  4289. // first one wins (prohibit is an exception that always wins)
  4290. $access = array();
  4291. foreach ($caps as $cap) {
  4292. foreach ($paths as $path) {
  4293. if (empty($defs[$cap][$path])) {
  4294. continue;
  4295. }
  4296. foreach($defs[$cap][$path] as $roleid => $perm) {
  4297. if ($perm == CAP_PROHIBIT) {
  4298. $access[$cap][$roleid] = CAP_PROHIBIT;
  4299. continue;
  4300. }
  4301. if (!isset($access[$cap][$roleid])) {
  4302. $access[$cap][$roleid] = (int)$perm;
  4303. }
  4304. }
  4305. }
  4306. }
  4307. // make lists of roles that are needed and prohibited in this context
  4308. $needed = array(); // one of these is enough
  4309. $prohibited = array(); // must not have any of these
  4310. foreach ($caps as $cap) {
  4311. if (empty($access[$cap])) {
  4312. continue;
  4313. }
  4314. foreach ($access[$cap] as $roleid => $perm) {
  4315. if ($perm == CAP_PROHIBIT) {
  4316. unset($needed[$cap][$roleid]);
  4317. $prohibited[$cap][$roleid] = true;
  4318. } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
  4319. $needed[$cap][$roleid] = true;
  4320. }
  4321. }
  4322. if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
  4323. // easy, nobody has the permission
  4324. unset($needed[$cap]);
  4325. unset($prohibited[$cap]);
  4326. } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
  4327. // everybody is disqualified on the frontapge
  4328. unset($needed[$cap]);
  4329. unset($prohibited[$cap]);
  4330. }
  4331. if (empty($prohibited[$cap])) {
  4332. unset($prohibited[$cap]);
  4333. }
  4334. }
  4335. if (empty($needed)) {
  4336. // there can not be anybody if no roles match this request
  4337. return array();
  4338. }
  4339. if (empty($prohibited)) {
  4340. // we can compact the needed roles
  4341. $n = array();
  4342. foreach ($needed as $cap) {
  4343. foreach ($cap as $roleid=>$unused) {
  4344. $n[$roleid] = true;
  4345. }
  4346. }
  4347. $needed = array('any'=>$n);
  4348. unset($n);
  4349. }
  4350. /// ***** Set up default fields ******
  4351. if (empty($fields)) {
  4352. if ($iscoursepage) {
  4353. $fields = 'u.*, ul.timeaccess AS lastaccess';
  4354. } else {
  4355. $fields = 'u.*';
  4356. }
  4357. } else {
  4358. if (debugging('', DEBUG_DEVELOPER) && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
  4359. debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
  4360. }
  4361. }
  4362. /// Set up default sort
  4363. if (empty($sort)) { // default to course lastaccess or just lastaccess
  4364. if ($iscoursepage) {
  4365. $sort = 'ul.timeaccess';
  4366. } else {
  4367. $sort = 'u.lastaccess';
  4368. }
  4369. }
  4370. $sortby = "ORDER BY $sort";
  4371. // Prepare query clauses
  4372. $wherecond = array();
  4373. $params = array();
  4374. $joins = array();
  4375. // User lastaccess JOIN
  4376. if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
  4377. // user_lastaccess is not required MDL-13810
  4378. } else {
  4379. if ($iscoursepage) {
  4380. $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
  4381. } else {
  4382. throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
  4383. }
  4384. }
  4385. /// We never return deleted users or guest account.
  4386. $wherecond[] = "u.deleted = 0 AND u.id <> :guestid";
  4387. $params['guestid'] = $CFG->siteguest;
  4388. /// Groups
  4389. if ($groups) {
  4390. $groups = (array)$groups;
  4391. list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp000');
  4392. $grouptest = "u.id IN (SELECT userid FROM {groups_members} gm WHERE gm.groupid $grouptest)";
  4393. $params = array_merge($params, $grpparams);
  4394. if ($useviewallgroups) {
  4395. $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
  4396. $wherecond[] = "($grouptest OR u.id IN (" . implode(',', array_keys($viewallgroupsusers)) . '))';
  4397. } else {
  4398. $wherecond[] = "($grouptest)";
  4399. }
  4400. }
  4401. /// User exceptions
  4402. if (!empty($exceptions)) {
  4403. $exceptions = (array)$exceptions;
  4404. list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc000', false);
  4405. $params = array_merge($params, $exparams);
  4406. $wherecond[] = "u.id $exsql";
  4407. }
  4408. // now add the needed and prohibited roles conditions as joins
  4409. if (!empty($needed['any'])) {
  4410. // simple case - there are no prohibits involved
  4411. if (!empty($needed['any'][$defaultuserroleid]) or ($isfrontpage and !empty($needed['any'][$defaultfrontpageroleid]))) {
  4412. // everybody
  4413. } else {
  4414. $joins[] = "JOIN (SELECT DISTINCT userid
  4415. FROM {role_assignments}
  4416. WHERE contextid IN ($ctxids)
  4417. AND roleid IN (".implode(',', array_keys($needed['any'])) .")
  4418. ) ra ON ra.userid = u.id";
  4419. }
  4420. } else {
  4421. $unions = array();
  4422. $everybody = false;
  4423. foreach ($needed as $cap=>$unused) {
  4424. if (empty($prohibited[$cap])) {
  4425. if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
  4426. $everybody = true;
  4427. break;
  4428. } else {
  4429. $unions[] = "SELECT userid
  4430. FROM {role_assignments}
  4431. WHERE contextid IN ($ctxids)
  4432. AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
  4433. }
  4434. } else {
  4435. if (!empty($prohibited[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
  4436. // nobody can have this cap because it is prevented in default roles
  4437. continue;
  4438. } else if (!empty($needed[$cap][$defaultuserroleid]) or ($isfrontpage and !empty($needed[$cap][$defaultfrontpageroleid]))) {
  4439. // everybody except the prohibitted - hiding does not matter
  4440. $unions[] = "SELECT id AS userid
  4441. FROM {user}
  4442. WHERE id NOT IN (SELECT userid
  4443. FROM {role_assignments}
  4444. WHERE contextid IN ($ctxids)
  4445. AND roleid IN (".implode(',', array_keys($prohibited[$cap])) ."))";
  4446. } else {
  4447. $unions[] = "SELECT userid
  4448. FROM {role_assignments}
  4449. WHERE contextid IN ($ctxids)
  4450. AND roleid IN (".implode(',', array_keys($needed[$cap])) .")
  4451. AND roleid NOT IN (".implode(',', array_keys($prohibited[$cap])) .")";
  4452. }
  4453. }
  4454. }
  4455. if (!$everybody) {
  4456. if ($unions) {
  4457. $joins[] = "JOIN (SELECT DISTINCT userid FROM ( ".implode(' UNION ', $unions)." ) us) ra ON ra.userid = u.id";
  4458. } else {
  4459. // only prohibits found - nobody can be matched
  4460. $wherecond[] = "1 = 2";
  4461. }
  4462. }
  4463. }
  4464. // Collect WHERE conditions and needed joins
  4465. $where = implode(' AND ', $wherecond);
  4466. if ($where !== '') {
  4467. $where = 'WHERE ' . $where;
  4468. }
  4469. $joins = implode("\n", $joins);
  4470. /// Ok, let's get the users!
  4471. $sql = "SELECT $fields
  4472. FROM {user} u
  4473. $joins
  4474. $where
  4475. ORDER BY $sort";
  4476. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  4477. }
  4478. /**
  4479. * Re-sort a users array based on a sorting policy
  4480. *
  4481. * Will re-sort a $users results array (from get_users_by_capability(), usually)
  4482. * based on a sorting policy. This is to support the odd practice of
  4483. * sorting teachers by 'authority', where authority was "lowest id of the role
  4484. * assignment".
  4485. *
  4486. * Will execute 1 database query. Only suitable for small numbers of users, as it
  4487. * uses an u.id IN() clause.
  4488. *
  4489. * Notes about the sorting criteria.
  4490. *
  4491. * As a default, we cannot rely on role.sortorder because then
  4492. * admins/coursecreators will always win. That is why the sane
  4493. * rule "is locality matters most", with sortorder as 2nd
  4494. * consideration.
  4495. *
  4496. * If you want role.sortorder, use the 'sortorder' policy, and
  4497. * name explicitly what roles you want to cover. It's probably
  4498. * a good idea to see what roles have the capabilities you want
  4499. * (array_diff() them against roiles that have 'can-do-anything'
  4500. * to weed out admin-ish roles. Or fetch a list of roles from
  4501. * variables like $CFG->coursecontact .
  4502. *
  4503. * @param array $users Users array, keyed on userid
  4504. * @param object $context
  4505. * @param array $roles ids of the roles to include, optional
  4506. * @param string $policy defaults to locality, more about
  4507. * @return array sorted copy of the array
  4508. */
  4509. function sort_by_roleassignment_authority($users, $context, $roles = array(), $sortpolicy = 'locality') {
  4510. global $DB;
  4511. $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
  4512. $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
  4513. if (empty($roles)) {
  4514. $roleswhere = '';
  4515. } else {
  4516. $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
  4517. }
  4518. $sql = "SELECT ra.userid
  4519. FROM {role_assignments} ra
  4520. JOIN {role} r
  4521. ON ra.roleid=r.id
  4522. JOIN {context} ctx
  4523. ON ra.contextid=ctx.id
  4524. WHERE $userswhere
  4525. $contextwhere
  4526. $roleswhere";
  4527. // Default 'locality' policy -- read PHPDoc notes
  4528. // about sort policies...
  4529. $orderby = 'ORDER BY '
  4530. .'ctx.depth DESC, ' /* locality wins */
  4531. .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
  4532. .'ra.id'; /* role assignment order tie-breaker */
  4533. if ($sortpolicy === 'sortorder') {
  4534. $orderby = 'ORDER BY '
  4535. .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
  4536. .'ra.id'; /* role assignment order tie-breaker */
  4537. }
  4538. $sortedids = $DB->get_fieldset_sql($sql . $orderby);
  4539. $sortedusers = array();
  4540. $seen = array();
  4541. foreach ($sortedids as $id) {
  4542. // Avoid duplicates
  4543. if (isset($seen[$id])) {
  4544. continue;
  4545. }
  4546. $seen[$id] = true;
  4547. // assign
  4548. $sortedusers[$id] = $users[$id];
  4549. }
  4550. return $sortedusers;
  4551. }
  4552. /**
  4553. * Gets all the users assigned this role in this context or higher
  4554. *
  4555. * @param int $roleid (can also be an array of ints!)
  4556. * @param stdClass $context
  4557. * @param bool $parent if true, get list of users assigned in higher context too
  4558. * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
  4559. * @param string $sort sort from user (u.) , role assignment (ra) or role (r.)
  4560. * @param bool $gethidden_ignored use enrolments instead
  4561. * @param string $group defaults to ''
  4562. * @param mixed $limitfrom defaults to ''
  4563. * @param mixed $limitnum defaults to ''
  4564. * @param string $extrawheretest defaults to ''
  4565. * @param string|array $whereparams defaults to ''
  4566. * @return array
  4567. */
  4568. function get_role_users($roleid, $context, $parent = false, $fields = '',
  4569. $sort = 'u.lastname, u.firstname', $gethidden_ignored = null, $group = '',
  4570. $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereparams = array()) {
  4571. global $DB;
  4572. if (empty($fields)) {
  4573. $fields = 'u.id, u.confirmed, u.username, u.firstname, u.lastname, '.
  4574. 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.city, '.
  4575. 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
  4576. 'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder';
  4577. }
  4578. $parentcontexts = '';
  4579. if ($parent) {
  4580. $parentcontexts = substr($context->path, 1); // kill leading slash
  4581. $parentcontexts = str_replace('/', ',', $parentcontexts);
  4582. if ($parentcontexts !== '') {
  4583. $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
  4584. }
  4585. }
  4586. if ($roleid) {
  4587. list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
  4588. $roleselect = "AND ra.roleid $rids";
  4589. } else {
  4590. $params = array();
  4591. $roleselect = '';
  4592. }
  4593. if ($group) {
  4594. $groupjoin = "JOIN {groups_members} gm ON gm.userid = u.id";
  4595. $groupselect = " AND gm.groupid = ? ";
  4596. $params[] = $group;
  4597. } else {
  4598. $groupjoin = '';
  4599. $groupselect = '';
  4600. }
  4601. array_unshift($params, $context->id);
  4602. if ($extrawheretest) {
  4603. $extrawheretest = ' AND ' . $extrawheretest;
  4604. $params = array_merge($params, $whereparams);
  4605. }
  4606. $sql = "SELECT DISTINCT $fields, ra.roleid
  4607. FROM {role_assignments} ra
  4608. JOIN {user} u ON u.id = ra.userid
  4609. JOIN {role} r ON ra.roleid = r.id
  4610. $groupjoin
  4611. WHERE (ra.contextid = ? $parentcontexts)
  4612. $roleselect
  4613. $groupselect
  4614. $extrawheretest
  4615. ORDER BY $sort"; // join now so that we can just use fullname() later
  4616. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  4617. }
  4618. /**
  4619. * Counts all the users assigned this role in this context or higher
  4620. *
  4621. * @param mixed $roleid either int or an array of ints
  4622. * @param object $context
  4623. * @param bool $parent if true, get list of users assigned in higher context too
  4624. * @return int Returns the result count
  4625. */
  4626. function count_role_users($roleid, $context, $parent = false) {
  4627. global $DB;
  4628. if ($parent) {
  4629. if ($contexts = get_parent_contexts($context)) {
  4630. $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
  4631. } else {
  4632. $parentcontexts = '';
  4633. }
  4634. } else {
  4635. $parentcontexts = '';
  4636. }
  4637. if ($roleid) {
  4638. list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
  4639. $roleselect = "AND r.roleid $rids";
  4640. } else {
  4641. $params = array();
  4642. $roleselect = '';
  4643. }
  4644. array_unshift($params, $context->id);
  4645. $sql = "SELECT count(u.id)
  4646. FROM {role_assignments} r
  4647. JOIN {user} u ON u.id = r.userid
  4648. WHERE (r.contextid = ? $parentcontexts)
  4649. $roleselect
  4650. AND u.deleted = 0";
  4651. return $DB->count_records_sql($sql, $params);
  4652. }
  4653. /**
  4654. * This function gets the list of courses that this user has a particular capability in.
  4655. * It is still not very efficient.
  4656. *
  4657. * @param string $capability Capability in question
  4658. * @param int $userid User ID or null for current user
  4659. * @param bool $doanything True if 'doanything' is permitted (default)
  4660. * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
  4661. * otherwise use a comma-separated list of the fields you require, not including id
  4662. * @param string $orderby If set, use a comma-separated list of fields from course
  4663. * table with sql modifiers (DESC) if needed
  4664. * @return array Array of courses, may have zero entries. Or false if query failed.
  4665. */
  4666. function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '') {
  4667. global $DB;
  4668. // Convert fields list and ordering
  4669. $fieldlist = '';
  4670. if ($fieldsexceptid) {
  4671. $fields = explode(',', $fieldsexceptid);
  4672. foreach($fields as $field) {
  4673. $fieldlist .= ',c.'.$field;
  4674. }
  4675. }
  4676. if ($orderby) {
  4677. $fields = explode(',', $orderby);
  4678. $orderby = '';
  4679. foreach($fields as $field) {
  4680. if ($orderby) {
  4681. $orderby .= ',';
  4682. }
  4683. $orderby .= 'c.'.$field;
  4684. }
  4685. $orderby = 'ORDER BY '.$orderby;
  4686. }
  4687. // Obtain a list of everything relevant about all courses including context.
  4688. // Note the result can be used directly as a context (we are going to), the course
  4689. // fields are just appended.
  4690. $courses = array();
  4691. $rs = $DB->get_recordset_sql("SELECT x.*, c.id AS courseid $fieldlist
  4692. FROM {course} c
  4693. INNER JOIN {context} x
  4694. ON (c.id=x.instanceid AND x.contextlevel=".CONTEXT_COURSE.")
  4695. $orderby");
  4696. // Check capability for each course in turn
  4697. foreach ($rs as $coursecontext) {
  4698. if (has_capability($capability, $coursecontext, $userid, $doanything)) {
  4699. // We've got the capability. Make the record look like a course record
  4700. // and store it
  4701. $coursecontext->id = $coursecontext->courseid;
  4702. unset($coursecontext->courseid);
  4703. unset($coursecontext->contextlevel);
  4704. unset($coursecontext->instanceid);
  4705. $courses[] = $coursecontext;
  4706. }
  4707. }
  4708. $rs->close();
  4709. return empty($courses) ? false : $courses;
  4710. }
  4711. /**
  4712. * This function finds the roles assigned directly to this context only
  4713. * i.e. no parents role
  4714. *
  4715. * @param object $context
  4716. * @return array
  4717. */
  4718. function get_roles_on_exact_context($context) {
  4719. global $DB;
  4720. return $DB->get_records_sql("SELECT r.*
  4721. FROM {role_assignments} ra, {role} r
  4722. WHERE ra.roleid = r.id AND ra.contextid = ?",
  4723. array($context->id));
  4724. }
  4725. /**
  4726. * Switches the current user to another role for the current session and only
  4727. * in the given context.
  4728. *
  4729. * The caller *must* check
  4730. * - that this op is allowed
  4731. * - that the requested role can be switched to in this context (use get_switchable_roles)
  4732. * - that the requested role is NOT $CFG->defaultuserroleid
  4733. *
  4734. * To "unswitch" pass 0 as the roleid.
  4735. *
  4736. * This function *will* modify $USER->access - beware
  4737. *
  4738. * @param integer $roleid the role to switch to.
  4739. * @param object $context the context in which to perform the switch.
  4740. * @return bool success or failure.
  4741. */
  4742. function role_switch($roleid, $context) {
  4743. global $USER;
  4744. //
  4745. // Plan of action
  4746. //
  4747. // - Add the ghost RA to $USER->access
  4748. // as $USER->access['rsw'][$path] = $roleid
  4749. //
  4750. // - Make sure $USER->access['rdef'] has the roledefs
  4751. // it needs to honour the switcherole
  4752. //
  4753. // Roledefs will get loaded "deep" here - down to the last child
  4754. // context. Note that
  4755. //
  4756. // - When visiting subcontexts, our selective accessdata loading
  4757. // will still work fine - though those ra/rdefs will be ignored
  4758. // appropriately while the switch is in place
  4759. //
  4760. // - If a switcherole happens at a category with tons of courses
  4761. // (that have many overrides for switched-to role), the session
  4762. // will get... quite large. Sometimes you just can't win.
  4763. //
  4764. // To un-switch just unset($USER->access['rsw'][$path])
  4765. //
  4766. // Note: it is not possible to switch to roles that do not have course:view
  4767. // Add the switch RA
  4768. if (!isset($USER->access['rsw'])) {
  4769. $USER->access['rsw'] = array();
  4770. }
  4771. if ($roleid == 0) {
  4772. unset($USER->access['rsw'][$context->path]);
  4773. if (empty($USER->access['rsw'])) {
  4774. unset($USER->access['rsw']);
  4775. }
  4776. return true;
  4777. }
  4778. $USER->access['rsw'][$context->path]=$roleid;
  4779. // Load roledefs
  4780. $USER->access = get_role_access_bycontext($roleid, $context,
  4781. $USER->access);
  4782. return true;
  4783. }
  4784. /**
  4785. * Checks if the user has switched roles within the given course.
  4786. *
  4787. * Note: You can only switch roles within the course, hence it takes a courseid
  4788. * rather than a context. On that note Petr volunteered to implement this across
  4789. * all other contexts, all requests for this should be forwarded to him ;)
  4790. *
  4791. * @param int $courseid The id of the course to check
  4792. * @return bool True if the user has switched roles within the course.
  4793. */
  4794. function is_role_switched($courseid) {
  4795. global $USER;
  4796. $context = get_context_instance(CONTEXT_COURSE, $courseid, MUST_EXIST);
  4797. return (!empty($USER->access['rsw'][$context->path]));
  4798. }
  4799. /**
  4800. * Get any role that has an override on exact context
  4801. *
  4802. * @global moodle_database
  4803. * @param stdClass $context The context to check
  4804. * @return array An array of roles
  4805. */
  4806. function get_roles_with_override_on_context($context) {
  4807. global $DB;
  4808. return $DB->get_records_sql("SELECT r.*
  4809. FROM {role_capabilities} rc, {role} r
  4810. WHERE rc.roleid = r.id AND rc.contextid = ?",
  4811. array($context->id));
  4812. }
  4813. /**
  4814. * Get all capabilities for this role on this context (overrides)
  4815. *
  4816. * @param object $role
  4817. * @param object $context
  4818. * @return array
  4819. */
  4820. function get_capabilities_from_role_on_context($role, $context) {
  4821. global $DB;
  4822. return $DB->get_records_sql("SELECT *
  4823. FROM {role_capabilities}
  4824. WHERE contextid = ? AND roleid = ?",
  4825. array($context->id, $role->id));
  4826. }
  4827. /**
  4828. * Find out which roles has assignment on this context
  4829. *
  4830. * @param object $context
  4831. * @return array
  4832. *
  4833. */
  4834. function get_roles_with_assignment_on_context($context) {
  4835. global $DB;
  4836. return $DB->get_records_sql("SELECT r.*
  4837. FROM {role_assignments} ra, {role} r
  4838. WHERE ra.roleid = r.id AND ra.contextid = ?",
  4839. array($context->id));
  4840. }
  4841. /**
  4842. * Find all user assignment of users for this role, on this context
  4843. *
  4844. * @param object $role
  4845. * @param object $context
  4846. * @return array
  4847. */
  4848. function get_users_from_role_on_context($role, $context) {
  4849. global $DB;
  4850. return $DB->get_records_sql("SELECT *
  4851. FROM {role_assignments}
  4852. WHERE contextid = ? AND roleid = ?",
  4853. array($context->id, $role->id));
  4854. }
  4855. /**
  4856. * Simple function returning a boolean true if user has roles
  4857. * in context or parent contexts, otherwise false.
  4858. *
  4859. * @param int $userid
  4860. * @param int $roleid
  4861. * @param int $contextid empty means any context
  4862. * @return bool
  4863. */
  4864. function user_has_role_assignment($userid, $roleid, $contextid = 0) {
  4865. global $DB;
  4866. if ($contextid) {
  4867. if (!$context = get_context_instance_by_id($contextid)) {
  4868. return false;
  4869. }
  4870. $parents = get_parent_contexts($context, true);
  4871. list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r0000');
  4872. $params['userid'] = $userid;
  4873. $params['roleid'] = $roleid;
  4874. $sql = "SELECT COUNT(ra.id)
  4875. FROM {role_assignments} ra
  4876. WHERE ra.userid = :userid AND ra.roleid = :roleid AND ra.contextid $contexts";
  4877. $count = $DB->get_field_sql($sql, $params);
  4878. return ($count > 0);
  4879. } else {
  4880. return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
  4881. }
  4882. }
  4883. /**
  4884. * Get role name or alias if exists and format the text.
  4885. *
  4886. * @param object $role role object
  4887. * @param object $coursecontext
  4888. * @return string name of role in course context
  4889. */
  4890. function role_get_name($role, $coursecontext) {
  4891. global $DB;
  4892. if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) {
  4893. return strip_tags(format_string($r->name));
  4894. } else {
  4895. return strip_tags(format_string($role->name));
  4896. }
  4897. }
  4898. /**
  4899. * Prepare list of roles for display, apply aliases and format text
  4900. *
  4901. * @param array $roleoptions array roleid => rolename or roleid => roleobject
  4902. * @param object $context a context
  4903. * @return array Array of context-specific role names, or role objexts with a ->localname field added.
  4904. */
  4905. function role_fix_names($roleoptions, $context, $rolenamedisplay = ROLENAME_ALIAS) {
  4906. global $DB;
  4907. // Make sure we are working with an array roleid => name. Normally we
  4908. // want to use the unlocalised name if the localised one is not present.
  4909. $newnames = array();
  4910. foreach ($roleoptions as $rid => $roleorname) {
  4911. if ($rolenamedisplay != ROLENAME_ALIAS_RAW) {
  4912. if (is_object($roleorname)) {
  4913. $newnames[$rid] = $roleorname->name;
  4914. } else {
  4915. $newnames[$rid] = $roleorname;
  4916. }
  4917. } else {
  4918. $newnames[$rid] = '';
  4919. }
  4920. }
  4921. // If necessary, get the localised names.
  4922. if ($rolenamedisplay != ROLENAME_ORIGINAL && !empty($context->id)) {
  4923. // Make sure we have a course context.
  4924. if ($context->contextlevel == CONTEXT_MODULE) {
  4925. if ($parentcontextid = array_shift(get_parent_contexts($context))) {
  4926. $context = get_context_instance_by_id($parentcontextid);
  4927. }
  4928. } else if ($context->contextlevel == CONTEXT_BLOCK) {
  4929. do {
  4930. if ($parentcontextid = array_shift(get_parent_contexts($context))) {
  4931. $context = get_context_instance_by_id($parentcontextid);
  4932. }
  4933. } while ($parentcontextid && $context->contextlevel != CONTEXT_COURSE);
  4934. }
  4935. // The get the relevant renames, and use them.
  4936. $aliasnames = $DB->get_records('role_names', array('contextid'=>$context->id));
  4937. foreach ($aliasnames as $alias) {
  4938. if (isset($newnames[$alias->roleid])) {
  4939. if ($rolenamedisplay == ROLENAME_ALIAS || $rolenamedisplay == ROLENAME_ALIAS_RAW) {
  4940. $newnames[$alias->roleid] = $alias->name;
  4941. } else if ($rolenamedisplay == ROLENAME_BOTH) {
  4942. $newnames[$alias->roleid] = $alias->name . ' (' . $roleoptions[$alias->roleid] . ')';
  4943. }
  4944. }
  4945. }
  4946. }
  4947. // Finally, apply format_string and put the result in the right place.
  4948. foreach ($roleoptions as $rid => $roleorname) {
  4949. if ($rolenamedisplay != ROLENAME_ALIAS_RAW) {
  4950. $newnames[$rid] = strip_tags(format_string($newnames[$rid]));
  4951. }
  4952. if (is_object($roleorname)) {
  4953. $roleoptions[$rid]->localname = $newnames[$rid];
  4954. } else {
  4955. $roleoptions[$rid] = $newnames[$rid];
  4956. }
  4957. }
  4958. return $roleoptions;
  4959. }
  4960. /**
  4961. * Aids in detecting if a new line is required when reading a new capability
  4962. *
  4963. * This function helps admin/roles/manage.php etc to detect if a new line should be printed
  4964. * when we read in a new capability.
  4965. * Most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
  4966. * but when we are in grade, all reports/import/export capabilities should be together
  4967. *
  4968. * @param string $cap component string a
  4969. * @param string $comp component string b
  4970. * @param mixed $contextlevel
  4971. * @return bool whether 2 component are in different "sections"
  4972. */
  4973. function component_level_changed($cap, $comp, $contextlevel) {
  4974. if (strstr($cap->component, '/') && strstr($comp, '/')) {
  4975. $compsa = explode('/', $cap->component);
  4976. $compsb = explode('/', $comp);
  4977. // list of system reports
  4978. if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
  4979. return false;
  4980. }
  4981. // we are in gradebook, still
  4982. if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
  4983. ($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
  4984. return false;
  4985. }
  4986. if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
  4987. return false;
  4988. }
  4989. }
  4990. return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
  4991. }
  4992. /**
  4993. * Rebuild all related context depth and path caches
  4994. *
  4995. * @param array $fixcontexts array of contexts, strongtyped
  4996. */
  4997. function rebuild_contexts(array $fixcontexts) {
  4998. global $DB;
  4999. foreach ($fixcontexts as $context) {
  5000. if ($context->path) {
  5001. mark_context_dirty($context->path);
  5002. }
  5003. $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$context->id/%'");
  5004. $DB->set_field('context', 'depth', 0, array('id'=>$context->id));
  5005. }
  5006. build_context_path(false);
  5007. }
  5008. /**
  5009. * Populate context.path and context.depth where missing.
  5010. *
  5011. * @param bool $force force a complete rebuild of the path and depth fields, defaults to false
  5012. */
  5013. function build_context_path($force = false) {
  5014. global $CFG, $DB, $ACCESSLIB_PRIVATE;
  5015. // System context
  5016. $sitectx = get_system_context(!$force);
  5017. $base = '/'.$sitectx->id;
  5018. // Sitecourse
  5019. $sitecoursectx = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>SITEID));
  5020. if ($force || $sitecoursectx->path !== "$base/{$sitecoursectx->id}") {
  5021. $DB->set_field('context', 'path', "$base/{$sitecoursectx->id}", array('id'=>$sitecoursectx->id));
  5022. $DB->set_field('context', 'depth', 2, array('id'=>$sitecoursectx->id));
  5023. $sitecoursectx = $DB->get_record('context', array('contextlevel'=>CONTEXT_COURSE, 'instanceid'=>SITEID));
  5024. }
  5025. $ctxemptyclause = " AND (ctx.path IS NULL
  5026. OR ctx.depth=0) ";
  5027. $emptyclause = " AND ({context}.path IS NULL
  5028. OR {context}.depth=0) ";
  5029. if ($force) {
  5030. $ctxemptyclause = $emptyclause = '';
  5031. }
  5032. /* MDL-11347:
  5033. * - mysql does not allow to use FROM in UPDATE statements
  5034. * - using two tables after UPDATE works in mysql, but might give unexpected
  5035. * results in pg 8 (depends on configuration)
  5036. * - using table alias in UPDATE does not work in pg < 8.2
  5037. *
  5038. * Different code for each database - mostly for performance reasons
  5039. */
  5040. $dbfamily = $DB->get_dbfamily();
  5041. if ($dbfamily == 'mysql') {
  5042. $updatesql = "UPDATE {context} ct, {context_temp} temp
  5043. SET ct.path = temp.path,
  5044. ct.depth = temp.depth
  5045. WHERE ct.id = temp.id";
  5046. } else if ($dbfamily == 'oracle') {
  5047. $updatesql = "UPDATE {context} ct
  5048. SET (ct.path, ct.depth) =
  5049. (SELECT temp.path, temp.depth
  5050. FROM {context_temp} temp
  5051. WHERE temp.id=ct.id)
  5052. WHERE EXISTS (SELECT 'x'
  5053. FROM {context_temp} temp
  5054. WHERE temp.id = ct.id)";
  5055. } else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') {
  5056. $updatesql = "UPDATE {context}
  5057. SET path = temp.path,
  5058. depth = temp.depth
  5059. FROM {context_temp} temp
  5060. WHERE temp.id={context}.id";
  5061. } else {
  5062. // sqlite and others
  5063. $updatesql = "UPDATE {context}
  5064. SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
  5065. depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id)
  5066. WHERE id IN (SELECT id FROM {context_temp})";
  5067. }
  5068. // Top level categories
  5069. $sql = "UPDATE {context}
  5070. SET depth=2, path=" . $DB->sql_concat("'$base/'", 'id') . "
  5071. WHERE contextlevel=".CONTEXT_COURSECAT."
  5072. AND EXISTS (SELECT 'x'
  5073. FROM {course_categories} cc
  5074. WHERE cc.id = {context}.instanceid
  5075. AND cc.depth=1)
  5076. $emptyclause";
  5077. $DB->execute($sql);
  5078. $DB->delete_records('context_temp');
  5079. // Deeper categories - one query per depthlevel
  5080. $maxdepth = $DB->get_field_sql("SELECT MAX(depth)
  5081. FROM {course_categories}");
  5082. for ($n=2; $n<=$maxdepth; $n++) {
  5083. $sql = "INSERT INTO {context_temp} (id, path, depth)
  5084. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", $n+1
  5085. FROM {context} ctx
  5086. JOIN {course_categories} c ON ctx.instanceid=c.id
  5087. JOIN {context} pctx ON c.parent=pctx.instanceid
  5088. WHERE ctx.contextlevel=".CONTEXT_COURSECAT."
  5089. AND pctx.contextlevel=".CONTEXT_COURSECAT."
  5090. AND c.depth=$n
  5091. AND NOT EXISTS (SELECT 'x'
  5092. FROM {context_temp} temp
  5093. WHERE temp.id = ctx.id)
  5094. $ctxemptyclause";
  5095. $DB->execute($sql);
  5096. // this is needed after every loop
  5097. // MDL-11532
  5098. $DB->execute($updatesql);
  5099. $DB->delete_records('context_temp');
  5100. }
  5101. // Courses -- except sitecourse
  5102. $sql = "INSERT INTO {context_temp} (id, path, depth)
  5103. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
  5104. FROM {context} ctx
  5105. JOIN {course} c ON ctx.instanceid=c.id
  5106. JOIN {context} pctx ON c.category=pctx.instanceid
  5107. WHERE ctx.contextlevel=".CONTEXT_COURSE."
  5108. AND c.id!=".SITEID."
  5109. AND pctx.contextlevel=".CONTEXT_COURSECAT."
  5110. AND NOT EXISTS (SELECT 'x'
  5111. FROM {context_temp} temp
  5112. WHERE temp.id = ctx.id)
  5113. $ctxemptyclause";
  5114. $DB->execute($sql);
  5115. $DB->execute($updatesql);
  5116. $DB->delete_records('context_temp');
  5117. // Module instances
  5118. $sql = "INSERT INTO {context_temp} (id, path, depth)
  5119. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
  5120. FROM {context} ctx
  5121. JOIN {course_modules} cm ON ctx.instanceid=cm.id
  5122. JOIN {context} pctx ON cm.course=pctx.instanceid
  5123. WHERE ctx.contextlevel=".CONTEXT_MODULE."
  5124. AND pctx.contextlevel=".CONTEXT_COURSE."
  5125. AND NOT EXISTS (SELECT 'x'
  5126. FROM {context_temp} temp
  5127. WHERE temp.id = ctx.id)
  5128. $ctxemptyclause";
  5129. $DB->execute($sql);
  5130. $DB->execute($updatesql);
  5131. $DB->delete_records('context_temp');
  5132. // User
  5133. $sql = "UPDATE {context}
  5134. SET depth=2, path=".$DB->sql_concat("'$base/'", 'id')."
  5135. WHERE contextlevel=".CONTEXT_USER."
  5136. AND EXISTS (SELECT 'x'
  5137. FROM {user} u
  5138. WHERE u.id = {context}.instanceid)
  5139. $emptyclause ";
  5140. $DB->execute($sql);
  5141. // Blocks
  5142. // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent
  5143. $sql = "INSERT INTO {context_temp} (id, path, depth)
  5144. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1
  5145. FROM {context} ctx
  5146. JOIN {block_instances} bi ON ctx.instanceid = bi.id
  5147. JOIN {context} pctx ON bi.parentcontextid = pctx.id
  5148. WHERE ctx.contextlevel=".CONTEXT_BLOCK."
  5149. AND pctx.path IS NOT NULL
  5150. AND NOT EXISTS (SELECT 'x'
  5151. FROM {context_temp} temp
  5152. WHERE temp.id = ctx.id)
  5153. $ctxemptyclause";
  5154. $DB->execute($sql);
  5155. $DB->execute($updatesql);
  5156. $DB->delete_records('context_temp');
  5157. // reset static course cache - it might have incorrect cached data
  5158. $ACCESSLIB_PRIVATE->contexcache->reset();
  5159. }
  5160. /**
  5161. * Update the path field of the context and all dep. subcontexts that follow
  5162. *
  5163. * Update the path field of the context and
  5164. * all the dependent subcontexts that follow
  5165. * the move.
  5166. *
  5167. * The most important thing here is to be as
  5168. * DB efficient as possible. This op can have a
  5169. * massive impact in the DB.
  5170. *
  5171. * @param stdClass $current context obj
  5172. * @param stdClass $newparent new parent obj
  5173. *
  5174. */
  5175. function context_moved($context, $newparent) {
  5176. global $DB;
  5177. $frompath = $context->path;
  5178. $newpath = $newparent->path . '/' . $context->id;
  5179. $setdepth = '';
  5180. if (($newparent->depth +1) != $context->depth) {
  5181. $diff = $newparent->depth - $context->depth + 1;
  5182. $setdepth = ", depth = depth + $diff";
  5183. }
  5184. $sql = "UPDATE {context}
  5185. SET path = ?
  5186. $setdepth
  5187. WHERE path = ?";
  5188. $params = array($newpath, $frompath);
  5189. $DB->execute($sql, $params);
  5190. $sql = "UPDATE {context}
  5191. SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath)+1))."
  5192. $setdepth
  5193. WHERE path LIKE ?";
  5194. $params = array($newpath, "{$frompath}/%");
  5195. $DB->execute($sql, $params);
  5196. mark_context_dirty($frompath);
  5197. mark_context_dirty($newpath);
  5198. }
  5199. /**
  5200. * Preloads context information together with instances.
  5201. * NOTE: in future this function may return empty strings
  5202. * if we implement different caching.
  5203. *
  5204. * @param string $joinon for example 'u.id'
  5205. * @param string $contextlevel context level of instance in $joinon
  5206. * @param string $tablealias context table alias
  5207. * @return array with two values - select and join part
  5208. */
  5209. function context_instance_preload_sql($joinon, $contextlevel, $tablealias) {
  5210. $select = ", $tablealias.id AS ctxid, $tablealias.path AS ctxpath, $tablealias.depth AS ctxdepth, $tablealias.contextlevel AS ctxlevel, $tablealias.instanceid AS ctxinstance";
  5211. $join = "LEFT JOIN {context} $tablealias ON ($tablealias.instanceid = $joinon AND $tablealias.contextlevel = $contextlevel)";
  5212. return array($select, $join);
  5213. }
  5214. /**
  5215. * Preloads context information from db record and strips the cached info.
  5216. * The db request has to ontain both the $join and $select from context_instance_preload_sql()
  5217. *
  5218. * @param object $rec
  5219. * @return void (modifies $rec)
  5220. */
  5221. function context_instance_preload(stdClass $rec) {
  5222. global $ACCESSLIB_PRIVATE;
  5223. if (empty($rec->ctxid)) {
  5224. // $rec does not have enough data, passed here repeatedly or context does not exist yet
  5225. return;
  5226. }
  5227. // note: in PHP5 the objects are passed by reference, no need to return $rec
  5228. $context = new stdClass();
  5229. $context->id = $rec->ctxid; unset($rec->ctxid);
  5230. $context->path = $rec->ctxpath; unset($rec->ctxpath);
  5231. $context->depth = $rec->ctxdepth; unset($rec->ctxdepth);
  5232. $context->contextlevel = $rec->ctxlevel; unset($rec->ctxlevel);
  5233. $context->instanceid = $rec->ctxinstance; unset($rec->ctxinstance);
  5234. $ACCESSLIB_PRIVATE->contexcache->add($context);
  5235. }
  5236. /**
  5237. * Fetch recent dirty contexts to know cheaply whether our $USER->access
  5238. * is stale and needs to be reloaded.
  5239. *
  5240. * Uses cache_flags
  5241. * @param int $time
  5242. * @return array Array of dirty contexts
  5243. */
  5244. function get_dirty_contexts($time) {
  5245. return get_cache_flags('accesslib/dirtycontexts', $time-2);
  5246. }
  5247. /**
  5248. * Mark a context as dirty (with timestamp)
  5249. * so as to force reloading of the context.
  5250. *
  5251. * @param string $path context path
  5252. */
  5253. function mark_context_dirty($path) {
  5254. global $CFG, $ACCESSLIB_PRIVATE;
  5255. if (during_initial_install()) {
  5256. return;
  5257. }
  5258. // only if it is a non-empty string
  5259. if (is_string($path) && $path !== '') {
  5260. set_cache_flag('accesslib/dirtycontexts', $path, 1, time()+$CFG->sessiontimeout);
  5261. if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
  5262. $ACCESSLIB_PRIVATE->dirtycontexts[$path] = 1;
  5263. }
  5264. }
  5265. }
  5266. /**
  5267. * Will walk the contextpath to answer whether
  5268. * the contextpath is dirty
  5269. *
  5270. * @param array $contexts array of strings
  5271. * @param obj|array $dirty Dirty contexts from get_dirty_contexts()
  5272. * @return bool
  5273. */
  5274. function is_contextpath_dirty($pathcontexts, $dirty) {
  5275. $path = '';
  5276. foreach ($pathcontexts as $ctx) {
  5277. $path = $path.'/'.$ctx;
  5278. if (isset($dirty[$path])) {
  5279. return true;
  5280. }
  5281. }
  5282. return false;
  5283. }
  5284. /**
  5285. * Fix the roles.sortorder field in the database, so it contains sequential integers,
  5286. * and return an array of roleids in order.
  5287. *
  5288. * @param array $allroles array of roles, as returned by get_all_roles();
  5289. * @return array $role->sortorder =-> $role->id with the keys in ascending order.
  5290. */
  5291. function fix_role_sortorder($allroles) {
  5292. global $DB;
  5293. $rolesort = array();
  5294. $i = 0;
  5295. foreach ($allroles as $role) {
  5296. $rolesort[$i] = $role->id;
  5297. if ($role->sortorder != $i) {
  5298. $r = new stdClass();
  5299. $r->id = $role->id;
  5300. $r->sortorder = $i;
  5301. $DB->update_record('role', $r);
  5302. $allroles[$role->id]->sortorder = $i;
  5303. }
  5304. $i++;
  5305. }
  5306. return $rolesort;
  5307. }
  5308. /**
  5309. * Switch the sort order of two roles (used in admin/roles/manage.php).
  5310. *
  5311. * @param object $first The first role. Actually, only ->sortorder is used.
  5312. * @param object $second The second role. Actually, only ->sortorder is used.
  5313. * @return boolean success or failure
  5314. */
  5315. function switch_roles($first, $second) {
  5316. global $DB;
  5317. $temp = $DB->get_field('role', 'MAX(sortorder) + 1', array());
  5318. $result = $DB->set_field('role', 'sortorder', $temp, array('sortorder' => $first->sortorder));
  5319. $result = $result && $DB->set_field('role', 'sortorder', $first->sortorder, array('sortorder' => $second->sortorder));
  5320. $result = $result && $DB->set_field('role', 'sortorder', $second->sortorder, array('sortorder' => $temp));
  5321. return $result;
  5322. }
  5323. /**
  5324. * duplicates all the base definitions of a role
  5325. *
  5326. * @param object $sourcerole role to copy from
  5327. * @param int $targetrole id of role to copy to
  5328. */
  5329. function role_cap_duplicate($sourcerole, $targetrole) {
  5330. global $DB;
  5331. $systemcontext = get_context_instance(CONTEXT_SYSTEM);
  5332. $caps = $DB->get_records_sql("SELECT *
  5333. FROM {role_capabilities}
  5334. WHERE roleid = ? AND contextid = ?",
  5335. array($sourcerole->id, $systemcontext->id));
  5336. // adding capabilities
  5337. foreach ($caps as $cap) {
  5338. unset($cap->id);
  5339. $cap->roleid = $targetrole;
  5340. $DB->insert_record('role_capabilities', $cap);
  5341. }
  5342. }
  5343. /**
  5344. * Returns two lists, this can be used to find out if user has capability.
  5345. * Having any needed role and no forbidden role in this context means
  5346. * user has this capability in this context.
  5347. * Use get_role_names_with_cap_in_context() if you need role names to display in the UI
  5348. *
  5349. * @param object $context
  5350. * @param string $capability
  5351. * @return array($neededroles, $forbiddenroles)
  5352. */
  5353. function get_roles_with_cap_in_context($context, $capability) {
  5354. global $DB;
  5355. $ctxids = trim($context->path, '/'); // kill leading slash
  5356. $ctxids = str_replace('/', ',', $ctxids);
  5357. $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
  5358. FROM {role_capabilities} rc
  5359. JOIN {context} ctx ON ctx.id = rc.contextid
  5360. WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
  5361. ORDER BY rc.roleid ASC, ctx.depth DESC";
  5362. $params = array('cap'=>$capability);
  5363. if (!$capdefs = $DB->get_records_sql($sql, $params)) {
  5364. // no cap definitions --> no capability
  5365. return array(array(), array());
  5366. }
  5367. $forbidden = array();
  5368. $needed = array();
  5369. foreach($capdefs as $def) {
  5370. if (isset($forbidden[$def->roleid])) {
  5371. continue;
  5372. }
  5373. if ($def->permission == CAP_PROHIBIT) {
  5374. $forbidden[$def->roleid] = $def->roleid;
  5375. unset($needed[$def->roleid]);
  5376. continue;
  5377. }
  5378. if (!isset($needed[$def->roleid])) {
  5379. if ($def->permission == CAP_ALLOW) {
  5380. $needed[$def->roleid] = true;
  5381. } else if ($def->permission == CAP_PREVENT) {
  5382. $needed[$def->roleid] = false;
  5383. }
  5384. }
  5385. }
  5386. unset($capdefs);
  5387. // remove all those roles not allowing
  5388. foreach($needed as $key=>$value) {
  5389. if (!$value) {
  5390. unset($needed[$key]);
  5391. } else {
  5392. $needed[$key] = $key;
  5393. }
  5394. }
  5395. return array($needed, $forbidden);
  5396. }
  5397. /**
  5398. * Returns an array of role IDs that have ALL of the the supplied capabilities
  5399. * Uses get_roles_with_cap_in_context(). Returns $allowed minus $forbidden
  5400. *
  5401. * @param object $context
  5402. * @param array $capabilities An array of capabilities
  5403. * @return array of roles with all of the required capabilities
  5404. */
  5405. function get_roles_with_caps_in_context($context, $capabilities) {
  5406. $neededarr = array();
  5407. $forbiddenarr = array();
  5408. foreach($capabilities as $caprequired) {
  5409. list($neededarr[], $forbiddenarr[]) = get_roles_with_cap_in_context($context, $caprequired);
  5410. }
  5411. $rolesthatcanrate = array();
  5412. if (!empty($neededarr)) {
  5413. foreach ($neededarr as $needed) {
  5414. if (empty($rolesthatcanrate)) {
  5415. $rolesthatcanrate = $needed;
  5416. } else {
  5417. //only want roles that have all caps
  5418. $rolesthatcanrate = array_intersect_key($rolesthatcanrate,$needed);
  5419. }
  5420. }
  5421. }
  5422. if (!empty($forbiddenarr) && !empty($rolesthatcanrate)) {
  5423. foreach ($forbiddenarr as $forbidden) {
  5424. //remove any roles that are forbidden any of the caps
  5425. $rolesthatcanrate = array_diff($rolesthatcanrate, $forbidden);
  5426. }
  5427. }
  5428. return $rolesthatcanrate;
  5429. }
  5430. /**
  5431. * Returns an array of role names that have ALL of the the supplied capabilities
  5432. * Uses get_roles_with_caps_in_context(). Returns $allowed minus $forbidden
  5433. *
  5434. * @param object $context
  5435. * @param array $capabilities An array of capabilities
  5436. * @return array of roles with all of the required capabilities
  5437. */
  5438. function get_role_names_with_caps_in_context($context, $capabilities) {
  5439. global $DB;
  5440. $rolesthatcanrate = get_roles_with_caps_in_context($context, $capabilities);
  5441. $allroles = array();
  5442. $roles = $DB->get_records('role', null, 'sortorder DESC');
  5443. foreach ($roles as $roleid=>$role) {
  5444. $allroles[$roleid] = $role->name;
  5445. }
  5446. $rolenames = array();
  5447. foreach ($rolesthatcanrate as $r) {
  5448. $rolenames[$r] = $allroles[$r];
  5449. }
  5450. $rolenames = role_fix_names($rolenames, $context);
  5451. return $rolenames;
  5452. }
  5453. /**
  5454. * This function verifies the prohibit comes from this context
  5455. * and there are no more prohibits in parent contexts.
  5456. * @param object $context
  5457. * @param string $capability name
  5458. * @return bool
  5459. */
  5460. function prohibit_is_removable($roleid, $context, $capability) {
  5461. global $DB;
  5462. $ctxids = trim($context->path, '/'); // kill leading slash
  5463. $ctxids = str_replace('/', ',', $ctxids);
  5464. $params = array('roleid'=>$roleid, 'cap'=>$capability, 'prohibit'=>CAP_PROHIBIT);
  5465. $sql = "SELECT ctx.id
  5466. FROM {role_capabilities} rc
  5467. JOIN {context} ctx ON ctx.id = rc.contextid
  5468. WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
  5469. ORDER BY ctx.depth DESC";
  5470. if (!$prohibits = $DB->get_records_sql($sql, $params)) {
  5471. // no prohibits == nothing to remove
  5472. return true;
  5473. }
  5474. if (count($prohibits) > 1) {
  5475. // more prohibints can not be removed
  5476. return false;
  5477. }
  5478. return !empty($prohibits[$context->id]);
  5479. }
  5480. /**
  5481. * More user friendly role permission changing,
  5482. * it should produce as few overrides as possible.
  5483. * @param int $roleid
  5484. * @param object $context
  5485. * @param string $capname capability name
  5486. * @param int $permission
  5487. * @return void
  5488. */
  5489. function role_change_permission($roleid, $context, $capname, $permission) {
  5490. global $DB;
  5491. if ($permission == CAP_INHERIT) {
  5492. unassign_capability($capname, $roleid, $context->id);
  5493. mark_context_dirty($context->path);
  5494. return;
  5495. }
  5496. $ctxids = trim($context->path, '/'); // kill leading slash
  5497. $ctxids = str_replace('/', ',', $ctxids);
  5498. $params = array('roleid'=>$roleid, 'cap'=>$capname);
  5499. $sql = "SELECT ctx.id, rc.permission, ctx.depth
  5500. FROM {role_capabilities} rc
  5501. JOIN {context} ctx ON ctx.id = rc.contextid
  5502. WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
  5503. ORDER BY ctx.depth DESC";
  5504. if ($existing = $DB->get_records_sql($sql, $params)) {
  5505. foreach($existing as $e) {
  5506. if ($e->permission == CAP_PROHIBIT) {
  5507. // prohibit can not be overridden, no point in changing anything
  5508. return;
  5509. }
  5510. }
  5511. $lowest = array_shift($existing);
  5512. if ($lowest->permission == $permission) {
  5513. // permission already set in this context or parent - nothing to do
  5514. return;
  5515. }
  5516. if ($existing) {
  5517. $parent = array_shift($existing);
  5518. if ($parent->permission == $permission) {
  5519. // permission already set in parent context or parent - just unset in this context
  5520. // we do this because we want as few overrides as possible for performance reasons
  5521. unassign_capability($capname, $roleid, $context->id);
  5522. mark_context_dirty($context->path);
  5523. return;
  5524. }
  5525. }
  5526. } else {
  5527. if ($permission == CAP_PREVENT) {
  5528. // nothing means role does not have permission
  5529. return;
  5530. }
  5531. }
  5532. // assign the needed capability
  5533. assign_capability($capname, $permission, $roleid, $context->id, true);
  5534. // force cap reloading
  5535. mark_context_dirty($context->path);
  5536. }