PageRenderTime 97ms CodeModel.GetById 15ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/accesslib.php

http://github.com/moodle/moodle
PHP | 7630 lines | 4256 code | 953 blank | 2421 comment | 816 complexity | 6a179ac9a9da8f1ad2194a8d84323484 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.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. * - context_course::instance($courseid), context_module::instance($cm->id), context_coursecat::instance($catid)
  25. * - context::instance_by_id($contextid)
  26. * - $context->get_parent_contexts();
  27. * - $context->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. * - is_enrolled()
  36. * - is_viewing()
  37. * - is_guest()
  38. * - is_siteadmin()
  39. * - isguestuser()
  40. * - isloggedin()
  41. *
  42. * What courses has this user access to?
  43. * - get_enrolled_users()
  44. *
  45. * What users can do X in this context?
  46. * - get_enrolled_users() - at and bellow course context
  47. * - get_users_by_capability() - above course context
  48. *
  49. * Modify roles
  50. * - role_assign()
  51. * - role_unassign()
  52. * - role_unassign_all()
  53. *
  54. * Advanced - for internal use only
  55. * - load_all_capabilities()
  56. * - reload_all_capabilities()
  57. * - has_capability_in_accessdata()
  58. * - get_user_roles_sitewide_accessdata()
  59. * - etc.
  60. *
  61. * <b>Name conventions</b>
  62. *
  63. * "ctx" means context
  64. * "ra" means role assignment
  65. * "rdef" means role definition
  66. *
  67. * <b>accessdata</b>
  68. *
  69. * Access control data is held in the "accessdata" array
  70. * which - for the logged-in user, will be in $USER->access
  71. *
  72. * For other users can be generated and passed around (but may also be cached
  73. * against userid in $ACCESSLIB_PRIVATE->accessdatabyuser).
  74. *
  75. * $accessdata is a multidimensional array, holding
  76. * role assignments (RAs), role switches and initialization time.
  77. *
  78. * Things are keyed on "contextpaths" (the path field of
  79. * the context table) for fast walking up/down the tree.
  80. * <code>
  81. * $accessdata['ra'][$contextpath] = array($roleid=>$roleid)
  82. * [$contextpath] = array($roleid=>$roleid)
  83. * [$contextpath] = array($roleid=>$roleid)
  84. * </code>
  85. *
  86. * <b>Stale accessdata</b>
  87. *
  88. * For the logged-in user, accessdata is long-lived.
  89. *
  90. * On each pageload we load $ACCESSLIB_PRIVATE->dirtycontexts which lists
  91. * context paths affected by changes. Any check at-or-below
  92. * a dirty context will trigger a transparent reload of accessdata.
  93. *
  94. * Changes at the system level will force the reload for everyone.
  95. *
  96. * <b>Default role caps</b>
  97. * The default role assignment is not in the DB, so we
  98. * add it manually to accessdata.
  99. *
  100. * This means that functions that work directly off the
  101. * DB need to ensure that the default role caps
  102. * are dealt with appropriately.
  103. *
  104. * @package core_access
  105. * @copyright 1999 onwards Martin Dougiamas http://dougiamas.com
  106. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  107. */
  108. defined('MOODLE_INTERNAL') || die();
  109. /** No capability change */
  110. define('CAP_INHERIT', 0);
  111. /** Allow permission, overrides CAP_PREVENT defined in parent contexts */
  112. define('CAP_ALLOW', 1);
  113. /** Prevent permission, overrides CAP_ALLOW defined in parent contexts */
  114. define('CAP_PREVENT', -1);
  115. /** Prohibit permission, overrides everything in current and child contexts */
  116. define('CAP_PROHIBIT', -1000);
  117. /** System context level - only one instance in every system */
  118. define('CONTEXT_SYSTEM', 10);
  119. /** User context level - one instance for each user describing what others can do to user */
  120. define('CONTEXT_USER', 30);
  121. /** Course category context level - one instance for each category */
  122. define('CONTEXT_COURSECAT', 40);
  123. /** Course context level - one instances for each course */
  124. define('CONTEXT_COURSE', 50);
  125. /** Course module context level - one instance for each course module */
  126. define('CONTEXT_MODULE', 70);
  127. /**
  128. * Block context level - one instance for each block, sticky blocks are tricky
  129. * because ppl think they should be able to override them at lower contexts.
  130. * Any other context level instance can be parent of block context.
  131. */
  132. define('CONTEXT_BLOCK', 80);
  133. /** Capability allow management of trusts - NOT IMPLEMENTED YET - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
  134. define('RISK_MANAGETRUST', 0x0001);
  135. /** Capability allows changes in system configuration - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
  136. define('RISK_CONFIG', 0x0002);
  137. /** Capability allows user to add scripted content - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
  138. define('RISK_XSS', 0x0004);
  139. /** Capability allows access to personal user information - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
  140. define('RISK_PERSONAL', 0x0008);
  141. /** Capability allows users to add content others may see - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
  142. define('RISK_SPAM', 0x0010);
  143. /** capability allows mass delete of data belonging to other users - see {@link http://docs.moodle.org/dev/Hardening_new_Roles_system} */
  144. define('RISK_DATALOSS', 0x0020);
  145. /** rolename displays - the name as defined in the role definition, localised if name empty */
  146. define('ROLENAME_ORIGINAL', 0);
  147. /** rolename displays - the name as defined by a role alias at the course level, falls back to ROLENAME_ORIGINAL if alias not present */
  148. define('ROLENAME_ALIAS', 1);
  149. /** rolename displays - Both, like this: Role alias (Original) */
  150. define('ROLENAME_BOTH', 2);
  151. /** rolename displays - the name as defined in the role definition and the shortname in brackets */
  152. define('ROLENAME_ORIGINALANDSHORT', 3);
  153. /** rolename displays - the name as defined by a role alias, in raw form suitable for editing */
  154. define('ROLENAME_ALIAS_RAW', 4);
  155. /** rolename displays - the name is simply short role name */
  156. define('ROLENAME_SHORT', 5);
  157. if (!defined('CONTEXT_CACHE_MAX_SIZE')) {
  158. /** maximum size of context cache - it is possible to tweak this config.php or in any script before inclusion of context.php */
  159. define('CONTEXT_CACHE_MAX_SIZE', 2500);
  160. }
  161. /**
  162. * Although this looks like a global variable, it isn't really.
  163. *
  164. * It is just a private implementation detail to accesslib that MUST NOT be used elsewhere.
  165. * It is used to cache various bits of data between function calls for performance reasons.
  166. * Sadly, a PHP global variable is the only way to implement this, without rewriting everything
  167. * as methods of a class, instead of functions.
  168. *
  169. * @access private
  170. * @global stdClass $ACCESSLIB_PRIVATE
  171. * @name $ACCESSLIB_PRIVATE
  172. */
  173. global $ACCESSLIB_PRIVATE;
  174. $ACCESSLIB_PRIVATE = new stdClass();
  175. $ACCESSLIB_PRIVATE->cacheroledefs = array(); // Holds site-wide role definitions.
  176. $ACCESSLIB_PRIVATE->dirtycontexts = null; // Dirty contexts cache, loaded from DB once per page
  177. $ACCESSLIB_PRIVATE->dirtyusers = null; // Dirty users cache, loaded from DB once per $USER->id
  178. $ACCESSLIB_PRIVATE->accessdatabyuser = array(); // Holds the cache of $accessdata structure for users (including $USER)
  179. /**
  180. * Clears accesslib's private caches. ONLY BE USED BY UNIT TESTS
  181. *
  182. * This method should ONLY BE USED BY UNIT TESTS. It clears all of
  183. * accesslib's private caches. You need to do this before setting up test data,
  184. * and also at the end of the tests.
  185. *
  186. * @access private
  187. * @return void
  188. */
  189. function accesslib_clear_all_caches_for_unit_testing() {
  190. global $USER;
  191. if (!PHPUNIT_TEST) {
  192. throw new coding_exception('You must not call clear_all_caches outside of unit tests.');
  193. }
  194. accesslib_clear_all_caches(true);
  195. accesslib_reset_role_cache();
  196. unset($USER->access);
  197. }
  198. /**
  199. * Clears accesslib's private caches. ONLY BE USED FROM THIS LIBRARY FILE!
  200. *
  201. * This reset does not touch global $USER.
  202. *
  203. * @access private
  204. * @param bool $resetcontexts
  205. * @return void
  206. */
  207. function accesslib_clear_all_caches($resetcontexts) {
  208. global $ACCESSLIB_PRIVATE;
  209. $ACCESSLIB_PRIVATE->dirtycontexts = null;
  210. $ACCESSLIB_PRIVATE->dirtyusers = null;
  211. $ACCESSLIB_PRIVATE->accessdatabyuser = array();
  212. if ($resetcontexts) {
  213. context_helper::reset_caches();
  214. }
  215. }
  216. /**
  217. * Full reset of accesslib's private role cache. ONLY TO BE USED FROM THIS LIBRARY FILE!
  218. *
  219. * This reset does not touch global $USER.
  220. *
  221. * Note: Only use this when the roles that need a refresh are unknown.
  222. *
  223. * @see accesslib_clear_role_cache()
  224. *
  225. * @access private
  226. * @return void
  227. */
  228. function accesslib_reset_role_cache() {
  229. global $ACCESSLIB_PRIVATE;
  230. $ACCESSLIB_PRIVATE->cacheroledefs = array();
  231. $cache = cache::make('core', 'roledefs');
  232. $cache->purge();
  233. }
  234. /**
  235. * Clears accesslib's private cache of a specific role or roles. ONLY BE USED FROM THIS LIBRARY FILE!
  236. *
  237. * This reset does not touch global $USER.
  238. *
  239. * @access private
  240. * @param int|array $roles
  241. * @return void
  242. */
  243. function accesslib_clear_role_cache($roles) {
  244. global $ACCESSLIB_PRIVATE;
  245. if (!is_array($roles)) {
  246. $roles = [$roles];
  247. }
  248. foreach ($roles as $role) {
  249. if (isset($ACCESSLIB_PRIVATE->cacheroledefs[$role])) {
  250. unset($ACCESSLIB_PRIVATE->cacheroledefs[$role]);
  251. }
  252. }
  253. $cache = cache::make('core', 'roledefs');
  254. $cache->delete_many($roles);
  255. }
  256. /**
  257. * Role is assigned at system context.
  258. *
  259. * @access private
  260. * @param int $roleid
  261. * @return array
  262. */
  263. function get_role_access($roleid) {
  264. $accessdata = get_empty_accessdata();
  265. $accessdata['ra']['/'.SYSCONTEXTID] = array((int)$roleid => (int)$roleid);
  266. return $accessdata;
  267. }
  268. /**
  269. * Fetch raw "site wide" role definitions.
  270. * Even MUC static acceleration cache appears a bit slow for this.
  271. * Important as can be hit hundreds of times per page.
  272. *
  273. * @param array $roleids List of role ids to fetch definitions for.
  274. * @return array Complete definition for each requested role.
  275. */
  276. function get_role_definitions(array $roleids) {
  277. global $ACCESSLIB_PRIVATE;
  278. if (empty($roleids)) {
  279. return array();
  280. }
  281. // Grab all keys we have not yet got in our static cache.
  282. if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
  283. $cache = cache::make('core', 'roledefs');
  284. foreach ($cache->get_many($uncached) as $roleid => $cachedroledef) {
  285. if (is_array($cachedroledef)) {
  286. $ACCESSLIB_PRIVATE->cacheroledefs[$roleid] = $cachedroledef;
  287. }
  288. }
  289. // Check we have the remaining keys from the MUC.
  290. if ($uncached = array_diff($roleids, array_keys($ACCESSLIB_PRIVATE->cacheroledefs))) {
  291. $uncached = get_role_definitions_uncached($uncached);
  292. $ACCESSLIB_PRIVATE->cacheroledefs += $uncached;
  293. $cache->set_many($uncached);
  294. }
  295. }
  296. // Return just the roles we need.
  297. return array_intersect_key($ACCESSLIB_PRIVATE->cacheroledefs, array_flip($roleids));
  298. }
  299. /**
  300. * Query raw "site wide" role definitions.
  301. *
  302. * @param array $roleids List of role ids to fetch definitions for.
  303. * @return array Complete definition for each requested role.
  304. */
  305. function get_role_definitions_uncached(array $roleids) {
  306. global $DB;
  307. if (empty($roleids)) {
  308. return array();
  309. }
  310. // Create a blank results array: even if a role has no capabilities,
  311. // we need to ensure it is included in the results to show we have
  312. // loaded all the capabilities that there are.
  313. $rdefs = array();
  314. foreach ($roleids as $roleid) {
  315. $rdefs[$roleid] = array();
  316. }
  317. // Load all the capabilities for these roles in all contexts.
  318. list($sql, $params) = $DB->get_in_or_equal($roleids);
  319. $sql = "SELECT ctx.path, rc.roleid, rc.capability, rc.permission
  320. FROM {role_capabilities} rc
  321. JOIN {context} ctx ON rc.contextid = ctx.id
  322. JOIN {capabilities} cap ON rc.capability = cap.name
  323. WHERE rc.roleid $sql";
  324. $rs = $DB->get_recordset_sql($sql, $params);
  325. // Store the capabilities into the expected data structure.
  326. foreach ($rs as $rd) {
  327. if (!isset($rdefs[$rd->roleid][$rd->path])) {
  328. $rdefs[$rd->roleid][$rd->path] = array();
  329. }
  330. $rdefs[$rd->roleid][$rd->path][$rd->capability] = (int) $rd->permission;
  331. }
  332. $rs->close();
  333. // Sometimes (e.g. get_user_capability_course_helper::get_capability_info_at_each_context)
  334. // we process role definitinons in a way that requires we see parent contexts
  335. // before child contexts. This sort ensures that works (and is faster than
  336. // sorting in the SQL query).
  337. foreach ($rdefs as $roleid => $rdef) {
  338. ksort($rdefs[$roleid]);
  339. }
  340. return $rdefs;
  341. }
  342. /**
  343. * Get the default guest role, this is used for guest account,
  344. * search engine spiders, etc.
  345. *
  346. * @return stdClass role record
  347. */
  348. function get_guest_role() {
  349. global $CFG, $DB;
  350. if (empty($CFG->guestroleid)) {
  351. if ($roles = $DB->get_records('role', array('archetype'=>'guest'))) {
  352. $guestrole = array_shift($roles); // Pick the first one
  353. set_config('guestroleid', $guestrole->id);
  354. return $guestrole;
  355. } else {
  356. debugging('Can not find any guest role!');
  357. return false;
  358. }
  359. } else {
  360. if ($guestrole = $DB->get_record('role', array('id'=>$CFG->guestroleid))) {
  361. return $guestrole;
  362. } else {
  363. // somebody is messing with guest roles, remove incorrect setting and try to find a new one
  364. set_config('guestroleid', '');
  365. return get_guest_role();
  366. }
  367. }
  368. }
  369. /**
  370. * Check whether a user has a particular capability in a given context.
  371. *
  372. * For example:
  373. * $context = context_module::instance($cm->id);
  374. * has_capability('mod/forum:replypost', $context)
  375. *
  376. * By default checks the capabilities of the current user, but you can pass a
  377. * different userid. By default will return true for admin users, but you can override that with the fourth argument.
  378. *
  379. * Guest and not-logged-in users can never get any dangerous capability - that is any write capability
  380. * or capabilities with XSS, config or data loss risks.
  381. *
  382. * @category access
  383. *
  384. * @param string $capability the name of the capability to check. For example mod/forum:view
  385. * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
  386. * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
  387. * @param boolean $doanything If false, ignores effect of admin role assignment
  388. * @return boolean true if the user has this capability. Otherwise false.
  389. */
  390. function has_capability($capability, context $context, $user = null, $doanything = true) {
  391. global $USER, $CFG, $SCRIPT, $ACCESSLIB_PRIVATE;
  392. if (during_initial_install()) {
  393. if ($SCRIPT === "/$CFG->admin/index.php"
  394. or $SCRIPT === "/$CFG->admin/cli/install.php"
  395. or $SCRIPT === "/$CFG->admin/cli/install_database.php"
  396. or (defined('BEHAT_UTIL') and BEHAT_UTIL)
  397. or (defined('PHPUNIT_UTIL') and PHPUNIT_UTIL)) {
  398. // we are in an installer - roles can not work yet
  399. return true;
  400. } else {
  401. return false;
  402. }
  403. }
  404. if (strpos($capability, 'moodle/legacy:') === 0) {
  405. throw new coding_exception('Legacy capabilities can not be used any more!');
  406. }
  407. if (!is_bool($doanything)) {
  408. throw new coding_exception('Capability parameter "doanything" is wierd, only true or false is allowed. This has to be fixed in code.');
  409. }
  410. // capability must exist
  411. if (!$capinfo = get_capability_info($capability)) {
  412. debugging('Capability "'.$capability.'" was not found! This has to be fixed in code.');
  413. return false;
  414. }
  415. if (!isset($USER->id)) {
  416. // should never happen
  417. $USER->id = 0;
  418. debugging('Capability check being performed on a user with no ID.', DEBUG_DEVELOPER);
  419. }
  420. // make sure there is a real user specified
  421. if ($user === null) {
  422. $userid = $USER->id;
  423. } else {
  424. $userid = is_object($user) ? $user->id : $user;
  425. }
  426. // make sure forcelogin cuts off not-logged-in users if enabled
  427. if (!empty($CFG->forcelogin) and $userid == 0) {
  428. return false;
  429. }
  430. // make sure the guest account and not-logged-in users never get any risky caps no matter what the actual settings are.
  431. if (($capinfo->captype === 'write') or ($capinfo->riskbitmask & (RISK_XSS | RISK_CONFIG | RISK_DATALOSS))) {
  432. if (isguestuser($userid) or $userid == 0) {
  433. return false;
  434. }
  435. }
  436. // Check whether context locking is enabled.
  437. if (!empty($CFG->contextlocking)) {
  438. if ($capinfo->captype === 'write' && $context->locked) {
  439. // Context locking applies to any write capability in a locked context.
  440. // It does not apply to moodle/site:managecontextlocks - this is to allow context locking to be unlocked.
  441. if ($capinfo->name !== 'moodle/site:managecontextlocks') {
  442. // It applies to all users who are not site admins.
  443. // It also applies to site admins when contextlockappliestoadmin is set.
  444. if (!is_siteadmin($userid) || !empty($CFG->contextlockappliestoadmin)) {
  445. return false;
  446. }
  447. }
  448. }
  449. }
  450. // somehow make sure the user is not deleted and actually exists
  451. if ($userid != 0) {
  452. if ($userid == $USER->id and isset($USER->deleted)) {
  453. // this prevents one query per page, it is a bit of cheating,
  454. // but hopefully session is terminated properly once user is deleted
  455. if ($USER->deleted) {
  456. return false;
  457. }
  458. } else {
  459. if (!context_user::instance($userid, IGNORE_MISSING)) {
  460. // no user context == invalid userid
  461. return false;
  462. }
  463. }
  464. }
  465. // context path/depth must be valid
  466. if (empty($context->path) or $context->depth == 0) {
  467. // this should not happen often, each upgrade tries to rebuild the context paths
  468. debugging('Context id '.$context->id.' does not have valid path, please use context_helper::build_all_paths()');
  469. if (is_siteadmin($userid)) {
  470. return true;
  471. } else {
  472. return false;
  473. }
  474. }
  475. // Find out if user is admin - it is not possible to override the doanything in any way
  476. // and it is not possible to switch to admin role either.
  477. if ($doanything) {
  478. if (is_siteadmin($userid)) {
  479. if ($userid != $USER->id) {
  480. return true;
  481. }
  482. // make sure switchrole is not used in this context
  483. if (empty($USER->access['rsw'])) {
  484. return true;
  485. }
  486. $parts = explode('/', trim($context->path, '/'));
  487. $path = '';
  488. $switched = false;
  489. foreach ($parts as $part) {
  490. $path .= '/' . $part;
  491. if (!empty($USER->access['rsw'][$path])) {
  492. $switched = true;
  493. break;
  494. }
  495. }
  496. if (!$switched) {
  497. return true;
  498. }
  499. //ok, admin switched role in this context, let's use normal access control rules
  500. }
  501. }
  502. // Careful check for staleness...
  503. $context->reload_if_dirty();
  504. if ($USER->id == $userid) {
  505. if (!isset($USER->access)) {
  506. load_all_capabilities();
  507. }
  508. $access =& $USER->access;
  509. } else {
  510. // make sure user accessdata is really loaded
  511. get_user_accessdata($userid, true);
  512. $access =& $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
  513. }
  514. return has_capability_in_accessdata($capability, $context, $access);
  515. }
  516. /**
  517. * Check if the user has any one of several capabilities from a list.
  518. *
  519. * This is just a utility method that calls has_capability in a loop. Try to put
  520. * the capabilities that most users are likely to have first in the list for best
  521. * performance.
  522. *
  523. * @category access
  524. * @see has_capability()
  525. *
  526. * @param array $capabilities an array of capability names.
  527. * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
  528. * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
  529. * @param boolean $doanything If false, ignore effect of admin role assignment
  530. * @return boolean true if the user has any of these capabilities. Otherwise false.
  531. */
  532. function has_any_capability(array $capabilities, context $context, $user = null, $doanything = true) {
  533. foreach ($capabilities as $capability) {
  534. if (has_capability($capability, $context, $user, $doanything)) {
  535. return true;
  536. }
  537. }
  538. return false;
  539. }
  540. /**
  541. * Check if the user has all the capabilities in a list.
  542. *
  543. * This is just a utility method that calls has_capability in a loop. Try to put
  544. * the capabilities that fewest users are likely to have first in the list for best
  545. * performance.
  546. *
  547. * @category access
  548. * @see has_capability()
  549. *
  550. * @param array $capabilities an array of capability names.
  551. * @param context $context the context to check the capability in. You normally get this with instance method of a context class.
  552. * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
  553. * @param boolean $doanything If false, ignore effect of admin role assignment
  554. * @return boolean true if the user has all of these capabilities. Otherwise false.
  555. */
  556. function has_all_capabilities(array $capabilities, context $context, $user = null, $doanything = true) {
  557. foreach ($capabilities as $capability) {
  558. if (!has_capability($capability, $context, $user, $doanything)) {
  559. return false;
  560. }
  561. }
  562. return true;
  563. }
  564. /**
  565. * Is course creator going to have capability in a new course?
  566. *
  567. * This is intended to be used in enrolment plugins before or during course creation,
  568. * do not use after the course is fully created.
  569. *
  570. * @category access
  571. *
  572. * @param string $capability the name of the capability to check.
  573. * @param context $context course or category context where is course going to be created
  574. * @param integer|stdClass $user A user id or object. By default (null) checks the permissions of the current user.
  575. * @return boolean true if the user will have this capability.
  576. *
  577. * @throws coding_exception if different type of context submitted
  578. */
  579. function guess_if_creator_will_have_course_capability($capability, context $context, $user = null) {
  580. global $CFG;
  581. if ($context->contextlevel != CONTEXT_COURSE and $context->contextlevel != CONTEXT_COURSECAT) {
  582. throw new coding_exception('Only course or course category context expected');
  583. }
  584. if (has_capability($capability, $context, $user)) {
  585. // User already has the capability, it could be only removed if CAP_PROHIBIT
  586. // was involved here, but we ignore that.
  587. return true;
  588. }
  589. if (!has_capability('moodle/course:create', $context, $user)) {
  590. return false;
  591. }
  592. if (!enrol_is_enabled('manual')) {
  593. return false;
  594. }
  595. if (empty($CFG->creatornewroleid)) {
  596. return false;
  597. }
  598. if ($context->contextlevel == CONTEXT_COURSE) {
  599. if (is_viewing($context, $user, 'moodle/role:assign') or is_enrolled($context, $user, 'moodle/role:assign')) {
  600. return false;
  601. }
  602. } else {
  603. if (has_capability('moodle/course:view', $context, $user) and has_capability('moodle/role:assign', $context, $user)) {
  604. return false;
  605. }
  606. }
  607. // Most likely they will be enrolled after the course creation is finished,
  608. // does the new role have the required capability?
  609. list($neededroles, $forbiddenroles) = get_roles_with_cap_in_context($context, $capability);
  610. return isset($neededroles[$CFG->creatornewroleid]);
  611. }
  612. /**
  613. * Check if the user is an admin at the site level.
  614. *
  615. * Please note that use of proper capabilities is always encouraged,
  616. * this function is supposed to be used from core or for temporary hacks.
  617. *
  618. * @category access
  619. *
  620. * @param int|stdClass $user_or_id user id or user object
  621. * @return bool true if user is one of the administrators, false otherwise
  622. */
  623. function is_siteadmin($user_or_id = null) {
  624. global $CFG, $USER;
  625. if ($user_or_id === null) {
  626. $user_or_id = $USER;
  627. }
  628. if (empty($user_or_id)) {
  629. return false;
  630. }
  631. if (!empty($user_or_id->id)) {
  632. $userid = $user_or_id->id;
  633. } else {
  634. $userid = $user_or_id;
  635. }
  636. // Because this script is called many times (150+ for course page) with
  637. // the same parameters, it is worth doing minor optimisations. This static
  638. // cache stores the value for a single userid, saving about 2ms from course
  639. // page load time without using significant memory. As the static cache
  640. // also includes the value it depends on, this cannot break unit tests.
  641. static $knownid, $knownresult, $knownsiteadmins;
  642. if ($knownid === $userid && $knownsiteadmins === $CFG->siteadmins) {
  643. return $knownresult;
  644. }
  645. $knownid = $userid;
  646. $knownsiteadmins = $CFG->siteadmins;
  647. $siteadmins = explode(',', $CFG->siteadmins);
  648. $knownresult = in_array($userid, $siteadmins);
  649. return $knownresult;
  650. }
  651. /**
  652. * Returns true if user has at least one role assign
  653. * of 'coursecontact' role (is potentially listed in some course descriptions).
  654. *
  655. * @param int $userid
  656. * @return bool
  657. */
  658. function has_coursecontact_role($userid) {
  659. global $DB, $CFG;
  660. if (empty($CFG->coursecontact)) {
  661. return false;
  662. }
  663. $sql = "SELECT 1
  664. FROM {role_assignments}
  665. WHERE userid = :userid AND roleid IN ($CFG->coursecontact)";
  666. return $DB->record_exists_sql($sql, array('userid'=>$userid));
  667. }
  668. /**
  669. * Does the user have a capability to do something?
  670. *
  671. * Walk the accessdata array and return true/false.
  672. * Deals with prohibits, role switching, aggregating
  673. * capabilities, etc.
  674. *
  675. * The main feature of here is being FAST and with no
  676. * side effects.
  677. *
  678. * Notes:
  679. *
  680. * Switch Role merges with default role
  681. * ------------------------------------
  682. * If you are a teacher in course X, you have at least
  683. * teacher-in-X + defaultloggedinuser-sitewide. So in the
  684. * course you'll have techer+defaultloggedinuser.
  685. * We try to mimic that in switchrole.
  686. *
  687. * Permission evaluation
  688. * ---------------------
  689. * Originally there was an extremely complicated way
  690. * to determine the user access that dealt with
  691. * "locality" or role assignments and role overrides.
  692. * Now we simply evaluate access for each role separately
  693. * and then verify if user has at least one role with allow
  694. * and at the same time no role with prohibit.
  695. *
  696. * @access private
  697. * @param string $capability
  698. * @param context $context
  699. * @param array $accessdata
  700. * @return bool
  701. */
  702. function has_capability_in_accessdata($capability, context $context, array &$accessdata) {
  703. global $CFG;
  704. // Build $paths as a list of current + all parent "paths" with order bottom-to-top
  705. $path = $context->path;
  706. $paths = array($path);
  707. while ($path = rtrim($path, '0123456789')) {
  708. $path = rtrim($path, '/');
  709. if ($path === '') {
  710. break;
  711. }
  712. $paths[] = $path;
  713. }
  714. $roles = array();
  715. $switchedrole = false;
  716. // Find out if role switched
  717. if (!empty($accessdata['rsw'])) {
  718. // From the bottom up...
  719. foreach ($paths as $path) {
  720. if (isset($accessdata['rsw'][$path])) {
  721. // Found a switchrole assignment - check for that role _plus_ the default user role
  722. $roles = array($accessdata['rsw'][$path]=>null, $CFG->defaultuserroleid=>null);
  723. $switchedrole = true;
  724. break;
  725. }
  726. }
  727. }
  728. if (!$switchedrole) {
  729. // get all users roles in this context and above
  730. foreach ($paths as $path) {
  731. if (isset($accessdata['ra'][$path])) {
  732. foreach ($accessdata['ra'][$path] as $roleid) {
  733. $roles[$roleid] = null;
  734. }
  735. }
  736. }
  737. }
  738. // Now find out what access is given to each role, going bottom-->up direction
  739. $rdefs = get_role_definitions(array_keys($roles));
  740. $allowed = false;
  741. foreach ($roles as $roleid => $ignored) {
  742. foreach ($paths as $path) {
  743. if (isset($rdefs[$roleid][$path][$capability])) {
  744. $perm = (int)$rdefs[$roleid][$path][$capability];
  745. if ($perm === CAP_PROHIBIT) {
  746. // any CAP_PROHIBIT found means no permission for the user
  747. return false;
  748. }
  749. if (is_null($roles[$roleid])) {
  750. $roles[$roleid] = $perm;
  751. }
  752. }
  753. }
  754. // CAP_ALLOW in any role means the user has a permission, we continue only to detect prohibits
  755. $allowed = ($allowed or $roles[$roleid] === CAP_ALLOW);
  756. }
  757. return $allowed;
  758. }
  759. /**
  760. * A convenience function that tests has_capability, and displays an error if
  761. * the user does not have that capability.
  762. *
  763. * NOTE before Moodle 2.0, this function attempted to make an appropriate
  764. * require_login call before checking the capability. This is no longer the case.
  765. * You must call require_login (or one of its variants) if you want to check the
  766. * user is logged in, before you call this function.
  767. *
  768. * @see has_capability()
  769. *
  770. * @param string $capability the name of the capability to check. For example mod/forum:view
  771. * @param context $context the context to check the capability in. You normally get this with context_xxxx::instance().
  772. * @param int $userid A user id. By default (null) checks the permissions of the current user.
  773. * @param bool $doanything If false, ignore effect of admin role assignment
  774. * @param string $errormessage The error string to to user. Defaults to 'nopermissions'.
  775. * @param string $stringfile The language file to load the error string from. Defaults to 'error'.
  776. * @return void terminates with an error if the user does not have the given capability.
  777. */
  778. function require_capability($capability, context $context, $userid = null, $doanything = true,
  779. $errormessage = 'nopermissions', $stringfile = '') {
  780. if (!has_capability($capability, $context, $userid, $doanything)) {
  781. throw new required_capability_exception($context, $capability, $errormessage, $stringfile);
  782. }
  783. }
  784. /**
  785. * Return a nested array showing all role assignments for the user.
  786. * [ra] => [contextpath][roleid] = roleid
  787. *
  788. * @access private
  789. * @param int $userid - the id of the user
  790. * @return array access info array
  791. */
  792. function get_user_roles_sitewide_accessdata($userid) {
  793. global $CFG, $DB;
  794. $accessdata = get_empty_accessdata();
  795. // start with the default role
  796. if (!empty($CFG->defaultuserroleid)) {
  797. $syscontext = context_system::instance();
  798. $accessdata['ra'][$syscontext->path][(int)$CFG->defaultuserroleid] = (int)$CFG->defaultuserroleid;
  799. }
  800. // load the "default frontpage role"
  801. if (!empty($CFG->defaultfrontpageroleid)) {
  802. $frontpagecontext = context_course::instance(get_site()->id);
  803. if ($frontpagecontext->path) {
  804. $accessdata['ra'][$frontpagecontext->path][(int)$CFG->defaultfrontpageroleid] = (int)$CFG->defaultfrontpageroleid;
  805. }
  806. }
  807. // Preload every assigned role.
  808. $sql = "SELECT ctx.path, ra.roleid, ra.contextid
  809. FROM {role_assignments} ra
  810. JOIN {context} ctx ON ctx.id = ra.contextid
  811. WHERE ra.userid = :userid";
  812. $rs = $DB->get_recordset_sql($sql, array('userid' => $userid));
  813. foreach ($rs as $ra) {
  814. // RAs leafs are arrays to support multi-role assignments...
  815. $accessdata['ra'][$ra->path][(int)$ra->roleid] = (int)$ra->roleid;
  816. }
  817. $rs->close();
  818. return $accessdata;
  819. }
  820. /**
  821. * Returns empty accessdata structure.
  822. *
  823. * @access private
  824. * @return array empt accessdata
  825. */
  826. function get_empty_accessdata() {
  827. $accessdata = array(); // named list
  828. $accessdata['ra'] = array();
  829. $accessdata['time'] = time();
  830. $accessdata['rsw'] = array();
  831. return $accessdata;
  832. }
  833. /**
  834. * Get accessdata for a given user.
  835. *
  836. * @access private
  837. * @param int $userid
  838. * @param bool $preloadonly true means do not return access array
  839. * @return array accessdata
  840. */
  841. function get_user_accessdata($userid, $preloadonly=false) {
  842. global $CFG, $ACCESSLIB_PRIVATE, $USER;
  843. if (isset($USER->access)) {
  844. $ACCESSLIB_PRIVATE->accessdatabyuser[$USER->id] = $USER->access;
  845. }
  846. if (!isset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid])) {
  847. if (empty($userid)) {
  848. if (!empty($CFG->notloggedinroleid)) {
  849. $accessdata = get_role_access($CFG->notloggedinroleid);
  850. } else {
  851. // weird
  852. return get_empty_accessdata();
  853. }
  854. } else if (isguestuser($userid)) {
  855. if ($guestrole = get_guest_role()) {
  856. $accessdata = get_role_access($guestrole->id);
  857. } else {
  858. //weird
  859. return get_empty_accessdata();
  860. }
  861. } else {
  862. // Includes default role and frontpage role.
  863. $accessdata = get_user_roles_sitewide_accessdata($userid);
  864. }
  865. $ACCESSLIB_PRIVATE->accessdatabyuser[$userid] = $accessdata;
  866. }
  867. if ($preloadonly) {
  868. return;
  869. } else {
  870. return $ACCESSLIB_PRIVATE->accessdatabyuser[$userid];
  871. }
  872. }
  873. /**
  874. * A convenience function to completely load all the capabilities
  875. * for the current user. It is called from has_capability() and functions change permissions.
  876. *
  877. * Call it only _after_ you've setup $USER and called check_enrolment_plugins();
  878. * @see check_enrolment_plugins()
  879. *
  880. * @access private
  881. * @return void
  882. */
  883. function load_all_capabilities() {
  884. global $USER;
  885. // roles not installed yet - we are in the middle of installation
  886. if (during_initial_install()) {
  887. return;
  888. }
  889. if (!isset($USER->id)) {
  890. // this should not happen
  891. $USER->id = 0;
  892. }
  893. unset($USER->access);
  894. $USER->access = get_user_accessdata($USER->id);
  895. // Clear to force a refresh
  896. unset($USER->mycourses);
  897. // init/reset internal enrol caches - active course enrolments and temp access
  898. $USER->enrol = array('enrolled'=>array(), 'tempguest'=>array());
  899. }
  900. /**
  901. * A convenience function to completely reload all the capabilities
  902. * for the current user when roles have been updated in a relevant
  903. * context -- but PRESERVING switchroles and loginas.
  904. * This function resets all accesslib and context caches.
  905. *
  906. * That is - completely transparent to the user.
  907. *
  908. * Note: reloads $USER->access completely.
  909. *
  910. * @access private
  911. * @return void
  912. */
  913. function reload_all_capabilities() {
  914. global $USER, $DB, $ACCESSLIB_PRIVATE;
  915. // copy switchroles
  916. $sw = array();
  917. if (!empty($USER->access['rsw'])) {
  918. $sw = $USER->access['rsw'];
  919. }
  920. accesslib_clear_all_caches(true);
  921. unset($USER->access);
  922. // Prevent dirty flags refetching on this page.
  923. $ACCESSLIB_PRIVATE->dirtycontexts = array();
  924. $ACCESSLIB_PRIVATE->dirtyusers = array($USER->id => false);
  925. load_all_capabilities();
  926. foreach ($sw as $path => $roleid) {
  927. if ($record = $DB->get_record('context', array('path'=>$path))) {
  928. $context = context::instance_by_id($record->id);
  929. if (has_capability('moodle/role:switchroles', $context)) {
  930. role_switch($roleid, $context);
  931. }
  932. }
  933. }
  934. }
  935. /**
  936. * Adds a temp role to current USER->access array.
  937. *
  938. * Useful for the "temporary guest" access we grant to logged-in users.
  939. * This is useful for enrol plugins only.
  940. *
  941. * @since Moodle 2.2
  942. * @param context_course $coursecontext
  943. * @param int $roleid
  944. * @return void
  945. */
  946. function load_temp_course_role(context_course $coursecontext, $roleid) {
  947. global $USER, $SITE;
  948. if (empty($roleid)) {
  949. debugging('invalid role specified in load_temp_course_role()');
  950. return;
  951. }
  952. if ($coursecontext->instanceid == $SITE->id) {
  953. debugging('Can not use temp roles on the frontpage');
  954. return;
  955. }
  956. if (!isset($USER->access)) {
  957. load_all_capabilities();
  958. }
  959. $coursecontext->reload_if_dirty();
  960. if (isset($USER->access['ra'][$coursecontext->path][$roleid])) {
  961. return;
  962. }
  963. $USER->access['ra'][$coursecontext->path][(int)$roleid] = (int)$roleid;
  964. }
  965. /**
  966. * Removes any extra guest roles from current USER->access array.
  967. * This is useful for enrol plugins only.
  968. *
  969. * @since Moodle 2.2
  970. * @param context_course $coursecontext
  971. * @return void
  972. */
  973. function remove_temp_course_roles(context_course $coursecontext) {
  974. global $DB, $USER, $SITE;
  975. if ($coursecontext->instanceid == $SITE->id) {
  976. debugging('Can not use temp roles on the frontpage');
  977. return;
  978. }
  979. if (empty($USER->access['ra'][$coursecontext->path])) {
  980. //no roles here, weird
  981. return;
  982. }
  983. $sql = "SELECT DISTINCT ra.roleid AS id
  984. FROM {role_assignments} ra
  985. WHERE ra.contextid = :contextid AND ra.userid = :userid";
  986. $ras = $DB->get_records_sql($sql, array('contextid'=>$coursecontext->id, 'userid'=>$USER->id));
  987. $USER->access['ra'][$coursecontext->path] = array();
  988. foreach ($ras as $r) {
  989. $USER->access['ra'][$coursecontext->path][(int)$r->id] = (int)$r->id;
  990. }
  991. }
  992. /**
  993. * Returns array of all role archetypes.
  994. *
  995. * @return array
  996. */
  997. function get_role_archetypes() {
  998. return array(
  999. 'manager' => 'manager',
  1000. 'coursecreator' => 'coursecreator',
  1001. 'editingteacher' => 'editingteacher',
  1002. 'teacher' => 'teacher',
  1003. 'student' => 'student',
  1004. 'guest' => 'guest',
  1005. 'user' => 'user',
  1006. 'frontpage' => 'frontpage'
  1007. );
  1008. }
  1009. /**
  1010. * Assign the defaults found in this capability definition to roles that have
  1011. * the corresponding legacy capabilities assigned to them.
  1012. *
  1013. * @param string $capability
  1014. * @param array $legacyperms an array in the format (example):
  1015. * 'guest' => CAP_PREVENT,
  1016. * 'student' => CAP_ALLOW,
  1017. * 'teacher' => CAP_ALLOW,
  1018. * 'editingteacher' => CAP_ALLOW,
  1019. * 'coursecreator' => CAP_ALLOW,
  1020. * 'manager' => CAP_ALLOW
  1021. * @return boolean success or failure.
  1022. */
  1023. function assign_legacy_capabilities($capability, $legacyperms) {
  1024. $archetypes = get_role_archetypes();
  1025. foreach ($legacyperms as $type => $perm) {
  1026. $systemcontext = context_system::instance();
  1027. if ($type === 'admin') {
  1028. debugging('Legacy type admin in access.php was renamed to manager, please update the code.');
  1029. $type = 'manager';
  1030. }
  1031. if (!array_key_exists($type, $archetypes)) {
  1032. print_error('invalidlegacy', '', '', $type);
  1033. }
  1034. if ($roles = get_archetype_roles($type)) {
  1035. foreach ($roles as $role) {
  1036. // Assign a site level capability.
  1037. if (!assign_capability($capability, $perm, $role->id, $systemcontext->id)) {
  1038. return false;
  1039. }
  1040. }
  1041. }
  1042. }
  1043. return true;
  1044. }
  1045. /**
  1046. * Verify capability risks.
  1047. *
  1048. * @param stdClass $capability a capability - a row from the capabilities table.
  1049. * @return boolean whether this capability is safe - that is, whether people with the
  1050. * safeoverrides capability should be allowed to change it.
  1051. */
  1052. function is_safe_capability($capability) {
  1053. return !((RISK_DATALOSS | RISK_MANAGETRUST | RISK_CONFIG | RISK_XSS | RISK_PERSONAL) & $capability->riskbitmask);
  1054. }
  1055. /**
  1056. * Get the local override (if any) for a given capability in a role in a context
  1057. *
  1058. * @param int $roleid
  1059. * @param int $contextid
  1060. * @param string $capability
  1061. * @return stdClass local capability override
  1062. */
  1063. function get_local_override($roleid, $contextid, $capability) {
  1064. global $DB;
  1065. return $DB->get_record_sql("
  1066. SELECT rc.*
  1067. FROM {role_capabilities} rc
  1068. JOIN {capability} cap ON rc.capability = cap.name
  1069. WHERE rc.roleid = :roleid AND rc.capability = :capability AND rc.contextid = :contextid", [
  1070. 'roleid' => $roleid,
  1071. 'contextid' => $contextid,
  1072. 'capability' => $capability,
  1073. ]);
  1074. }
  1075. /**
  1076. * Returns context instance plus related course and cm instances
  1077. *
  1078. * @param int $contextid
  1079. * @return array of ($context, $course, $cm)
  1080. */
  1081. function get_context_info_array($contextid) {
  1082. global $DB;
  1083. $context = context::instance_by_id($contextid, MUST_EXIST);
  1084. $course = null;
  1085. $cm = null;
  1086. if ($context->contextlevel == CONTEXT_COURSE) {
  1087. $course = $DB->get_record('course', array('id'=>$context->instanceid), '*', MUST_EXIST);
  1088. } else if ($context->contextlevel == CONTEXT_MODULE) {
  1089. $cm = get_coursemodule_from_id('', $context->instanceid, 0, false, MUST_EXIST);
  1090. $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
  1091. } else if ($context->contextlevel == CONTEXT_BLOCK) {
  1092. $parent = $context->get_parent_context();
  1093. if ($parent->contextlevel == CONTEXT_COURSE) {
  1094. $course = $DB->get_record('course', array('id'=>$parent->instanceid), '*', MUST_EXIST);
  1095. } else if ($parent->contextlevel == CONTEXT_MODULE) {
  1096. $cm = get_coursemodule_from_id('', $parent->instanceid, 0, false, MUST_EXIST);
  1097. $course = $DB->get_record('course', array('id'=>$cm->course), '*', MUST_EXIST);
  1098. }
  1099. }
  1100. return array($context, $course, $cm);
  1101. }
  1102. /**
  1103. * Function that creates a role
  1104. *
  1105. * @param string $name role name
  1106. * @param string $shortname role short name
  1107. * @param string $description role description
  1108. * @param string $archetype
  1109. * @return int id or dml_exception
  1110. */
  1111. function create_role($name, $shortname, $description, $archetype = '') {
  1112. global $DB;
  1113. if (strpos($archetype, 'moodle/legacy:') !== false) {
  1114. throw new coding_exception('Use new role archetype parameter in create_role() instead of old legacy capabilities.');
  1115. }
  1116. // verify role archetype actually exists
  1117. $archetypes = get_role_archetypes();
  1118. if (empty($archetypes[$archetype])) {
  1119. $archetype = '';
  1120. }
  1121. // Insert the role record.
  1122. $role = new stdClass();
  1123. $role->name = $name;
  1124. $role->shortname = $shortname;
  1125. $role->description = $description;
  1126. $role->archetype = $archetype;
  1127. //find free sortorder number
  1128. $role->sortorder = $DB->get_field('role', 'MAX(sortorder) + 1', array());
  1129. if (empty($role->sortorder)) {
  1130. $role->sortorder = 1;
  1131. }
  1132. $id = $DB->insert_record('role', $role);
  1133. return $id;
  1134. }
  1135. /**
  1136. * Function that deletes a role and cleanups up after it
  1137. *
  1138. * @param int $roleid id of role to delete
  1139. * @return bool always true
  1140. */
  1141. function delete_role($roleid) {
  1142. global $DB;
  1143. // first unssign all users
  1144. role_unassign_all(array('roleid'=>$roleid));
  1145. // cleanup all references to this role, ignore errors
  1146. $DB->delete_records('role_capabilities', array('roleid'=>$roleid));
  1147. $DB->delete_records('role_allow_assign', array('roleid'=>$roleid));
  1148. $DB->delete_records('role_allow_assign', array('allowassign'=>$roleid));
  1149. $DB->delete_records('role_allow_override', array('roleid'=>$roleid));
  1150. $DB->delete_records('role_allow_override', array('allowoverride'=>$roleid));
  1151. $DB->delete_records('role_names', array('roleid'=>$roleid));
  1152. $DB->delete_records('role_context_levels', array('roleid'=>$roleid));
  1153. // Get role record before it's deleted.
  1154. $role = $DB->get_record('role', array('id'=>$roleid));
  1155. // Finally delete the role itself.
  1156. $DB->delete_records('role', array('id'=>$roleid));
  1157. // Trigger event.
  1158. $event = \core\event\role_deleted::create(
  1159. array(
  1160. 'context' => context_system::instance(),
  1161. 'objectid' => $roleid,
  1162. 'other' =>
  1163. array(
  1164. 'shortname' => $role->shortname,
  1165. 'description' => $role->description,
  1166. 'archetype' => $role->archetype
  1167. )
  1168. )
  1169. );
  1170. $event->add_record_snapshot('role', $role);
  1171. $event->trigger();
  1172. // Reset any cache of this role, including MUC.
  1173. accesslib_clear_role_cache($roleid);
  1174. return true;
  1175. }
  1176. /**
  1177. * Function to write context specific overrides, or default capabilities.
  1178. *
  1179. * @param string $capability string name
  1180. * @param int $permission CAP_ constants
  1181. * @param int $roleid role id
  1182. * @param int|context $contextid context id
  1183. * @param bool $overwrite
  1184. * @return bool always true or exception
  1185. */
  1186. function assign_capability($capability, $permission, $roleid, $contextid, $overwrite = false) {
  1187. global $USER, $DB;
  1188. if ($contextid instanceof context) {
  1189. $context = $contextid;
  1190. } else {
  1191. $context = context::instance_by_id($contextid);
  1192. }
  1193. // Capability must exist.
  1194. if (!$capinfo = get_capability_info($capability)) {
  1195. throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
  1196. }
  1197. if (empty($permission) || $permission == CAP_INHERIT) { // if permission is not set
  1198. unassign_capability($capability, $roleid, $context->id);
  1199. return true;
  1200. }
  1201. $existing = $DB->get_record('role_capabilities', array('contextid'=>$context->id, 'roleid'=>$roleid, 'capability'=>$capability));
  1202. if ($existing and !$overwrite) { // We want to keep whatever is there already
  1203. return true;
  1204. }
  1205. $cap = new stdClass();
  1206. $cap->contextid = $context->id;
  1207. $cap->roleid = $roleid;
  1208. $cap->capability = $capability;
  1209. $cap->permission = $permission;
  1210. $cap->timemodified = time();
  1211. $cap->modifierid = empty($USER->id) ? 0 : $USER->id;
  1212. if ($existing) {
  1213. $cap->id = $existing->id;
  1214. $DB->update_record('role_capabilities', $cap);
  1215. } else {
  1216. if ($DB->record_exists('context', array('id'=>$context->id))) {
  1217. $DB->insert_record('role_capabilities', $cap);
  1218. }
  1219. }
  1220. // Trigger capability_assigned event.
  1221. \core\event\capability_assigned::create([
  1222. 'userid' => $cap->modifierid,
  1223. 'context' => $context,
  1224. 'objectid' => $roleid,
  1225. 'other' => [
  1226. 'capability' => $capability,
  1227. 'oldpermission' => $existing->permission ?? CAP_INHERIT,
  1228. 'permission' => $permission
  1229. ]
  1230. ])->trigger();
  1231. // Reset any cache of this role, including MUC.
  1232. accesslib_clear_role_cache($roleid);
  1233. return true;
  1234. }
  1235. /**
  1236. * Unassign a capability from a role.
  1237. *
  1238. * @param string $capability the name of the capability
  1239. * @param int $roleid the role id
  1240. * @param int|context $contextid null means all contexts
  1241. * @return boolean true or exception
  1242. */
  1243. function unassign_capability($capability, $roleid, $contextid = null) {
  1244. global $DB, $USER;
  1245. // Capability must exist.
  1246. if (!$capinfo = get_capability_info($capability)) {
  1247. throw new coding_exception("Capability '{$capability}' was not found! This has to be fixed in code.");
  1248. }
  1249. if (!empty($contextid)) {
  1250. if ($contextid instanceof context) {
  1251. $context = $contextid;
  1252. } else {
  1253. $context = context::instance_by_id($contextid);
  1254. }
  1255. // delete from context rel, if this is the last override in this context
  1256. $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid, 'contextid'=>$context->id));
  1257. } else {
  1258. $DB->delete_records('role_capabilities', array('capability'=>$capability, 'roleid'=>$roleid));
  1259. }
  1260. // Trigger capability_assigned event.
  1261. \core\event\capability_unassigned::create([
  1262. 'userid' => $USER->id,
  1263. 'context' => $context ?? context_system::instance(),
  1264. 'objectid' => $roleid,
  1265. 'other' => [
  1266. 'capability' => $capability,
  1267. ]
  1268. ])->trigger();
  1269. // Reset any cache of this role, including MUC.
  1270. accesslib_clear_role_cache($roleid);
  1271. return true;
  1272. }
  1273. /**
  1274. * Get the roles that have a given capability assigned to it
  1275. *
  1276. * This function does not resolve the actual permission of the capability.
  1277. * It just checks for permissions and overrides.
  1278. * Use get_roles_with_cap_in_context() if resolution is required.
  1279. *
  1280. * @param string $capability capability name (string)
  1281. * @param string $permission optional, the permission defined for this capability
  1282. * either CAP_ALLOW, CAP_PREVENT or CAP_PROHIBIT. Defaults to null which means any.
  1283. * @param stdClass $context null means any
  1284. * @return array of role records
  1285. */
  1286. function get_roles_with_capability($capability, $permission = null, $context = null) {
  1287. global $DB;
  1288. if ($context) {
  1289. $contexts = $context->get_parent_context_ids(true);
  1290. list($insql, $params) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED, 'ctx');
  1291. $contextsql = "AND rc.contextid $insql";
  1292. } else {
  1293. $params = array();
  1294. $contextsql = '';
  1295. }
  1296. if ($permission) {
  1297. $permissionsql = " AND rc.permission = :permission";
  1298. $params['permission'] = $permission;
  1299. } else {
  1300. $permissionsql = '';
  1301. }
  1302. $sql = "SELECT r.*
  1303. FROM {role} r
  1304. WHERE r.id IN (SELECT rc.roleid
  1305. FROM {role_capabilities} rc
  1306. JOIN {capabilities} cap ON rc.capability = cap.name
  1307. WHERE rc.capability = :capname
  1308. $contextsql
  1309. $permissionsql)";
  1310. $params['capname'] = $capability;
  1311. return $DB->get_records_sql($sql, $params);
  1312. }
  1313. /**
  1314. * This function makes a role-assignment (a role for a user in a particular context)
  1315. *
  1316. * @param int $roleid the role of the id
  1317. * @param int $userid userid
  1318. * @param int|context $contextid id of the context
  1319. * @param string $component example 'enrol_ldap', defaults to '' which means manual assignment,
  1320. * @param int $itemid id of enrolment/auth plugin
  1321. * @param string $timemodified defaults to current time
  1322. * @return int new/existing id of the assignment
  1323. */
  1324. function role_assign($roleid, $userid, $contextid, $component = '', $itemid = 0, $timemodified = '') {
  1325. global $USER, $DB;
  1326. // first of all detect if somebody is using old style parameters
  1327. if ($contextid === 0 or is_numeric($component)) {
  1328. throw new coding_exception('Invalid call to role_assign(), code needs to be updated to use new order of parameters');
  1329. }
  1330. // now validate all parameters
  1331. if (empty($roleid)) {
  1332. throw new coding_exception('Invalid call to role_assign(), roleid can not be empty');
  1333. }
  1334. if (empty($userid)) {
  1335. throw new coding_exception('Invalid call to role_assign(), userid can not be empty');
  1336. }
  1337. if ($itemid) {
  1338. if (strpos($component, '_') === false) {
  1339. throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as"enrol_" when itemid specified', 'component:'.$component);
  1340. }
  1341. } else {
  1342. $itemid = 0;
  1343. if ($component !== '' and strpos($component, '_') === false) {
  1344. throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
  1345. }
  1346. }
  1347. if (!$DB->record_exists('user', array('id'=>$userid, 'deleted'=>0))) {
  1348. throw new coding_exception('User ID does not exist or is deleted!', 'userid:'.$userid);
  1349. }
  1350. if ($contextid instanceof context) {
  1351. $context = $contextid;
  1352. } else {
  1353. $context = context::instance_by_id($contextid, MUST_EXIST);
  1354. }
  1355. if (!$timemodified) {
  1356. $timemodified = time();
  1357. }
  1358. // Check for existing entry
  1359. $ras = $DB->get_records('role_assignments', array('roleid'=>$roleid, 'contextid'=>$context->id, 'userid'=>$userid, 'component'=>$component, 'itemid'=>$itemid), 'id');
  1360. if ($ras) {
  1361. // role already assigned - this should not happen
  1362. if (count($ras) > 1) {
  1363. // very weird - remove all duplicates!
  1364. $ra = array_shift($ras);
  1365. foreach ($ras as $r) {
  1366. $DB->delete_records('role_assignments', array('id'=>$r->id));
  1367. }
  1368. } else {
  1369. $ra = reset($ras);
  1370. }
  1371. // actually there is no need to update, reset anything or trigger any event, so just return
  1372. return $ra->id;
  1373. }
  1374. // Create a new entry
  1375. $ra = new stdClass();
  1376. $ra->roleid = $roleid;
  1377. $ra->contextid = $context->id;
  1378. $ra->userid = $userid;
  1379. $ra->component = $component;
  1380. $ra->itemid = $itemid;
  1381. $ra->timemodified = $timemodified;
  1382. $ra->modifierid = empty($USER->id) ? 0 : $USER->id;
  1383. $ra->sortorder = 0;
  1384. $ra->id = $DB->insert_record('role_assignments', $ra);
  1385. // Role assignments have changed, so mark user as dirty.
  1386. mark_user_dirty($userid);
  1387. core_course_category::role_assignment_changed($roleid, $context);
  1388. $event = \core\event\role_assigned::create(array(
  1389. 'context' => $context,
  1390. 'objectid' => $ra->roleid,
  1391. 'relateduserid' => $ra->userid,
  1392. 'other' => array(
  1393. 'id' => $ra->id,
  1394. 'component' => $ra->component,
  1395. 'itemid' => $ra->itemid
  1396. )
  1397. ));
  1398. $event->add_record_snapshot('role_assignments', $ra);
  1399. $event->trigger();
  1400. return $ra->id;
  1401. }
  1402. /**
  1403. * Removes one role assignment
  1404. *
  1405. * @param int $roleid
  1406. * @param int $userid
  1407. * @param int $contextid
  1408. * @param string $component
  1409. * @param int $itemid
  1410. * @return void
  1411. */
  1412. function role_unassign($roleid, $userid, $contextid, $component = '', $itemid = 0) {
  1413. // first make sure the params make sense
  1414. if ($roleid == 0 or $userid == 0 or $contextid == 0) {
  1415. throw new coding_exception('Invalid call to role_unassign(), please use role_unassign_all() when removing multiple role assignments');
  1416. }
  1417. if ($itemid) {
  1418. if (strpos($component, '_') === false) {
  1419. throw new coding_exception('Invalid call to role_assign(), component must start with plugin type such as "enrol_" when itemid specified', 'component:'.$component);
  1420. }
  1421. } else {
  1422. $itemid = 0;
  1423. if ($component !== '' and strpos($component, '_') === false) {
  1424. throw new coding_exception('Invalid call to role_assign(), invalid component string', 'component:'.$component);
  1425. }
  1426. }
  1427. role_unassign_all(array('roleid'=>$roleid, 'userid'=>$userid, 'contextid'=>$contextid, 'component'=>$component, 'itemid'=>$itemid), false, false);
  1428. }
  1429. /**
  1430. * Removes multiple role assignments, parameters may contain:
  1431. * 'roleid', 'userid', 'contextid', 'component', 'enrolid'.
  1432. *
  1433. * @param array $params role assignment parameters
  1434. * @param bool $subcontexts unassign in subcontexts too
  1435. * @param bool $includemanual include manual role assignments too
  1436. * @return void
  1437. */
  1438. function role_unassign_all(array $params, $subcontexts = false, $includemanual = false) {
  1439. global $USER, $CFG, $DB;
  1440. if (!$params) {
  1441. throw new coding_exception('Missing parameters in role_unsassign_all() call');
  1442. }
  1443. $allowed = array('roleid', 'userid', 'contextid', 'component', 'itemid');
  1444. foreach ($params as $key=>$value) {
  1445. if (!in_array($key, $allowed)) {
  1446. throw new coding_exception('Unknown role_unsassign_all() parameter key', 'key:'.$key);
  1447. }
  1448. }
  1449. if (isset($params['component']) and $params['component'] !== '' and strpos($params['component'], '_') === false) {
  1450. throw new coding_exception('Invalid component paramter in role_unsassign_all() call', 'component:'.$params['component']);
  1451. }
  1452. if ($includemanual) {
  1453. if (!isset($params['component']) or $params['component'] === '') {
  1454. throw new coding_exception('include manual parameter requires component parameter in role_unsassign_all() call');
  1455. }
  1456. }
  1457. if ($subcontexts) {
  1458. if (empty($params['contextid'])) {
  1459. throw new coding_exception('subcontexts paramtere requires component parameter in role_unsassign_all() call');
  1460. }
  1461. }
  1462. $ras = $DB->get_records('role_assignments', $params);
  1463. foreach ($ras as $ra) {
  1464. $DB->delete_records('role_assignments', array('id'=>$ra->id));
  1465. if ($context = context::instance_by_id($ra->contextid, IGNORE_MISSING)) {
  1466. // Role assignments have changed, so mark user as dirty.
  1467. mark_user_dirty($ra->userid);
  1468. $event = \core\event\role_unassigned::create(array(
  1469. 'context' => $context,
  1470. 'objectid' => $ra->roleid,
  1471. 'relateduserid' => $ra->userid,
  1472. 'other' => array(
  1473. 'id' => $ra->id,
  1474. 'component' => $ra->component,
  1475. 'itemid' => $ra->itemid
  1476. )
  1477. ));
  1478. $event->add_record_snapshot('role_assignments', $ra);
  1479. $event->trigger();
  1480. core_course_category::role_assignment_changed($ra->roleid, $context);
  1481. }
  1482. }
  1483. unset($ras);
  1484. // process subcontexts
  1485. if ($subcontexts and $context = context::instance_by_id($params['contextid'], IGNORE_MISSING)) {
  1486. if ($params['contextid'] instanceof context) {
  1487. $context = $params['contextid'];
  1488. } else {
  1489. $context = context::instance_by_id($params['contextid'], IGNORE_MISSING);
  1490. }
  1491. if ($context) {
  1492. $contexts = $context->get_child_contexts();
  1493. $mparams = $params;
  1494. foreach ($contexts as $context) {
  1495. $mparams['contextid'] = $context->id;
  1496. $ras = $DB->get_records('role_assignments', $mparams);
  1497. foreach ($ras as $ra) {
  1498. $DB->delete_records('role_assignments', array('id'=>$ra->id));
  1499. // Role assignments have changed, so mark user as dirty.
  1500. mark_user_dirty($ra->userid);
  1501. $event = \core\event\role_unassigned::create(
  1502. array('context'=>$context, 'objectid'=>$ra->roleid, 'relateduserid'=>$ra->userid,
  1503. 'other'=>array('id'=>$ra->id, 'component'=>$ra->component, 'itemid'=>$ra->itemid)));
  1504. $event->add_record_snapshot('role_assignments', $ra);
  1505. $event->trigger();
  1506. core_course_category::role_assignment_changed($ra->roleid, $context);
  1507. }
  1508. }
  1509. }
  1510. }
  1511. // do this once more for all manual role assignments
  1512. if ($includemanual) {
  1513. $params['component'] = '';
  1514. role_unassign_all($params, $subcontexts, false);
  1515. }
  1516. }
  1517. /**
  1518. * Mark a user as dirty (with timestamp) so as to force reloading of the user session.
  1519. *
  1520. * @param int $userid
  1521. * @return void
  1522. */
  1523. function mark_user_dirty($userid) {
  1524. global $CFG, $ACCESSLIB_PRIVATE;
  1525. if (during_initial_install()) {
  1526. return;
  1527. }
  1528. // Throw exception if invalid userid is provided.
  1529. if (empty($userid)) {
  1530. throw new coding_exception('Invalid user parameter supplied for mark_user_dirty() function!');
  1531. }
  1532. // Set dirty flag in database, set dirty field locally, and clear local accessdata cache.
  1533. set_cache_flag('accesslib/dirtyusers', $userid, 1, time() + $CFG->sessiontimeout);
  1534. $ACCESSLIB_PRIVATE->dirtyusers[$userid] = 1;
  1535. unset($ACCESSLIB_PRIVATE->accessdatabyuser[$userid]);
  1536. }
  1537. /**
  1538. * Determines if a user is currently logged in
  1539. *
  1540. * @category access
  1541. *
  1542. * @return bool
  1543. */
  1544. function isloggedin() {
  1545. global $USER;
  1546. return (!empty($USER->id));
  1547. }
  1548. /**
  1549. * Determines if a user is logged in as real guest user with username 'guest'.
  1550. *
  1551. * @category access
  1552. *
  1553. * @param int|object $user mixed user object or id, $USER if not specified
  1554. * @return bool true if user is the real guest user, false if not logged in or other user
  1555. */
  1556. function isguestuser($user = null) {
  1557. global $USER, $DB, $CFG;
  1558. // make sure we have the user id cached in config table, because we are going to use it a lot
  1559. if (empty($CFG->siteguest)) {
  1560. if (!$guestid = $DB->get_field('user', 'id', array('username'=>'guest', 'mnethostid'=>$CFG->mnet_localhost_id))) {
  1561. // guest does not exist yet, weird
  1562. return false;
  1563. }
  1564. set_config('siteguest', $guestid);
  1565. }
  1566. if ($user === null) {
  1567. $user = $USER;
  1568. }
  1569. if ($user === null) {
  1570. // happens when setting the $USER
  1571. return false;
  1572. } else if (is_numeric($user)) {
  1573. return ($CFG->siteguest == $user);
  1574. } else if (is_object($user)) {
  1575. if (empty($user->id)) {
  1576. return false; // not logged in means is not be guest
  1577. } else {
  1578. return ($CFG->siteguest == $user->id);
  1579. }
  1580. } else {
  1581. throw new coding_exception('Invalid user parameter supplied for isguestuser() function!');
  1582. }
  1583. }
  1584. /**
  1585. * Does user have a (temporary or real) guest access to course?
  1586. *
  1587. * @category access
  1588. *
  1589. * @param context $context
  1590. * @param stdClass|int $user
  1591. * @return bool
  1592. */
  1593. function is_guest(context $context, $user = null) {
  1594. global $USER;
  1595. // first find the course context
  1596. $coursecontext = $context->get_course_context();
  1597. // make sure there is a real user specified
  1598. if ($user === null) {
  1599. $userid = isset($USER->id) ? $USER->id : 0;
  1600. } else {
  1601. $userid = is_object($user) ? $user->id : $user;
  1602. }
  1603. if (isguestuser($userid)) {
  1604. // can not inspect or be enrolled
  1605. return true;
  1606. }
  1607. if (has_capability('moodle/course:view', $coursecontext, $user)) {
  1608. // viewing users appear out of nowhere, they are neither guests nor participants
  1609. return false;
  1610. }
  1611. // consider only real active enrolments here
  1612. if (is_enrolled($coursecontext, $user, '', true)) {
  1613. return false;
  1614. }
  1615. return true;
  1616. }
  1617. /**
  1618. * Returns true if the user has moodle/course:view capability in the course,
  1619. * this is intended for admins, managers (aka small admins), inspectors, etc.
  1620. *
  1621. * @category access
  1622. *
  1623. * @param context $context
  1624. * @param int|stdClass $user if null $USER is used
  1625. * @param string $withcapability extra capability name
  1626. * @return bool
  1627. */
  1628. function is_viewing(context $context, $user = null, $withcapability = '') {
  1629. // first find the course context
  1630. $coursecontext = $context->get_course_context();
  1631. if (isguestuser($user)) {
  1632. // can not inspect
  1633. return false;
  1634. }
  1635. if (!has_capability('moodle/course:view', $coursecontext, $user)) {
  1636. // admins are allowed to inspect courses
  1637. return false;
  1638. }
  1639. if ($withcapability and !has_capability($withcapability, $context, $user)) {
  1640. // site admins always have the capability, but the enrolment above blocks
  1641. return false;
  1642. }
  1643. return true;
  1644. }
  1645. /**
  1646. * Returns true if the user is able to access the course.
  1647. *
  1648. * This function is in no way, shape, or form a substitute for require_login.
  1649. * It should only be used in circumstances where it is not possible to call require_login
  1650. * such as the navigation.
  1651. *
  1652. * This function checks many of the methods of access to a course such as the view
  1653. * capability, enrollments, and guest access. It also makes use of the cache
  1654. * generated by require_login for guest access.
  1655. *
  1656. * The flags within the $USER object that are used here should NEVER be used outside
  1657. * of this function can_access_course and require_login. Doing so WILL break future
  1658. * versions.
  1659. *
  1660. * @param stdClass $course record
  1661. * @param stdClass|int|null $user user record or id, current user if null
  1662. * @param string $withcapability Check for this capability as well.
  1663. * @param bool $onlyactive consider only active enrolments in enabled plugins and time restrictions
  1664. * @return boolean Returns true if the user is able to access the course
  1665. */
  1666. function can_access_course(stdClass $course, $user = null, $withcapability = '', $onlyactive = false) {
  1667. global $DB, $USER;
  1668. // this function originally accepted $coursecontext parameter
  1669. if ($course instanceof context) {
  1670. if ($course instanceof context_course) {
  1671. debugging('deprecated context parameter, please use $course record');
  1672. $coursecontext = $course;
  1673. $course = $DB->get_record('course', array('id'=>$coursecontext->instanceid));
  1674. } else {
  1675. debugging('Invalid context parameter, please use $course record');
  1676. return false;
  1677. }
  1678. } else {
  1679. $coursecontext = context_course::instance($course->id);
  1680. }
  1681. if (!isset($USER->id)) {
  1682. // should never happen
  1683. $USER->id = 0;
  1684. debugging('Course access check being performed on a user with no ID.', DEBUG_DEVELOPER);
  1685. }
  1686. // make sure there is a user specified
  1687. if ($user === null) {
  1688. $userid = $USER->id;
  1689. } else {
  1690. $userid = is_object($user) ? $user->id : $user;
  1691. }
  1692. unset($user);
  1693. if ($withcapability and !has_capability($withcapability, $coursecontext, $userid)) {
  1694. return false;
  1695. }
  1696. if ($userid == $USER->id) {
  1697. if (!empty($USER->access['rsw'][$coursecontext->path])) {
  1698. // the fact that somebody switched role means they can access the course no matter to what role they switched
  1699. return true;
  1700. }
  1701. }
  1702. if (!$course->visible and !has_capability('moodle/course:viewhiddencourses', $coursecontext, $userid)) {
  1703. return false;
  1704. }
  1705. if (is_viewing($coursecontext, $userid)) {
  1706. return true;
  1707. }
  1708. if ($userid != $USER->id) {
  1709. // for performance reasons we do not verify temporary guest access for other users, sorry...
  1710. return is_enrolled($coursecontext, $userid, '', $onlyactive);
  1711. }
  1712. // === from here we deal only with $USER ===
  1713. $coursecontext->reload_if_dirty();
  1714. if (isset($USER->enrol['enrolled'][$course->id])) {
  1715. if ($USER->enrol['enrolled'][$course->id] > time()) {
  1716. return true;
  1717. }
  1718. }
  1719. if (isset($USER->enrol['tempguest'][$course->id])) {
  1720. if ($USER->enrol['tempguest'][$course->id] > time()) {
  1721. return true;
  1722. }
  1723. }
  1724. if (is_enrolled($coursecontext, $USER, '', $onlyactive)) {
  1725. return true;
  1726. }
  1727. if (!core_course_category::can_view_course_info($course)) {
  1728. // No guest access if user does not have capability to browse courses.
  1729. return false;
  1730. }
  1731. // if not enrolled try to gain temporary guest access
  1732. $instances = $DB->get_records('enrol', array('courseid'=>$course->id, 'status'=>ENROL_INSTANCE_ENABLED), 'sortorder, id ASC');
  1733. $enrols = enrol_get_plugins(true);
  1734. foreach ($instances as $instance) {
  1735. if (!isset($enrols[$instance->enrol])) {
  1736. continue;
  1737. }
  1738. // Get a duration for the guest access, a timestamp in the future, 0 (always) or false.
  1739. $until = $enrols[$instance->enrol]->try_guestaccess($instance);
  1740. if ($until !== false and $until > time()) {
  1741. $USER->enrol['tempguest'][$course->id] = $until;
  1742. return true;
  1743. }
  1744. }
  1745. if (isset($USER->enrol['tempguest'][$course->id])) {
  1746. unset($USER->enrol['tempguest'][$course->id]);
  1747. remove_temp_course_roles($coursecontext);
  1748. }
  1749. return false;
  1750. }
  1751. /**
  1752. * Loads the capability definitions for the component (from file).
  1753. *
  1754. * Loads the capability definitions for the component (from file). If no
  1755. * capabilities are defined for the component, we simply return an empty array.
  1756. *
  1757. * @access private
  1758. * @param string $component full plugin name, examples: 'moodle', 'mod_forum'
  1759. * @return array array of capabilities
  1760. */
  1761. function load_capability_def($component) {
  1762. $defpath = core_component::get_component_directory($component).'/db/access.php';
  1763. $capabilities = array();
  1764. if (file_exists($defpath)) {
  1765. require($defpath);
  1766. if (!empty(${$component.'_capabilities'})) {
  1767. // BC capability array name
  1768. // since 2.0 we prefer $capabilities instead - it is easier to use and matches db/* files
  1769. debugging('componentname_capabilities array is deprecated, please use $capabilities array only in access.php files');
  1770. $capabilities = ${$component.'_capabilities'};
  1771. }
  1772. }
  1773. return $capabilities;
  1774. }
  1775. /**
  1776. * Gets the capabilities that have been cached in the database for this component.
  1777. *
  1778. * @access private
  1779. * @param string $component - examples: 'moodle', 'mod_forum'
  1780. * @return array array of capabilities
  1781. */
  1782. function get_cached_capabilities($component = 'moodle') {
  1783. global $DB;
  1784. $caps = get_all_capabilities();
  1785. $componentcaps = array();
  1786. foreach ($caps as $cap) {
  1787. if ($cap['component'] == $component) {
  1788. $componentcaps[] = (object) $cap;
  1789. }
  1790. }
  1791. return $componentcaps;
  1792. }
  1793. /**
  1794. * Returns default capabilities for given role archetype.
  1795. *
  1796. * @param string $archetype role archetype
  1797. * @return array
  1798. */
  1799. function get_default_capabilities($archetype) {
  1800. global $DB;
  1801. if (!$archetype) {
  1802. return array();
  1803. }
  1804. $alldefs = array();
  1805. $defaults = array();
  1806. $components = array();
  1807. $allcaps = get_all_capabilities();
  1808. foreach ($allcaps as $cap) {
  1809. if (!in_array($cap['component'], $components)) {
  1810. $components[] = $cap['component'];
  1811. $alldefs = array_merge($alldefs, load_capability_def($cap['component']));
  1812. }
  1813. }
  1814. foreach ($alldefs as $name=>$def) {
  1815. // Use array 'archetypes if available. Only if not specified, use 'legacy'.
  1816. if (isset($def['archetypes'])) {
  1817. if (isset($def['archetypes'][$archetype])) {
  1818. $defaults[$name] = $def['archetypes'][$archetype];
  1819. }
  1820. // 'legacy' is for backward compatibility with 1.9 access.php
  1821. } else {
  1822. if (isset($def['legacy'][$archetype])) {
  1823. $defaults[$name] = $def['legacy'][$archetype];
  1824. }
  1825. }
  1826. }
  1827. return $defaults;
  1828. }
  1829. /**
  1830. * Return default roles that can be assigned, overridden or switched
  1831. * by give role archetype.
  1832. *
  1833. * @param string $type assign|override|switch|view
  1834. * @param string $archetype
  1835. * @return array of role ids
  1836. */
  1837. function get_default_role_archetype_allows($type, $archetype) {
  1838. global $DB;
  1839. if (empty($archetype)) {
  1840. return array();
  1841. }
  1842. $roles = $DB->get_records('role');
  1843. $archetypemap = array();
  1844. foreach ($roles as $role) {
  1845. if ($role->archetype) {
  1846. $archetypemap[$role->archetype][$role->id] = $role->id;
  1847. }
  1848. }
  1849. $defaults = array(
  1850. 'assign' => array(
  1851. 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student'),
  1852. 'coursecreator' => array(),
  1853. 'editingteacher' => array('teacher', 'student'),
  1854. 'teacher' => array(),
  1855. 'student' => array(),
  1856. 'guest' => array(),
  1857. 'user' => array(),
  1858. 'frontpage' => array(),
  1859. ),
  1860. 'override' => array(
  1861. 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
  1862. 'coursecreator' => array(),
  1863. 'editingteacher' => array('teacher', 'student', 'guest'),
  1864. 'teacher' => array(),
  1865. 'student' => array(),
  1866. 'guest' => array(),
  1867. 'user' => array(),
  1868. 'frontpage' => array(),
  1869. ),
  1870. 'switch' => array(
  1871. 'manager' => array('editingteacher', 'teacher', 'student', 'guest'),
  1872. 'coursecreator' => array(),
  1873. 'editingteacher' => array('teacher', 'student', 'guest'),
  1874. 'teacher' => array('student', 'guest'),
  1875. 'student' => array(),
  1876. 'guest' => array(),
  1877. 'user' => array(),
  1878. 'frontpage' => array(),
  1879. ),
  1880. 'view' => array(
  1881. 'manager' => array('manager', 'coursecreator', 'editingteacher', 'teacher', 'student', 'guest', 'user', 'frontpage'),
  1882. 'coursecreator' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
  1883. 'editingteacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
  1884. 'teacher' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
  1885. 'student' => array('coursecreator', 'editingteacher', 'teacher', 'student'),
  1886. 'guest' => array(),
  1887. 'user' => array(),
  1888. 'frontpage' => array(),
  1889. ),
  1890. );
  1891. if (!isset($defaults[$type][$archetype])) {
  1892. debugging("Unknown type '$type'' or archetype '$archetype''");
  1893. return array();
  1894. }
  1895. $return = array();
  1896. foreach ($defaults[$type][$archetype] as $at) {
  1897. if (isset($archetypemap[$at])) {
  1898. foreach ($archetypemap[$at] as $roleid) {
  1899. $return[$roleid] = $roleid;
  1900. }
  1901. }
  1902. }
  1903. return $return;
  1904. }
  1905. /**
  1906. * Reset role capabilities to default according to selected role archetype.
  1907. * If no archetype selected, removes all capabilities.
  1908. *
  1909. * This applies to capabilities that are assigned to the role (that you could
  1910. * edit in the 'define roles' interface), and not to any capability overrides
  1911. * in different locations.
  1912. *
  1913. * @param int $roleid ID of role to reset capabilities for
  1914. */
  1915. function reset_role_capabilities($roleid) {
  1916. global $DB;
  1917. $role = $DB->get_record('role', array('id'=>$roleid), '*', MUST_EXIST);
  1918. $defaultcaps = get_default_capabilities($role->archetype);
  1919. $systemcontext = context_system::instance();
  1920. $DB->delete_records('role_capabilities',
  1921. array('roleid' => $roleid, 'contextid' => $systemcontext->id));
  1922. foreach ($defaultcaps as $cap=>$permission) {
  1923. assign_capability($cap, $permission, $roleid, $systemcontext->id);
  1924. }
  1925. // Reset any cache of this role, including MUC.
  1926. accesslib_clear_role_cache($roleid);
  1927. }
  1928. /**
  1929. * Updates the capabilities table with the component capability definitions.
  1930. * If no parameters are given, the function updates the core moodle
  1931. * capabilities.
  1932. *
  1933. * Note that the absence of the db/access.php capabilities definition file
  1934. * will cause any stored capabilities for the component to be removed from
  1935. * the database.
  1936. *
  1937. * @access private
  1938. * @param string $component examples: 'moodle', 'mod/forum', 'block/quiz_results'
  1939. * @return boolean true if success, exception in case of any problems
  1940. */
  1941. function update_capabilities($component = 'moodle') {
  1942. global $DB, $OUTPUT;
  1943. $storedcaps = array();
  1944. $filecaps = load_capability_def($component);
  1945. foreach ($filecaps as $capname=>$unused) {
  1946. if (!preg_match('|^[a-z]+/[a-z_0-9]+:[a-z_0-9]+$|', $capname)) {
  1947. debugging("Coding problem: Invalid capability name '$capname', use 'clonepermissionsfrom' field for migration.");
  1948. }
  1949. }
  1950. // It is possible somebody directly modified the DB (according to accesslib_test anyway).
  1951. // So ensure our updating is based on fresh data.
  1952. cache::make('core', 'capabilities')->delete('core_capabilities');
  1953. $cachedcaps = get_cached_capabilities($component);
  1954. if ($cachedcaps) {
  1955. foreach ($cachedcaps as $cachedcap) {
  1956. array_push($storedcaps, $cachedcap->name);
  1957. // update risk bitmasks and context levels in existing capabilities if needed
  1958. if (array_key_exists($cachedcap->name, $filecaps)) {
  1959. if (!array_key_exists('riskbitmask', $filecaps[$cachedcap->name])) {
  1960. $filecaps[$cachedcap->name]['riskbitmask'] = 0; // no risk if not specified
  1961. }
  1962. if ($cachedcap->captype != $filecaps[$cachedcap->name]['captype']) {
  1963. $updatecap = new stdClass();
  1964. $updatecap->id = $cachedcap->id;
  1965. $updatecap->captype = $filecaps[$cachedcap->name]['captype'];
  1966. $DB->update_record('capabilities', $updatecap);
  1967. }
  1968. if ($cachedcap->riskbitmask != $filecaps[$cachedcap->name]['riskbitmask']) {
  1969. $updatecap = new stdClass();
  1970. $updatecap->id = $cachedcap->id;
  1971. $updatecap->riskbitmask = $filecaps[$cachedcap->name]['riskbitmask'];
  1972. $DB->update_record('capabilities', $updatecap);
  1973. }
  1974. if (!array_key_exists('contextlevel', $filecaps[$cachedcap->name])) {
  1975. $filecaps[$cachedcap->name]['contextlevel'] = 0; // no context level defined
  1976. }
  1977. if ($cachedcap->contextlevel != $filecaps[$cachedcap->name]['contextlevel']) {
  1978. $updatecap = new stdClass();
  1979. $updatecap->id = $cachedcap->id;
  1980. $updatecap->contextlevel = $filecaps[$cachedcap->name]['contextlevel'];
  1981. $DB->update_record('capabilities', $updatecap);
  1982. }
  1983. }
  1984. }
  1985. }
  1986. // Flush the cached again, as we have changed DB.
  1987. cache::make('core', 'capabilities')->delete('core_capabilities');
  1988. // Are there new capabilities in the file definition?
  1989. $newcaps = array();
  1990. foreach ($filecaps as $filecap => $def) {
  1991. if (!$storedcaps ||
  1992. ($storedcaps && in_array($filecap, $storedcaps) === false)) {
  1993. if (!array_key_exists('riskbitmask', $def)) {
  1994. $def['riskbitmask'] = 0; // no risk if not specified
  1995. }
  1996. $newcaps[$filecap] = $def;
  1997. }
  1998. }
  1999. // Add new capabilities to the stored definition.
  2000. $existingcaps = $DB->get_records_menu('capabilities', array(), 'id', 'id, name');
  2001. foreach ($newcaps as $capname => $capdef) {
  2002. $capability = new stdClass();
  2003. $capability->name = $capname;
  2004. $capability->captype = $capdef['captype'];
  2005. $capability->contextlevel = $capdef['contextlevel'];
  2006. $capability->component = $component;
  2007. $capability->riskbitmask = $capdef['riskbitmask'];
  2008. $DB->insert_record('capabilities', $capability, false);
  2009. // Flush the cached, as we have changed DB.
  2010. cache::make('core', 'capabilities')->delete('core_capabilities');
  2011. if (isset($capdef['clonepermissionsfrom']) && in_array($capdef['clonepermissionsfrom'], $existingcaps)){
  2012. if ($rolecapabilities = $DB->get_records('role_capabilities', array('capability'=>$capdef['clonepermissionsfrom']))){
  2013. foreach ($rolecapabilities as $rolecapability){
  2014. //assign_capability will update rather than insert if capability exists
  2015. if (!assign_capability($capname, $rolecapability->permission,
  2016. $rolecapability->roleid, $rolecapability->contextid, true)){
  2017. echo $OUTPUT->notification('Could not clone capabilities for '.$capname);
  2018. }
  2019. }
  2020. }
  2021. // we ignore archetype key if we have cloned permissions
  2022. } else if (isset($capdef['archetypes']) && is_array($capdef['archetypes'])) {
  2023. assign_legacy_capabilities($capname, $capdef['archetypes']);
  2024. // 'legacy' is for backward compatibility with 1.9 access.php
  2025. } else if (isset($capdef['legacy']) && is_array($capdef['legacy'])) {
  2026. assign_legacy_capabilities($capname, $capdef['legacy']);
  2027. }
  2028. }
  2029. // Are there any capabilities that have been removed from the file
  2030. // definition that we need to delete from the stored capabilities and
  2031. // role assignments?
  2032. capabilities_cleanup($component, $filecaps);
  2033. // reset static caches
  2034. accesslib_reset_role_cache();
  2035. // Flush the cached again, as we have changed DB.
  2036. cache::make('core', 'capabilities')->delete('core_capabilities');
  2037. return true;
  2038. }
  2039. /**
  2040. * Deletes cached capabilities that are no longer needed by the component.
  2041. * Also unassigns these capabilities from any roles that have them.
  2042. * NOTE: this function is called from lib/db/upgrade.php
  2043. *
  2044. * @access private
  2045. * @param string $component examples: 'moodle', 'mod_forum', 'block_quiz_results'
  2046. * @param array $newcapdef array of the new capability definitions that will be
  2047. * compared with the cached capabilities
  2048. * @return int number of deprecated capabilities that have been removed
  2049. */
  2050. function capabilities_cleanup($component, $newcapdef = null) {
  2051. global $DB;
  2052. $removedcount = 0;
  2053. if ($cachedcaps = get_cached_capabilities($component)) {
  2054. foreach ($cachedcaps as $cachedcap) {
  2055. if (empty($newcapdef) ||
  2056. array_key_exists($cachedcap->name, $newcapdef) === false) {
  2057. // Delete from roles.
  2058. if ($roles = get_roles_with_capability($cachedcap->name)) {
  2059. foreach ($roles as $role) {
  2060. if (!unassign_capability($cachedcap->name, $role->id)) {
  2061. print_error('cannotunassigncap', 'error', '', (object)array('cap'=>$cachedcap->name, 'role'=>$role->name));
  2062. }
  2063. }
  2064. }
  2065. // Remove from role_capabilities for any old ones.
  2066. $DB->delete_records('role_capabilities', array('capability' => $cachedcap->name));
  2067. // Remove from capabilities cache.
  2068. $DB->delete_records('capabilities', array('name' => $cachedcap->name));
  2069. $removedcount++;
  2070. } // End if.
  2071. }
  2072. }
  2073. if ($removedcount) {
  2074. cache::make('core', 'capabilities')->delete('core_capabilities');
  2075. }
  2076. return $removedcount;
  2077. }
  2078. /**
  2079. * Returns an array of all the known types of risk
  2080. * The array keys can be used, for example as CSS class names, or in calls to
  2081. * print_risk_icon. The values are the corresponding RISK_ constants.
  2082. *
  2083. * @return array all the known types of risk.
  2084. */
  2085. function get_all_risks() {
  2086. return array(
  2087. 'riskmanagetrust' => RISK_MANAGETRUST,
  2088. 'riskconfig' => RISK_CONFIG,
  2089. 'riskxss' => RISK_XSS,
  2090. 'riskpersonal' => RISK_PERSONAL,
  2091. 'riskspam' => RISK_SPAM,
  2092. 'riskdataloss' => RISK_DATALOSS,
  2093. );
  2094. }
  2095. /**
  2096. * Return a link to moodle docs for a given capability name
  2097. *
  2098. * @param stdClass $capability a capability - a row from the mdl_capabilities table.
  2099. * @return string the human-readable capability name as a link to Moodle Docs.
  2100. */
  2101. function get_capability_docs_link($capability) {
  2102. $url = get_docs_url('Capabilities/' . $capability->name);
  2103. return '<a onclick="this.target=\'docspopup\'" href="' . $url . '">' . get_capability_string($capability->name) . '</a>';
  2104. }
  2105. /**
  2106. * This function pulls out all the resolved capabilities (overrides and
  2107. * defaults) of a role used in capability overrides in contexts at a given
  2108. * context.
  2109. *
  2110. * @param int $roleid
  2111. * @param context $context
  2112. * @param string $cap capability, optional, defaults to ''
  2113. * @return array Array of capabilities
  2114. */
  2115. function role_context_capabilities($roleid, context $context, $cap = '') {
  2116. global $DB;
  2117. $contexts = $context->get_parent_context_ids(true);
  2118. $contexts = '('.implode(',', $contexts).')';
  2119. $params = array($roleid);
  2120. if ($cap) {
  2121. $search = " AND rc.capability = ? ";
  2122. $params[] = $cap;
  2123. } else {
  2124. $search = '';
  2125. }
  2126. $sql = "SELECT rc.*
  2127. FROM {role_capabilities} rc
  2128. JOIN {context} c ON rc.contextid = c.id
  2129. JOIN {capabilities} cap ON rc.capability = cap.name
  2130. WHERE rc.contextid in $contexts
  2131. AND rc.roleid = ?
  2132. $search
  2133. ORDER BY c.contextlevel DESC, rc.capability DESC";
  2134. $capabilities = array();
  2135. if ($records = $DB->get_records_sql($sql, $params)) {
  2136. // We are traversing via reverse order.
  2137. foreach ($records as $record) {
  2138. // If not set yet (i.e. inherit or not set at all), or currently we have a prohibit
  2139. if (!isset($capabilities[$record->capability]) || $record->permission<-500) {
  2140. $capabilities[$record->capability] = $record->permission;
  2141. }
  2142. }
  2143. }
  2144. return $capabilities;
  2145. }
  2146. /**
  2147. * Constructs array with contextids as first parameter and context paths,
  2148. * in both cases bottom top including self.
  2149. *
  2150. * @access private
  2151. * @param context $context
  2152. * @return array
  2153. */
  2154. function get_context_info_list(context $context) {
  2155. $contextids = explode('/', ltrim($context->path, '/'));
  2156. $contextpaths = array();
  2157. $contextids2 = $contextids;
  2158. while ($contextids2) {
  2159. $contextpaths[] = '/' . implode('/', $contextids2);
  2160. array_pop($contextids2);
  2161. }
  2162. return array($contextids, $contextpaths);
  2163. }
  2164. /**
  2165. * Check if context is the front page context or a context inside it
  2166. *
  2167. * Returns true if this context is the front page context, or a context inside it,
  2168. * otherwise false.
  2169. *
  2170. * @param context $context a context object.
  2171. * @return bool
  2172. */
  2173. function is_inside_frontpage(context $context) {
  2174. $frontpagecontext = context_course::instance(SITEID);
  2175. return strpos($context->path . '/', $frontpagecontext->path . '/') === 0;
  2176. }
  2177. /**
  2178. * Returns capability information (cached)
  2179. *
  2180. * @param string $capabilityname
  2181. * @return stdClass or null if capability not found
  2182. */
  2183. function get_capability_info($capabilityname) {
  2184. $caps = get_all_capabilities();
  2185. if (!isset($caps[$capabilityname])) {
  2186. return null;
  2187. }
  2188. return (object) $caps[$capabilityname];
  2189. }
  2190. /**
  2191. * Returns all capabilitiy records, preferably from MUC and not database.
  2192. *
  2193. * @return array All capability records indexed by capability name
  2194. */
  2195. function get_all_capabilities() {
  2196. global $DB;
  2197. $cache = cache::make('core', 'capabilities');
  2198. if (!$allcaps = $cache->get('core_capabilities')) {
  2199. $rs = $DB->get_recordset('capabilities');
  2200. $allcaps = array();
  2201. foreach ($rs as $capability) {
  2202. $capability->riskbitmask = (int) $capability->riskbitmask;
  2203. $allcaps[$capability->name] = (array) $capability;
  2204. }
  2205. $rs->close();
  2206. $cache->set('core_capabilities', $allcaps);
  2207. }
  2208. return $allcaps;
  2209. }
  2210. /**
  2211. * Returns the human-readable, translated version of the capability.
  2212. * Basically a big switch statement.
  2213. *
  2214. * @param string $capabilityname e.g. mod/choice:readresponses
  2215. * @return string
  2216. */
  2217. function get_capability_string($capabilityname) {
  2218. // Typical capability name is 'plugintype/pluginname:capabilityname'
  2219. list($type, $name, $capname) = preg_split('|[/:]|', $capabilityname);
  2220. if ($type === 'moodle') {
  2221. $component = 'core_role';
  2222. } else if ($type === 'quizreport') {
  2223. //ugly hack!!
  2224. $component = 'quiz_'.$name;
  2225. } else {
  2226. $component = $type.'_'.$name;
  2227. }
  2228. $stringname = $name.':'.$capname;
  2229. if ($component === 'core_role' or get_string_manager()->string_exists($stringname, $component)) {
  2230. return get_string($stringname, $component);
  2231. }
  2232. $dir = core_component::get_component_directory($component);
  2233. if (!file_exists($dir)) {
  2234. // plugin broken or does not exist, do not bother with printing of debug message
  2235. return $capabilityname.' ???';
  2236. }
  2237. // something is wrong in plugin, better print debug
  2238. return get_string($stringname, $component);
  2239. }
  2240. /**
  2241. * This gets the mod/block/course/core etc strings.
  2242. *
  2243. * @param string $component
  2244. * @param int $contextlevel
  2245. * @return string|bool String is success, false if failed
  2246. */
  2247. function get_component_string($component, $contextlevel) {
  2248. if ($component === 'moodle' || $component === 'core') {
  2249. return context_helper::get_level_name($contextlevel);
  2250. }
  2251. list($type, $name) = core_component::normalize_component($component);
  2252. $dir = core_component::get_plugin_directory($type, $name);
  2253. if (!file_exists($dir)) {
  2254. // plugin not installed, bad luck, there is no way to find the name
  2255. return $component . ' ???';
  2256. }
  2257. // Some plugin types need an extra prefix to make the name easy to understand.
  2258. switch ($type) {
  2259. case 'quiz':
  2260. $prefix = get_string('quizreport', 'quiz') . ': ';
  2261. break;
  2262. case 'repository':
  2263. $prefix = get_string('repository', 'repository') . ': ';
  2264. break;
  2265. case 'gradeimport':
  2266. $prefix = get_string('gradeimport', 'grades') . ': ';
  2267. break;
  2268. case 'gradeexport':
  2269. $prefix = get_string('gradeexport', 'grades') . ': ';
  2270. break;
  2271. case 'gradereport':
  2272. $prefix = get_string('gradereport', 'grades') . ': ';
  2273. break;
  2274. case 'webservice':
  2275. $prefix = get_string('webservice', 'webservice') . ': ';
  2276. break;
  2277. case 'block':
  2278. $prefix = get_string('block') . ': ';
  2279. break;
  2280. case 'mod':
  2281. $prefix = get_string('activity') . ': ';
  2282. break;
  2283. // Default case, just use the plugin name.
  2284. default:
  2285. $prefix = '';
  2286. }
  2287. return $prefix . get_string('pluginname', $component);
  2288. }
  2289. /**
  2290. * Gets the list of roles assigned to this context and up (parents)
  2291. * from the aggregation of:
  2292. * a) the list of roles that are visible on user profile page and participants page (profileroles setting) and;
  2293. * b) if applicable, those roles that are assigned in the context.
  2294. *
  2295. * @param context $context
  2296. * @return array
  2297. */
  2298. function get_profile_roles(context $context) {
  2299. global $CFG, $DB;
  2300. // If the current user can assign roles, then they can see all roles on the profile and participants page,
  2301. // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
  2302. if (has_capability('moodle/role:assign', $context)) {
  2303. $rolesinscope = array_keys(get_all_roles($context));
  2304. } else {
  2305. $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
  2306. }
  2307. if (empty($rolesinscope)) {
  2308. return [];
  2309. }
  2310. list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
  2311. list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
  2312. $params = array_merge($params, $cparams);
  2313. if ($coursecontext = $context->get_course_context(false)) {
  2314. $params['coursecontext'] = $coursecontext->id;
  2315. } else {
  2316. $params['coursecontext'] = 0;
  2317. }
  2318. $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
  2319. FROM {role_assignments} ra, {role} r
  2320. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2321. WHERE r.id = ra.roleid
  2322. AND ra.contextid $contextlist
  2323. AND r.id $rallowed
  2324. ORDER BY r.sortorder ASC";
  2325. return $DB->get_records_sql($sql, $params);
  2326. }
  2327. /**
  2328. * Gets the list of roles assigned to this context and up (parents)
  2329. *
  2330. * @param context $context
  2331. * @param boolean $includeparents, false means without parents.
  2332. * @return array
  2333. */
  2334. function get_roles_used_in_context(context $context, $includeparents = true) {
  2335. global $DB;
  2336. if ($includeparents === true) {
  2337. list($contextlist, $params) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'cl');
  2338. } else {
  2339. list($contextlist, $params) = $DB->get_in_or_equal($context->id, SQL_PARAMS_NAMED, 'cl');
  2340. }
  2341. if ($coursecontext = $context->get_course_context(false)) {
  2342. $params['coursecontext'] = $coursecontext->id;
  2343. } else {
  2344. $params['coursecontext'] = 0;
  2345. }
  2346. $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
  2347. FROM {role_assignments} ra, {role} r
  2348. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2349. WHERE r.id = ra.roleid
  2350. AND ra.contextid $contextlist
  2351. ORDER BY r.sortorder ASC";
  2352. return $DB->get_records_sql($sql, $params);
  2353. }
  2354. /**
  2355. * This function is used to print roles column in user profile page.
  2356. * It is using the CFG->profileroles to limit the list to only interesting roles.
  2357. * (The permission tab has full details of user role assignments.)
  2358. *
  2359. * @param int $userid
  2360. * @param int $courseid
  2361. * @return string
  2362. */
  2363. function get_user_roles_in_course($userid, $courseid) {
  2364. global $CFG, $DB;
  2365. if ($courseid == SITEID) {
  2366. $context = context_system::instance();
  2367. } else {
  2368. $context = context_course::instance($courseid);
  2369. }
  2370. // If the current user can assign roles, then they can see all roles on the profile and participants page,
  2371. // provided the roles are assigned to at least 1 user in the context. If not, only the policy-defined roles.
  2372. if (has_capability('moodle/role:assign', $context)) {
  2373. $rolesinscope = array_keys(get_all_roles($context));
  2374. } else {
  2375. $rolesinscope = empty($CFG->profileroles) ? [] : array_map('trim', explode(',', $CFG->profileroles));
  2376. }
  2377. if (empty($rolesinscope)) {
  2378. return '';
  2379. }
  2380. list($rallowed, $params) = $DB->get_in_or_equal($rolesinscope, SQL_PARAMS_NAMED, 'a');
  2381. list($contextlist, $cparams) = $DB->get_in_or_equal($context->get_parent_context_ids(true), SQL_PARAMS_NAMED, 'p');
  2382. $params = array_merge($params, $cparams);
  2383. if ($coursecontext = $context->get_course_context(false)) {
  2384. $params['coursecontext'] = $coursecontext->id;
  2385. } else {
  2386. $params['coursecontext'] = 0;
  2387. }
  2388. $sql = "SELECT DISTINCT r.id, r.name, r.shortname, r.sortorder, rn.name AS coursealias
  2389. FROM {role_assignments} ra, {role} r
  2390. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2391. WHERE r.id = ra.roleid
  2392. AND ra.contextid $contextlist
  2393. AND r.id $rallowed
  2394. AND ra.userid = :userid
  2395. ORDER BY r.sortorder ASC";
  2396. $params['userid'] = $userid;
  2397. $rolestring = '';
  2398. if ($roles = $DB->get_records_sql($sql, $params)) {
  2399. $viewableroles = get_viewable_roles($context, $userid);
  2400. $rolenames = array();
  2401. foreach ($roles as $roleid => $unused) {
  2402. if (isset($viewableroles[$roleid])) {
  2403. $url = new moodle_url('/user/index.php', ['contextid' => $context->id, 'roleid' => $roleid]);
  2404. $rolenames[] = '<a href="' . $url . '">' . $viewableroles[$roleid] . '</a>';
  2405. }
  2406. }
  2407. $rolestring = implode(',', $rolenames);
  2408. }
  2409. return $rolestring;
  2410. }
  2411. /**
  2412. * Checks if a user can assign users to a particular role in this context
  2413. *
  2414. * @param context $context
  2415. * @param int $targetroleid - the id of the role you want to assign users to
  2416. * @return boolean
  2417. */
  2418. function user_can_assign(context $context, $targetroleid) {
  2419. global $DB;
  2420. // First check to see if the user is a site administrator.
  2421. if (is_siteadmin()) {
  2422. return true;
  2423. }
  2424. // Check if user has override capability.
  2425. // If not return false.
  2426. if (!has_capability('moodle/role:assign', $context)) {
  2427. return false;
  2428. }
  2429. // pull out all active roles of this user from this context(or above)
  2430. if ($userroles = get_user_roles($context)) {
  2431. foreach ($userroles as $userrole) {
  2432. // if any in the role_allow_override table, then it's ok
  2433. if ($DB->get_record('role_allow_assign', array('roleid'=>$userrole->roleid, 'allowassign'=>$targetroleid))) {
  2434. return true;
  2435. }
  2436. }
  2437. }
  2438. return false;
  2439. }
  2440. /**
  2441. * Returns all site roles in correct sort order.
  2442. *
  2443. * Note: this method does not localise role names or descriptions,
  2444. * use role_get_names() if you need role names.
  2445. *
  2446. * @param context $context optional context for course role name aliases
  2447. * @return array of role records with optional coursealias property
  2448. */
  2449. function get_all_roles(context $context = null) {
  2450. global $DB;
  2451. if (!$context or !$coursecontext = $context->get_course_context(false)) {
  2452. $coursecontext = null;
  2453. }
  2454. if ($coursecontext) {
  2455. $sql = "SELECT r.*, rn.name AS coursealias
  2456. FROM {role} r
  2457. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2458. ORDER BY r.sortorder ASC";
  2459. return $DB->get_records_sql($sql, array('coursecontext'=>$coursecontext->id));
  2460. } else {
  2461. return $DB->get_records('role', array(), 'sortorder ASC');
  2462. }
  2463. }
  2464. /**
  2465. * Returns roles of a specified archetype
  2466. *
  2467. * @param string $archetype
  2468. * @return array of full role records
  2469. */
  2470. function get_archetype_roles($archetype) {
  2471. global $DB;
  2472. return $DB->get_records('role', array('archetype'=>$archetype), 'sortorder ASC');
  2473. }
  2474. /**
  2475. * Gets all the user roles assigned in this context, or higher contexts for a list of users.
  2476. *
  2477. * If you try using the combination $userids = [], $checkparentcontexts = true then this is likely
  2478. * to cause an out-of-memory error on large Moodle sites, so this combination is deprecated and
  2479. * outputs a warning, even though it is the default.
  2480. *
  2481. * @param context $context
  2482. * @param array $userids. An empty list means fetch all role assignments for the context.
  2483. * @param bool $checkparentcontexts defaults to true
  2484. * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
  2485. * @return array
  2486. */
  2487. function get_users_roles(context $context, $userids = [], $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
  2488. global $DB;
  2489. if (!$userids && $checkparentcontexts) {
  2490. debugging('Please do not call get_users_roles() with $checkparentcontexts = true ' .
  2491. 'and $userids array not set. This combination causes large Moodle sites ' .
  2492. 'with lots of site-wide role assignemnts to run out of memory.', DEBUG_DEVELOPER);
  2493. }
  2494. if ($checkparentcontexts) {
  2495. $contextids = $context->get_parent_context_ids();
  2496. } else {
  2497. $contextids = array();
  2498. }
  2499. $contextids[] = $context->id;
  2500. list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, 'con');
  2501. // If userids was passed as an empty array, we fetch all role assignments for the course.
  2502. if (empty($userids)) {
  2503. $useridlist = ' IS NOT NULL ';
  2504. $uparams = [];
  2505. } else {
  2506. list($useridlist, $uparams) = $DB->get_in_or_equal($userids, SQL_PARAMS_NAMED, 'uids');
  2507. }
  2508. $sql = "SELECT ra.*, r.name, r.shortname, ra.userid
  2509. FROM {role_assignments} ra, {role} r, {context} c
  2510. WHERE ra.userid $useridlist
  2511. AND ra.roleid = r.id
  2512. AND ra.contextid = c.id
  2513. AND ra.contextid $contextids
  2514. ORDER BY $order";
  2515. $all = $DB->get_records_sql($sql , array_merge($params, $uparams));
  2516. // Return results grouped by userid.
  2517. $result = [];
  2518. foreach ($all as $id => $record) {
  2519. if (!isset($result[$record->userid])) {
  2520. $result[$record->userid] = [];
  2521. }
  2522. $result[$record->userid][$record->id] = $record;
  2523. }
  2524. // Make sure all requested users are included in the result, even if they had no role assignments.
  2525. foreach ($userids as $id) {
  2526. if (!isset($result[$id])) {
  2527. $result[$id] = [];
  2528. }
  2529. }
  2530. return $result;
  2531. }
  2532. /**
  2533. * Gets all the user roles assigned in this context, or higher contexts
  2534. * this is mainly used when checking if a user can assign a role, or overriding a role
  2535. * i.e. we need to know what this user holds, in order to verify against allow_assign and
  2536. * allow_override tables
  2537. *
  2538. * @param context $context
  2539. * @param int $userid
  2540. * @param bool $checkparentcontexts defaults to true
  2541. * @param string $order defaults to 'c.contextlevel DESC, r.sortorder ASC'
  2542. * @return array
  2543. */
  2544. function get_user_roles(context $context, $userid = 0, $checkparentcontexts = true, $order = 'c.contextlevel DESC, r.sortorder ASC') {
  2545. global $USER, $DB;
  2546. if (empty($userid)) {
  2547. if (empty($USER->id)) {
  2548. return array();
  2549. }
  2550. $userid = $USER->id;
  2551. }
  2552. if ($checkparentcontexts) {
  2553. $contextids = $context->get_parent_context_ids();
  2554. } else {
  2555. $contextids = array();
  2556. }
  2557. $contextids[] = $context->id;
  2558. list($contextids, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_QM);
  2559. array_unshift($params, $userid);
  2560. $sql = "SELECT ra.*, r.name, r.shortname
  2561. FROM {role_assignments} ra, {role} r, {context} c
  2562. WHERE ra.userid = ?
  2563. AND ra.roleid = r.id
  2564. AND ra.contextid = c.id
  2565. AND ra.contextid $contextids
  2566. ORDER BY $order";
  2567. return $DB->get_records_sql($sql ,$params);
  2568. }
  2569. /**
  2570. * Like get_user_roles, but adds in the authenticated user role, and the front
  2571. * page roles, if applicable.
  2572. *
  2573. * @param context $context the context.
  2574. * @param int $userid optional. Defaults to $USER->id
  2575. * @return array of objects with fields ->userid, ->contextid and ->roleid.
  2576. */
  2577. function get_user_roles_with_special(context $context, $userid = 0) {
  2578. global $CFG, $USER;
  2579. if (empty($userid)) {
  2580. if (empty($USER->id)) {
  2581. return array();
  2582. }
  2583. $userid = $USER->id;
  2584. }
  2585. $ras = get_user_roles($context, $userid);
  2586. // Add front-page role if relevant.
  2587. $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
  2588. $isfrontpage = ($context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID) ||
  2589. is_inside_frontpage($context);
  2590. if ($defaultfrontpageroleid && $isfrontpage) {
  2591. $frontpagecontext = context_course::instance(SITEID);
  2592. $ra = new stdClass();
  2593. $ra->userid = $userid;
  2594. $ra->contextid = $frontpagecontext->id;
  2595. $ra->roleid = $defaultfrontpageroleid;
  2596. $ras[] = $ra;
  2597. }
  2598. // Add authenticated user role if relevant.
  2599. $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
  2600. if ($defaultuserroleid && !isguestuser($userid)) {
  2601. $systemcontext = context_system::instance();
  2602. $ra = new stdClass();
  2603. $ra->userid = $userid;
  2604. $ra->contextid = $systemcontext->id;
  2605. $ra->roleid = $defaultuserroleid;
  2606. $ras[] = $ra;
  2607. }
  2608. return $ras;
  2609. }
  2610. /**
  2611. * Creates a record in the role_allow_override table
  2612. *
  2613. * @param int $fromroleid source roleid
  2614. * @param int $targetroleid target roleid
  2615. * @return void
  2616. */
  2617. function core_role_set_override_allowed($fromroleid, $targetroleid) {
  2618. global $DB;
  2619. $record = new stdClass();
  2620. $record->roleid = $fromroleid;
  2621. $record->allowoverride = $targetroleid;
  2622. $DB->insert_record('role_allow_override', $record);
  2623. }
  2624. /**
  2625. * Creates a record in the role_allow_assign table
  2626. *
  2627. * @param int $fromroleid source roleid
  2628. * @param int $targetroleid target roleid
  2629. * @return void
  2630. */
  2631. function core_role_set_assign_allowed($fromroleid, $targetroleid) {
  2632. global $DB;
  2633. $record = new stdClass();
  2634. $record->roleid = $fromroleid;
  2635. $record->allowassign = $targetroleid;
  2636. $DB->insert_record('role_allow_assign', $record);
  2637. }
  2638. /**
  2639. * Creates a record in the role_allow_switch table
  2640. *
  2641. * @param int $fromroleid source roleid
  2642. * @param int $targetroleid target roleid
  2643. * @return void
  2644. */
  2645. function core_role_set_switch_allowed($fromroleid, $targetroleid) {
  2646. global $DB;
  2647. $record = new stdClass();
  2648. $record->roleid = $fromroleid;
  2649. $record->allowswitch = $targetroleid;
  2650. $DB->insert_record('role_allow_switch', $record);
  2651. }
  2652. /**
  2653. * Creates a record in the role_allow_view table
  2654. *
  2655. * @param int $fromroleid source roleid
  2656. * @param int $targetroleid target roleid
  2657. * @return void
  2658. */
  2659. function core_role_set_view_allowed($fromroleid, $targetroleid) {
  2660. global $DB;
  2661. $record = new stdClass();
  2662. $record->roleid = $fromroleid;
  2663. $record->allowview = $targetroleid;
  2664. $DB->insert_record('role_allow_view', $record);
  2665. }
  2666. /**
  2667. * Gets a list of roles that this user can assign in this context
  2668. *
  2669. * @param context $context the context.
  2670. * @param int $rolenamedisplay the type of role name to display. One of the
  2671. * ROLENAME_X constants. Default ROLENAME_ALIAS.
  2672. * @param bool $withusercounts if true, count the number of users with each role.
  2673. * @param integer|object $user A user id or object. By default (null) checks the permissions of the current user.
  2674. * @return array if $withusercounts is false, then an array $roleid => $rolename.
  2675. * if $withusercounts is true, returns a list of three arrays,
  2676. * $rolenames, $rolecounts, and $nameswithcounts.
  2677. */
  2678. function get_assignable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withusercounts = false, $user = null) {
  2679. global $USER, $DB;
  2680. // make sure there is a real user specified
  2681. if ($user === null) {
  2682. $userid = isset($USER->id) ? $USER->id : 0;
  2683. } else {
  2684. $userid = is_object($user) ? $user->id : $user;
  2685. }
  2686. if (!has_capability('moodle/role:assign', $context, $userid)) {
  2687. if ($withusercounts) {
  2688. return array(array(), array(), array());
  2689. } else {
  2690. return array();
  2691. }
  2692. }
  2693. $params = array();
  2694. $extrafields = '';
  2695. if ($withusercounts) {
  2696. $extrafields = ', (SELECT COUNT(DISTINCT u.id)
  2697. FROM {role_assignments} cra JOIN {user} u ON cra.userid = u.id
  2698. WHERE cra.roleid = r.id AND cra.contextid = :conid AND u.deleted = 0
  2699. ) AS usercount';
  2700. $params['conid'] = $context->id;
  2701. }
  2702. if (is_siteadmin($userid)) {
  2703. // show all roles allowed in this context to admins
  2704. $assignrestriction = "";
  2705. } else {
  2706. $parents = $context->get_parent_context_ids(true);
  2707. $contexts = implode(',' , $parents);
  2708. $assignrestriction = "JOIN (SELECT DISTINCT raa.allowassign AS id
  2709. FROM {role_allow_assign} raa
  2710. JOIN {role_assignments} ra ON ra.roleid = raa.roleid
  2711. WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
  2712. ) ar ON ar.id = r.id";
  2713. $params['userid'] = $userid;
  2714. }
  2715. $params['contextlevel'] = $context->contextlevel;
  2716. if ($coursecontext = $context->get_course_context(false)) {
  2717. $params['coursecontext'] = $coursecontext->id;
  2718. } else {
  2719. $params['coursecontext'] = 0; // no course aliases
  2720. $coursecontext = null;
  2721. }
  2722. $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias $extrafields
  2723. FROM {role} r
  2724. $assignrestriction
  2725. JOIN {role_context_levels} rcl ON (rcl.contextlevel = :contextlevel AND r.id = rcl.roleid)
  2726. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2727. ORDER BY r.sortorder ASC";
  2728. $roles = $DB->get_records_sql($sql, $params);
  2729. $rolenames = role_fix_names($roles, $coursecontext, $rolenamedisplay, true);
  2730. if (!$withusercounts) {
  2731. return $rolenames;
  2732. }
  2733. $rolecounts = array();
  2734. $nameswithcounts = array();
  2735. foreach ($roles as $role) {
  2736. $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->usercount . ')';
  2737. $rolecounts[$role->id] = $roles[$role->id]->usercount;
  2738. }
  2739. return array($rolenames, $rolecounts, $nameswithcounts);
  2740. }
  2741. /**
  2742. * Gets a list of roles that this user can switch to in a context
  2743. *
  2744. * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
  2745. * This function just process the contents of the role_allow_switch table. You also need to
  2746. * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
  2747. *
  2748. * @param context $context a context.
  2749. * @return array an array $roleid => $rolename.
  2750. */
  2751. function get_switchable_roles(context $context) {
  2752. global $USER, $DB;
  2753. // You can't switch roles without this capability.
  2754. if (!has_capability('moodle/role:switchroles', $context)) {
  2755. return [];
  2756. }
  2757. $params = array();
  2758. $extrajoins = '';
  2759. $extrawhere = '';
  2760. if (!is_siteadmin()) {
  2761. // Admins are allowed to switch to any role with.
  2762. // Others are subject to the additional constraint that the switch-to role must be allowed by
  2763. // 'role_allow_switch' for some role they have assigned in this context or any parent.
  2764. $parents = $context->get_parent_context_ids(true);
  2765. $contexts = implode(',' , $parents);
  2766. $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
  2767. JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
  2768. $extrawhere = "WHERE ra.userid = :userid AND ra.contextid IN ($contexts)";
  2769. $params['userid'] = $USER->id;
  2770. }
  2771. if ($coursecontext = $context->get_course_context(false)) {
  2772. $params['coursecontext'] = $coursecontext->id;
  2773. } else {
  2774. $params['coursecontext'] = 0; // no course aliases
  2775. $coursecontext = null;
  2776. }
  2777. $query = "
  2778. SELECT r.id, r.name, r.shortname, rn.name AS coursealias
  2779. FROM (SELECT DISTINCT rc.roleid
  2780. FROM {role_capabilities} rc
  2781. $extrajoins
  2782. $extrawhere) idlist
  2783. JOIN {role} r ON r.id = idlist.roleid
  2784. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2785. ORDER BY r.sortorder";
  2786. $roles = $DB->get_records_sql($query, $params);
  2787. return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
  2788. }
  2789. /**
  2790. * Gets a list of roles that this user can view in a context
  2791. *
  2792. * @param context $context a context.
  2793. * @param int $userid id of user.
  2794. * @return array an array $roleid => $rolename.
  2795. */
  2796. function get_viewable_roles(context $context, $userid = null) {
  2797. global $USER, $DB;
  2798. if ($userid == null) {
  2799. $userid = $USER->id;
  2800. }
  2801. $params = array();
  2802. $extrajoins = '';
  2803. $extrawhere = '';
  2804. if (!is_siteadmin()) {
  2805. // Admins are allowed to view any role.
  2806. // Others are subject to the additional constraint that the view role must be allowed by
  2807. // 'role_allow_view' for some role they have assigned in this context or any parent.
  2808. $contexts = $context->get_parent_context_ids(true);
  2809. list($insql, $inparams) = $DB->get_in_or_equal($contexts, SQL_PARAMS_NAMED);
  2810. $extrajoins = "JOIN {role_allow_view} ras ON ras.allowview = r.id
  2811. JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
  2812. $extrawhere = "WHERE ra.userid = :userid AND ra.contextid $insql";
  2813. $params += $inparams;
  2814. $params['userid'] = $userid;
  2815. }
  2816. if ($coursecontext = $context->get_course_context(false)) {
  2817. $params['coursecontext'] = $coursecontext->id;
  2818. } else {
  2819. $params['coursecontext'] = 0; // No course aliases.
  2820. $coursecontext = null;
  2821. }
  2822. $query = "
  2823. SELECT r.id, r.name, r.shortname, rn.name AS coursealias, r.sortorder
  2824. FROM {role} r
  2825. $extrajoins
  2826. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2827. $extrawhere
  2828. GROUP BY r.id, r.name, r.shortname, rn.name, r.sortorder
  2829. ORDER BY r.sortorder";
  2830. $roles = $DB->get_records_sql($query, $params);
  2831. return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
  2832. }
  2833. /**
  2834. * Gets a list of roles that this user can override in this context.
  2835. *
  2836. * @param context $context the context.
  2837. * @param int $rolenamedisplay the type of role name to display. One of the
  2838. * ROLENAME_X constants. Default ROLENAME_ALIAS.
  2839. * @param bool $withcounts if true, count the number of overrides that are set for each role.
  2840. * @return array if $withcounts is false, then an array $roleid => $rolename.
  2841. * if $withusercounts is true, returns a list of three arrays,
  2842. * $rolenames, $rolecounts, and $nameswithcounts.
  2843. */
  2844. function get_overridable_roles(context $context, $rolenamedisplay = ROLENAME_ALIAS, $withcounts = false) {
  2845. global $USER, $DB;
  2846. if (!has_any_capability(array('moodle/role:safeoverride', 'moodle/role:override'), $context)) {
  2847. if ($withcounts) {
  2848. return array(array(), array(), array());
  2849. } else {
  2850. return array();
  2851. }
  2852. }
  2853. $parents = $context->get_parent_context_ids(true);
  2854. $contexts = implode(',' , $parents);
  2855. $params = array();
  2856. $extrafields = '';
  2857. $params['userid'] = $USER->id;
  2858. if ($withcounts) {
  2859. $extrafields = ', (SELECT COUNT(rc.id) FROM {role_capabilities} rc
  2860. WHERE rc.roleid = ro.id AND rc.contextid = :conid) AS overridecount';
  2861. $params['conid'] = $context->id;
  2862. }
  2863. if ($coursecontext = $context->get_course_context(false)) {
  2864. $params['coursecontext'] = $coursecontext->id;
  2865. } else {
  2866. $params['coursecontext'] = 0; // no course aliases
  2867. $coursecontext = null;
  2868. }
  2869. if (is_siteadmin()) {
  2870. // show all roles to admins
  2871. $roles = $DB->get_records_sql("
  2872. SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
  2873. FROM {role} ro
  2874. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
  2875. ORDER BY ro.sortorder ASC", $params);
  2876. } else {
  2877. $roles = $DB->get_records_sql("
  2878. SELECT ro.id, ro.name, ro.shortname, rn.name AS coursealias $extrafields
  2879. FROM {role} ro
  2880. JOIN (SELECT DISTINCT r.id
  2881. FROM {role} r
  2882. JOIN {role_allow_override} rao ON r.id = rao.allowoverride
  2883. JOIN {role_assignments} ra ON rao.roleid = ra.roleid
  2884. WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
  2885. ) inline_view ON ro.id = inline_view.id
  2886. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = ro.id)
  2887. ORDER BY ro.sortorder ASC", $params);
  2888. }
  2889. $rolenames = role_fix_names($roles, $context, $rolenamedisplay, true);
  2890. if (!$withcounts) {
  2891. return $rolenames;
  2892. }
  2893. $rolecounts = array();
  2894. $nameswithcounts = array();
  2895. foreach ($roles as $role) {
  2896. $nameswithcounts[$role->id] = $rolenames[$role->id] . ' (' . $roles[$role->id]->overridecount . ')';
  2897. $rolecounts[$role->id] = $roles[$role->id]->overridecount;
  2898. }
  2899. return array($rolenames, $rolecounts, $nameswithcounts);
  2900. }
  2901. /**
  2902. * Create a role menu suitable for default role selection in enrol plugins.
  2903. *
  2904. * @package core_enrol
  2905. *
  2906. * @param context $context
  2907. * @param int $addroleid current or default role - always added to list
  2908. * @return array roleid=>localised role name
  2909. */
  2910. function get_default_enrol_roles(context $context, $addroleid = null) {
  2911. global $DB;
  2912. $params = array('contextlevel'=>CONTEXT_COURSE);
  2913. if ($coursecontext = $context->get_course_context(false)) {
  2914. $params['coursecontext'] = $coursecontext->id;
  2915. } else {
  2916. $params['coursecontext'] = 0; // no course names
  2917. $coursecontext = null;
  2918. }
  2919. if ($addroleid) {
  2920. $addrole = "OR r.id = :addroleid";
  2921. $params['addroleid'] = $addroleid;
  2922. } else {
  2923. $addrole = "";
  2924. }
  2925. $sql = "SELECT r.id, r.name, r.shortname, rn.name AS coursealias
  2926. FROM {role} r
  2927. LEFT JOIN {role_context_levels} rcl ON (rcl.roleid = r.id AND rcl.contextlevel = :contextlevel)
  2928. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  2929. WHERE rcl.id IS NOT NULL $addrole
  2930. ORDER BY sortorder DESC";
  2931. $roles = $DB->get_records_sql($sql, $params);
  2932. return role_fix_names($roles, $context, ROLENAME_BOTH, true);
  2933. }
  2934. /**
  2935. * Return context levels where this role is assignable.
  2936. *
  2937. * @param integer $roleid the id of a role.
  2938. * @return array list of the context levels at which this role may be assigned.
  2939. */
  2940. function get_role_contextlevels($roleid) {
  2941. global $DB;
  2942. return $DB->get_records_menu('role_context_levels', array('roleid' => $roleid),
  2943. 'contextlevel', 'id,contextlevel');
  2944. }
  2945. /**
  2946. * Return roles suitable for assignment at the specified context level.
  2947. *
  2948. * NOTE: this function name looks like a typo, should be probably get_roles_for_contextlevel()
  2949. *
  2950. * @param integer $contextlevel a contextlevel.
  2951. * @return array list of role ids that are assignable at this context level.
  2952. */
  2953. function get_roles_for_contextlevels($contextlevel) {
  2954. global $DB;
  2955. return $DB->get_records_menu('role_context_levels', array('contextlevel' => $contextlevel),
  2956. '', 'id,roleid');
  2957. }
  2958. /**
  2959. * Returns default context levels where roles can be assigned.
  2960. *
  2961. * @param string $rolearchetype one of the role archetypes - that is, one of the keys
  2962. * from the array returned by get_role_archetypes();
  2963. * @return array list of the context levels at which this type of role may be assigned by default.
  2964. */
  2965. function get_default_contextlevels($rolearchetype) {
  2966. static $defaults = array(
  2967. 'manager' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT, CONTEXT_COURSE),
  2968. 'coursecreator' => array(CONTEXT_SYSTEM, CONTEXT_COURSECAT),
  2969. 'editingteacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
  2970. 'teacher' => array(CONTEXT_COURSE, CONTEXT_MODULE),
  2971. 'student' => array(CONTEXT_COURSE, CONTEXT_MODULE),
  2972. 'guest' => array(),
  2973. 'user' => array(),
  2974. 'frontpage' => array());
  2975. if (isset($defaults[$rolearchetype])) {
  2976. return $defaults[$rolearchetype];
  2977. } else {
  2978. return array();
  2979. }
  2980. }
  2981. /**
  2982. * Set the context levels at which a particular role can be assigned.
  2983. * Throws exceptions in case of error.
  2984. *
  2985. * @param integer $roleid the id of a role.
  2986. * @param array $contextlevels the context levels at which this role should be assignable,
  2987. * duplicate levels are removed.
  2988. * @return void
  2989. */
  2990. function set_role_contextlevels($roleid, array $contextlevels) {
  2991. global $DB;
  2992. $DB->delete_records('role_context_levels', array('roleid' => $roleid));
  2993. $rcl = new stdClass();
  2994. $rcl->roleid = $roleid;
  2995. $contextlevels = array_unique($contextlevels);
  2996. foreach ($contextlevels as $level) {
  2997. $rcl->contextlevel = $level;
  2998. $DB->insert_record('role_context_levels', $rcl, false, true);
  2999. }
  3000. }
  3001. /**
  3002. * Gets sql joins for finding users with capability in the given context.
  3003. *
  3004. * @param context $context Context for the join.
  3005. * @param string|array $capability Capability name or array of names.
  3006. * If an array is provided then this is the equivalent of a logical 'OR',
  3007. * i.e. the user needs to have one of these capabilities.
  3008. * @param string $useridcolumn e.g. 'u.id'.
  3009. * @return \core\dml\sql_join Contains joins, wheres, params.
  3010. * This function will set ->cannotmatchanyrows if applicable.
  3011. * This may let you skip doing a DB query.
  3012. */
  3013. function get_with_capability_join(context $context, $capability, $useridcolumn) {
  3014. global $CFG, $DB;
  3015. // Add a unique prefix to param names to ensure they are unique.
  3016. static $i = 0;
  3017. $i++;
  3018. $paramprefix = 'eu' . $i . '_';
  3019. $defaultuserroleid = isset($CFG->defaultuserroleid) ? $CFG->defaultuserroleid : 0;
  3020. $defaultfrontpageroleid = isset($CFG->defaultfrontpageroleid) ? $CFG->defaultfrontpageroleid : 0;
  3021. $ctxids = trim($context->path, '/');
  3022. $ctxids = str_replace('/', ',', $ctxids);
  3023. // Context is the frontpage
  3024. $isfrontpage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid == SITEID;
  3025. $isfrontpage = $isfrontpage || is_inside_frontpage($context);
  3026. $caps = (array) $capability;
  3027. // Construct list of context paths bottom --> top.
  3028. list($contextids, $paths) = get_context_info_list($context);
  3029. // We need to find out all roles that have these capabilities either in definition or in overrides.
  3030. $defs = [];
  3031. list($incontexts, $params) = $DB->get_in_or_equal($contextids, SQL_PARAMS_NAMED, $paramprefix . 'con');
  3032. list($incaps, $params2) = $DB->get_in_or_equal($caps, SQL_PARAMS_NAMED, $paramprefix . 'cap');
  3033. // Check whether context locking is enabled.
  3034. // Filter out any write capability if this is the case.
  3035. $excludelockedcaps = '';
  3036. $excludelockedcapsparams = [];
  3037. if (!empty($CFG->contextlocking) && $context->locked) {
  3038. $excludelockedcaps = 'AND (cap.captype = :capread OR cap.name = :managelockscap)';
  3039. $excludelockedcapsparams['capread'] = 'read';
  3040. $excludelockedcapsparams['managelockscap'] = 'moodle/site:managecontextlocks';
  3041. }
  3042. $params = array_merge($params, $params2, $excludelockedcapsparams);
  3043. $sql = "SELECT rc.id, rc.roleid, rc.permission, rc.capability, ctx.path
  3044. FROM {role_capabilities} rc
  3045. JOIN {capabilities} cap ON rc.capability = cap.name
  3046. JOIN {context} ctx on rc.contextid = ctx.id
  3047. WHERE rc.contextid $incontexts AND rc.capability $incaps $excludelockedcaps";
  3048. $rcs = $DB->get_records_sql($sql, $params);
  3049. foreach ($rcs as $rc) {
  3050. $defs[$rc->capability][$rc->path][$rc->roleid] = $rc->permission;
  3051. }
  3052. // Go through the permissions bottom-->top direction to evaluate the current permission,
  3053. // first one wins (prohibit is an exception that always wins).
  3054. $access = [];
  3055. foreach ($caps as $cap) {
  3056. foreach ($paths as $path) {
  3057. if (empty($defs[$cap][$path])) {
  3058. continue;
  3059. }
  3060. foreach ($defs[$cap][$path] as $roleid => $perm) {
  3061. if ($perm == CAP_PROHIBIT) {
  3062. $access[$cap][$roleid] = CAP_PROHIBIT;
  3063. continue;
  3064. }
  3065. if (!isset($access[$cap][$roleid])) {
  3066. $access[$cap][$roleid] = (int)$perm;
  3067. }
  3068. }
  3069. }
  3070. }
  3071. // Make lists of roles that are needed and prohibited in this context.
  3072. $needed = []; // One of these is enough.
  3073. $prohibited = []; // Must not have any of these.
  3074. foreach ($caps as $cap) {
  3075. if (empty($access[$cap])) {
  3076. continue;
  3077. }
  3078. foreach ($access[$cap] as $roleid => $perm) {
  3079. if ($perm == CAP_PROHIBIT) {
  3080. unset($needed[$cap][$roleid]);
  3081. $prohibited[$cap][$roleid] = true;
  3082. } else if ($perm == CAP_ALLOW and empty($prohibited[$cap][$roleid])) {
  3083. $needed[$cap][$roleid] = true;
  3084. }
  3085. }
  3086. if (empty($needed[$cap]) or !empty($prohibited[$cap][$defaultuserroleid])) {
  3087. // Easy, nobody has the permission.
  3088. unset($needed[$cap]);
  3089. unset($prohibited[$cap]);
  3090. } else if ($isfrontpage and !empty($prohibited[$cap][$defaultfrontpageroleid])) {
  3091. // Everybody is disqualified on the frontpage.
  3092. unset($needed[$cap]);
  3093. unset($prohibited[$cap]);
  3094. }
  3095. if (empty($prohibited[$cap])) {
  3096. unset($prohibited[$cap]);
  3097. }
  3098. }
  3099. if (empty($needed)) {
  3100. // There can not be anybody if no roles match this request.
  3101. return new \core\dml\sql_join('', '1 = 2', [], true);
  3102. }
  3103. if (empty($prohibited)) {
  3104. // We can compact the needed roles.
  3105. $n = [];
  3106. foreach ($needed as $cap) {
  3107. foreach ($cap as $roleid => $unused) {
  3108. $n[$roleid] = true;
  3109. }
  3110. }
  3111. $needed = ['any' => $n];
  3112. unset($n);
  3113. }
  3114. // Prepare query clauses.
  3115. $wherecond = [];
  3116. $params = [];
  3117. $joins = [];
  3118. $cannotmatchanyrows = false;
  3119. // We never return deleted users or guest account.
  3120. // Use a hack to get the deleted user column without an API change.
  3121. $deletedusercolumn = substr($useridcolumn, 0, -2) . 'deleted';
  3122. $wherecond[] = "$deletedusercolumn = 0 AND $useridcolumn <> :{$paramprefix}guestid";
  3123. $params[$paramprefix . 'guestid'] = $CFG->siteguest;
  3124. // Now add the needed and prohibited roles conditions as joins.
  3125. if (!empty($needed['any'])) {
  3126. // Simple case - there are no prohibits involved.
  3127. if (!empty($needed['any'][$defaultuserroleid]) ||
  3128. ($isfrontpage && !empty($needed['any'][$defaultfrontpageroleid]))) {
  3129. // Everybody.
  3130. } else {
  3131. $joins[] = "JOIN (SELECT DISTINCT userid
  3132. FROM {role_assignments}
  3133. WHERE contextid IN ($ctxids)
  3134. AND roleid IN (" . implode(',', array_keys($needed['any'])) . ")
  3135. ) ra ON ra.userid = $useridcolumn";
  3136. }
  3137. } else {
  3138. $unions = [];
  3139. $everybody = false;
  3140. foreach ($needed as $cap => $unused) {
  3141. if (empty($prohibited[$cap])) {
  3142. if (!empty($needed[$cap][$defaultuserroleid]) ||
  3143. ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
  3144. $everybody = true;
  3145. break;
  3146. } else {
  3147. $unions[] = "SELECT userid
  3148. FROM {role_assignments}
  3149. WHERE contextid IN ($ctxids)
  3150. AND roleid IN (".implode(',', array_keys($needed[$cap])) .")";
  3151. }
  3152. } else {
  3153. if (!empty($prohibited[$cap][$defaultuserroleid]) ||
  3154. ($isfrontpage && !empty($prohibited[$cap][$defaultfrontpageroleid]))) {
  3155. // Nobody can have this cap because it is prohibited in default roles.
  3156. continue;
  3157. } else if (!empty($needed[$cap][$defaultuserroleid]) ||
  3158. ($isfrontpage && !empty($needed[$cap][$defaultfrontpageroleid]))) {
  3159. // Everybody except the prohibited - hiding does not matter.
  3160. $unions[] = "SELECT id AS userid
  3161. FROM {user}
  3162. WHERE id NOT IN (SELECT userid
  3163. FROM {role_assignments}
  3164. WHERE contextid IN ($ctxids)
  3165. AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
  3166. } else {
  3167. $unions[] = "SELECT userid
  3168. FROM {role_assignments}
  3169. WHERE contextid IN ($ctxids) AND roleid IN (" . implode(',', array_keys($needed[$cap])) . ")
  3170. AND userid NOT IN (
  3171. SELECT userid
  3172. FROM {role_assignments}
  3173. WHERE contextid IN ($ctxids)
  3174. AND roleid IN (" . implode(',', array_keys($prohibited[$cap])) . "))";
  3175. }
  3176. }
  3177. }
  3178. if (!$everybody) {
  3179. if ($unions) {
  3180. $joins[] = "JOIN (
  3181. SELECT DISTINCT userid
  3182. FROM (
  3183. " . implode("\n UNION \n", $unions) . "
  3184. ) us
  3185. ) ra ON ra.userid = $useridcolumn";
  3186. } else {
  3187. // Only prohibits found - nobody can be matched.
  3188. $wherecond[] = "1 = 2";
  3189. $cannotmatchanyrows = true;
  3190. }
  3191. }
  3192. }
  3193. return new \core\dml\sql_join(implode("\n", $joins), implode(" AND ", $wherecond), $params, $cannotmatchanyrows);
  3194. }
  3195. /**
  3196. * Who has this capability in this context?
  3197. *
  3198. * This can be a very expensive call - use sparingly and keep
  3199. * the results if you are going to need them again soon.
  3200. *
  3201. * Note if $fields is empty this function attempts to get u.*
  3202. * which can get rather large - and has a serious perf impact
  3203. * on some DBs.
  3204. *
  3205. * @param context $context
  3206. * @param string|array $capability - capability name(s)
  3207. * @param string $fields - fields to be pulled. The user table is aliased to 'u'. u.id MUST be included.
  3208. * @param string $sort - the sort order. Default is lastaccess time.
  3209. * @param mixed $limitfrom - number of records to skip (offset)
  3210. * @param mixed $limitnum - number of records to fetch
  3211. * @param string|array $groups - single group or array of groups - only return
  3212. * users who are in one of these group(s).
  3213. * @param string|array $exceptions - list of users to exclude, comma separated or array
  3214. * @param bool $notuseddoanything not used any more, admin accounts are never returned
  3215. * @param bool $notusedview - use get_enrolled_sql() instead
  3216. * @param bool $useviewallgroups if $groups is set the return users who
  3217. * have capability both $capability and moodle/site:accessallgroups
  3218. * in this context, as well as users who have $capability and who are
  3219. * in $groups.
  3220. * @return array of user records
  3221. */
  3222. function get_users_by_capability(context $context, $capability, $fields = '', $sort = '', $limitfrom = '', $limitnum = '',
  3223. $groups = '', $exceptions = '', $notuseddoanything = null, $notusedview = null, $useviewallgroups = false) {
  3224. global $CFG, $DB;
  3225. // Context is a course page other than the frontpage.
  3226. $iscoursepage = $context->contextlevel == CONTEXT_COURSE && $context->instanceid != SITEID;
  3227. // Set up default fields list if necessary.
  3228. if (empty($fields)) {
  3229. if ($iscoursepage) {
  3230. $fields = 'u.*, ul.timeaccess AS lastaccess';
  3231. } else {
  3232. $fields = 'u.*';
  3233. }
  3234. } else {
  3235. if ($CFG->debugdeveloper && strpos($fields, 'u.*') === false && strpos($fields, 'u.id') === false) {
  3236. debugging('u.id must be included in the list of fields passed to get_users_by_capability().', DEBUG_DEVELOPER);
  3237. }
  3238. }
  3239. // Set up default sort if necessary.
  3240. if (empty($sort)) { // default to course lastaccess or just lastaccess
  3241. if ($iscoursepage) {
  3242. $sort = 'ul.timeaccess';
  3243. } else {
  3244. $sort = 'u.lastaccess';
  3245. }
  3246. }
  3247. // Get the bits of SQL relating to capabilities.
  3248. $sqljoin = get_with_capability_join($context, $capability, 'u.id');
  3249. if ($sqljoin->cannotmatchanyrows) {
  3250. return [];
  3251. }
  3252. // Prepare query clauses.
  3253. $wherecond = [$sqljoin->wheres];
  3254. $params = $sqljoin->params;
  3255. $joins = [$sqljoin->joins];
  3256. // Add user lastaccess JOIN, if required.
  3257. if ((strpos($sort, 'ul.timeaccess') === false) and (strpos($fields, 'ul.timeaccess') === false)) {
  3258. // Here user_lastaccess is not required MDL-13810.
  3259. } else {
  3260. if ($iscoursepage) {
  3261. $joins[] = "LEFT OUTER JOIN {user_lastaccess} ul ON (ul.userid = u.id AND ul.courseid = {$context->instanceid})";
  3262. } else {
  3263. throw new coding_exception('Invalid sort in get_users_by_capability(), ul.timeaccess allowed only for course contexts.');
  3264. }
  3265. }
  3266. // Groups.
  3267. if ($groups) {
  3268. $groups = (array)$groups;
  3269. list($grouptest, $grpparams) = $DB->get_in_or_equal($groups, SQL_PARAMS_NAMED, 'grp');
  3270. $joins[] = "LEFT OUTER JOIN (SELECT DISTINCT userid
  3271. FROM {groups_members}
  3272. WHERE groupid $grouptest
  3273. ) gm ON gm.userid = u.id";
  3274. $params = array_merge($params, $grpparams);
  3275. $grouptest = 'gm.userid IS NOT NULL';
  3276. if ($useviewallgroups) {
  3277. $viewallgroupsusers = get_users_by_capability($context, 'moodle/site:accessallgroups', 'u.id, u.id', '', '', '', '', $exceptions);
  3278. if (!empty($viewallgroupsusers)) {
  3279. $grouptest .= ' OR u.id IN (' . implode(',', array_keys($viewallgroupsusers)) . ')';
  3280. }
  3281. }
  3282. $wherecond[] = "($grouptest)";
  3283. }
  3284. // User exceptions.
  3285. if (!empty($exceptions)) {
  3286. $exceptions = (array)$exceptions;
  3287. list($exsql, $exparams) = $DB->get_in_or_equal($exceptions, SQL_PARAMS_NAMED, 'exc', false);
  3288. $params = array_merge($params, $exparams);
  3289. $wherecond[] = "u.id $exsql";
  3290. }
  3291. // Collect WHERE conditions and needed joins.
  3292. $where = implode(' AND ', $wherecond);
  3293. if ($where !== '') {
  3294. $where = 'WHERE ' . $where;
  3295. }
  3296. $joins = implode("\n", $joins);
  3297. // Finally! we have all the bits, run the query.
  3298. $sql = "SELECT $fields
  3299. FROM {user} u
  3300. $joins
  3301. $where
  3302. ORDER BY $sort";
  3303. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  3304. }
  3305. /**
  3306. * Re-sort a users array based on a sorting policy
  3307. *
  3308. * Will re-sort a $users results array (from get_users_by_capability(), usually)
  3309. * based on a sorting policy. This is to support the odd practice of
  3310. * sorting teachers by 'authority', where authority was "lowest id of the role
  3311. * assignment".
  3312. *
  3313. * Will execute 1 database query. Only suitable for small numbers of users, as it
  3314. * uses an u.id IN() clause.
  3315. *
  3316. * Notes about the sorting criteria.
  3317. *
  3318. * As a default, we cannot rely on role.sortorder because then
  3319. * admins/coursecreators will always win. That is why the sane
  3320. * rule "is locality matters most", with sortorder as 2nd
  3321. * consideration.
  3322. *
  3323. * If you want role.sortorder, use the 'sortorder' policy, and
  3324. * name explicitly what roles you want to cover. It's probably
  3325. * a good idea to see what roles have the capabilities you want
  3326. * (array_diff() them against roiles that have 'can-do-anything'
  3327. * to weed out admin-ish roles. Or fetch a list of roles from
  3328. * variables like $CFG->coursecontact .
  3329. *
  3330. * @param array $users Users array, keyed on userid
  3331. * @param context $context
  3332. * @param array $roles ids of the roles to include, optional
  3333. * @param string $sortpolicy defaults to locality, more about
  3334. * @return array sorted copy of the array
  3335. */
  3336. function sort_by_roleassignment_authority($users, context $context, $roles = array(), $sortpolicy = 'locality') {
  3337. global $DB;
  3338. $userswhere = ' ra.userid IN (' . implode(',',array_keys($users)) . ')';
  3339. $contextwhere = 'AND ra.contextid IN ('.str_replace('/', ',',substr($context->path, 1)).')';
  3340. if (empty($roles)) {
  3341. $roleswhere = '';
  3342. } else {
  3343. $roleswhere = ' AND ra.roleid IN ('.implode(',',$roles).')';
  3344. }
  3345. $sql = "SELECT ra.userid
  3346. FROM {role_assignments} ra
  3347. JOIN {role} r
  3348. ON ra.roleid=r.id
  3349. JOIN {context} ctx
  3350. ON ra.contextid=ctx.id
  3351. WHERE $userswhere
  3352. $contextwhere
  3353. $roleswhere";
  3354. // Default 'locality' policy -- read PHPDoc notes
  3355. // about sort policies...
  3356. $orderby = 'ORDER BY '
  3357. .'ctx.depth DESC, ' /* locality wins */
  3358. .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
  3359. .'ra.id'; /* role assignment order tie-breaker */
  3360. if ($sortpolicy === 'sortorder') {
  3361. $orderby = 'ORDER BY '
  3362. .'r.sortorder ASC, ' /* rolesorting 2nd criteria */
  3363. .'ra.id'; /* role assignment order tie-breaker */
  3364. }
  3365. $sortedids = $DB->get_fieldset_sql($sql . $orderby);
  3366. $sortedusers = array();
  3367. $seen = array();
  3368. foreach ($sortedids as $id) {
  3369. // Avoid duplicates
  3370. if (isset($seen[$id])) {
  3371. continue;
  3372. }
  3373. $seen[$id] = true;
  3374. // assign
  3375. $sortedusers[$id] = $users[$id];
  3376. }
  3377. return $sortedusers;
  3378. }
  3379. /**
  3380. * Gets all the users assigned this role in this context or higher
  3381. *
  3382. * Note that moodle is based on capabilities and it is usually better
  3383. * to check permissions than to check role ids as the capabilities
  3384. * system is more flexible. If you really need, you can to use this
  3385. * function but consider has_capability() as a possible substitute.
  3386. *
  3387. * All $sort fields are added into $fields if not present there yet.
  3388. *
  3389. * If $roleid is an array or is empty (all roles) you need to set $fields
  3390. * (and $sort by extension) params according to it, as the first field
  3391. * returned by the database should be unique (ra.id is the best candidate).
  3392. *
  3393. * @param int $roleid (can also be an array of ints!)
  3394. * @param context $context
  3395. * @param bool $parent if true, get list of users assigned in higher context too
  3396. * @param string $fields fields from user (u.) , role assignment (ra) or role (r.)
  3397. * @param string $sort sort from user (u.) , role assignment (ra.) or role (r.).
  3398. * null => use default sort from users_order_by_sql.
  3399. * @param bool $all true means all, false means limit to enrolled users
  3400. * @param string $group defaults to ''
  3401. * @param mixed $limitfrom defaults to ''
  3402. * @param mixed $limitnum defaults to ''
  3403. * @param string $extrawheretest defaults to ''
  3404. * @param array $whereorsortparams any paramter values used by $sort or $extrawheretest.
  3405. * @return array
  3406. */
  3407. function get_role_users($roleid, context $context, $parent = false, $fields = '',
  3408. $sort = null, $all = true, $group = '',
  3409. $limitfrom = '', $limitnum = '', $extrawheretest = '', $whereorsortparams = array()) {
  3410. global $DB;
  3411. if (empty($fields)) {
  3412. $allnames = get_all_user_name_fields(true, 'u');
  3413. $fields = 'u.id, u.confirmed, u.username, '. $allnames . ', ' .
  3414. 'u.maildisplay, u.mailformat, u.maildigest, u.email, u.emailstop, u.city, '.
  3415. 'u.country, u.picture, u.idnumber, u.department, u.institution, '.
  3416. 'u.lang, u.timezone, u.lastaccess, u.mnethostid, r.name AS rolename, r.sortorder, '.
  3417. 'r.shortname AS roleshortname, rn.name AS rolecoursealias';
  3418. }
  3419. // Prevent wrong function uses.
  3420. if ((empty($roleid) || is_array($roleid)) && strpos($fields, 'ra.id') !== 0) {
  3421. debugging('get_role_users() without specifying one single roleid needs to be called prefixing ' .
  3422. 'role assignments id (ra.id) as unique field, you can use $fields param for it.');
  3423. if (!empty($roleid)) {
  3424. // Solving partially the issue when specifying multiple roles.
  3425. $users = array();
  3426. foreach ($roleid as $id) {
  3427. // Ignoring duplicated keys keeping the first user appearance.
  3428. $users = $users + get_role_users($id, $context, $parent, $fields, $sort, $all, $group,
  3429. $limitfrom, $limitnum, $extrawheretest, $whereorsortparams);
  3430. }
  3431. return $users;
  3432. }
  3433. }
  3434. $parentcontexts = '';
  3435. if ($parent) {
  3436. $parentcontexts = substr($context->path, 1); // kill leading slash
  3437. $parentcontexts = str_replace('/', ',', $parentcontexts);
  3438. if ($parentcontexts !== '') {
  3439. $parentcontexts = ' OR ra.contextid IN ('.$parentcontexts.' )';
  3440. }
  3441. }
  3442. if ($roleid) {
  3443. list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_NAMED, 'r');
  3444. $roleselect = "AND ra.roleid $rids";
  3445. } else {
  3446. $params = array();
  3447. $roleselect = '';
  3448. }
  3449. if ($coursecontext = $context->get_course_context(false)) {
  3450. $params['coursecontext'] = $coursecontext->id;
  3451. } else {
  3452. $params['coursecontext'] = 0;
  3453. }
  3454. if ($group) {
  3455. $groupjoin = "JOIN {groups_members} gm ON gm.userid = u.id";
  3456. $groupselect = " AND gm.groupid = :groupid ";
  3457. $params['groupid'] = $group;
  3458. } else {
  3459. $groupjoin = '';
  3460. $groupselect = '';
  3461. }
  3462. $params['contextid'] = $context->id;
  3463. if ($extrawheretest) {
  3464. $extrawheretest = ' AND ' . $extrawheretest;
  3465. }
  3466. if ($whereorsortparams) {
  3467. $params = array_merge($params, $whereorsortparams);
  3468. }
  3469. if (!$sort) {
  3470. list($sort, $sortparams) = users_order_by_sql('u');
  3471. $params = array_merge($params, $sortparams);
  3472. }
  3473. // Adding the fields from $sort that are not present in $fields.
  3474. $sortarray = preg_split('/,\s*/', $sort);
  3475. $fieldsarray = preg_split('/,\s*/', $fields);
  3476. // Discarding aliases from the fields.
  3477. $fieldnames = array();
  3478. foreach ($fieldsarray as $key => $field) {
  3479. list($fieldnames[$key]) = explode(' ', $field);
  3480. }
  3481. $addedfields = array();
  3482. foreach ($sortarray as $sortfield) {
  3483. // Throw away any additional arguments to the sort (e.g. ASC/DESC).
  3484. list($sortfield) = explode(' ', $sortfield);
  3485. list($tableprefix) = explode('.', $sortfield);
  3486. $fieldpresent = false;
  3487. foreach ($fieldnames as $fieldname) {
  3488. if ($fieldname === $sortfield || $fieldname === $tableprefix.'.*') {
  3489. $fieldpresent = true;
  3490. break;
  3491. }
  3492. }
  3493. if (!$fieldpresent) {
  3494. $fieldsarray[] = $sortfield;
  3495. $addedfields[] = $sortfield;
  3496. }
  3497. }
  3498. $fields = implode(', ', $fieldsarray);
  3499. if (!empty($addedfields)) {
  3500. $addedfields = implode(', ', $addedfields);
  3501. debugging('get_role_users() adding '.$addedfields.' to the query result because they were required by $sort but missing in $fields');
  3502. }
  3503. if ($all === null) {
  3504. // Previously null was used to indicate that parameter was not used.
  3505. $all = true;
  3506. }
  3507. if (!$all and $coursecontext) {
  3508. // Do not use get_enrolled_sql() here for performance reasons.
  3509. $ejoin = "JOIN {user_enrolments} ue ON ue.userid = u.id
  3510. JOIN {enrol} e ON (e.id = ue.enrolid AND e.courseid = :ecourseid)";
  3511. $params['ecourseid'] = $coursecontext->instanceid;
  3512. } else {
  3513. $ejoin = "";
  3514. }
  3515. $sql = "SELECT DISTINCT $fields, ra.roleid
  3516. FROM {role_assignments} ra
  3517. JOIN {user} u ON u.id = ra.userid
  3518. JOIN {role} r ON ra.roleid = r.id
  3519. $ejoin
  3520. LEFT JOIN {role_names} rn ON (rn.contextid = :coursecontext AND rn.roleid = r.id)
  3521. $groupjoin
  3522. WHERE (ra.contextid = :contextid $parentcontexts)
  3523. $roleselect
  3524. $groupselect
  3525. $extrawheretest
  3526. ORDER BY $sort"; // join now so that we can just use fullname() later
  3527. return $DB->get_records_sql($sql, $params, $limitfrom, $limitnum);
  3528. }
  3529. /**
  3530. * Counts all the users assigned this role in this context or higher
  3531. *
  3532. * @param int|array $roleid either int or an array of ints
  3533. * @param context $context
  3534. * @param bool $parent if true, get list of users assigned in higher context too
  3535. * @return int Returns the result count
  3536. */
  3537. function count_role_users($roleid, context $context, $parent = false) {
  3538. global $DB;
  3539. if ($parent) {
  3540. if ($contexts = $context->get_parent_context_ids()) {
  3541. $parentcontexts = ' OR r.contextid IN ('.implode(',', $contexts).')';
  3542. } else {
  3543. $parentcontexts = '';
  3544. }
  3545. } else {
  3546. $parentcontexts = '';
  3547. }
  3548. if ($roleid) {
  3549. list($rids, $params) = $DB->get_in_or_equal($roleid, SQL_PARAMS_QM);
  3550. $roleselect = "AND r.roleid $rids";
  3551. } else {
  3552. $params = array();
  3553. $roleselect = '';
  3554. }
  3555. array_unshift($params, $context->id);
  3556. $sql = "SELECT COUNT(DISTINCT u.id)
  3557. FROM {role_assignments} r
  3558. JOIN {user} u ON u.id = r.userid
  3559. WHERE (r.contextid = ? $parentcontexts)
  3560. $roleselect
  3561. AND u.deleted = 0";
  3562. return $DB->count_records_sql($sql, $params);
  3563. }
  3564. /**
  3565. * This function gets the list of courses that this user has a particular capability in.
  3566. *
  3567. * It is now reasonably efficient, but bear in mind that if there are users who have the capability
  3568. * everywhere, it may return an array of all courses.
  3569. *
  3570. * @param string $capability Capability in question
  3571. * @param int $userid User ID or null for current user
  3572. * @param bool $doanything True if 'doanything' is permitted (default)
  3573. * @param string $fieldsexceptid Leave blank if you only need 'id' in the course records;
  3574. * otherwise use a comma-separated list of the fields you require, not including id.
  3575. * Add ctxid, ctxpath, ctxdepth etc to return course context information for preloading.
  3576. * @param string $orderby If set, use a comma-separated list of fields from course
  3577. * table with sql modifiers (DESC) if needed
  3578. * @param int $limit Limit the number of courses to return on success. Zero equals all entries.
  3579. * @return array|bool Array of courses, if none found false is returned.
  3580. */
  3581. function get_user_capability_course($capability, $userid = null, $doanything = true, $fieldsexceptid = '', $orderby = '',
  3582. $limit = 0) {
  3583. global $DB, $USER;
  3584. // Default to current user.
  3585. if (!$userid) {
  3586. $userid = $USER->id;
  3587. }
  3588. if ($doanything && is_siteadmin($userid)) {
  3589. // If the user is a site admin and $doanything is enabled then there is no need to restrict
  3590. // the list of courses.
  3591. $contextlimitsql = '';
  3592. $contextlimitparams = [];
  3593. } else {
  3594. // Gets SQL to limit contexts ('x' table) to those where the user has this capability.
  3595. list ($contextlimitsql, $contextlimitparams) = \core\access\get_user_capability_course_helper::get_sql(
  3596. $userid, $capability);
  3597. if (!$contextlimitsql) {
  3598. // If the does not have this capability in any context, return false without querying.
  3599. return false;
  3600. }
  3601. $contextlimitsql = 'WHERE' . $contextlimitsql;
  3602. }
  3603. // Convert fields list and ordering
  3604. $fieldlist = '';
  3605. if ($fieldsexceptid) {
  3606. $fields = array_map('trim', explode(',', $fieldsexceptid));
  3607. foreach ($fields as $field) {
  3608. // Context fields have a different alias.
  3609. if (strpos($field, 'ctx') === 0) {
  3610. switch($field) {
  3611. case 'ctxlevel' :
  3612. $realfield = 'contextlevel';
  3613. break;
  3614. case 'ctxinstance' :
  3615. $realfield = 'instanceid';
  3616. break;
  3617. default:
  3618. $realfield = substr($field, 3);
  3619. break;
  3620. }
  3621. $fieldlist .= ',x.' . $realfield . ' AS ' . $field;
  3622. } else {
  3623. $fieldlist .= ',c.'.$field;
  3624. }
  3625. }
  3626. }
  3627. if ($orderby) {
  3628. $fields = explode(',', $orderby);
  3629. $orderby = '';
  3630. foreach ($fields as $field) {
  3631. if ($orderby) {
  3632. $orderby .= ',';
  3633. }
  3634. $orderby .= 'c.'.$field;
  3635. }
  3636. $orderby = 'ORDER BY '.$orderby;
  3637. }
  3638. $courses = array();
  3639. $rs = $DB->get_recordset_sql("
  3640. SELECT c.id $fieldlist
  3641. FROM {course} c
  3642. JOIN {context} x ON c.id = x.instanceid AND x.contextlevel = ?
  3643. $contextlimitsql
  3644. $orderby", array_merge([CONTEXT_COURSE], $contextlimitparams));
  3645. foreach ($rs as $course) {
  3646. $courses[] = $course;
  3647. $limit--;
  3648. if ($limit == 0) {
  3649. break;
  3650. }
  3651. }
  3652. $rs->close();
  3653. return empty($courses) ? false : $courses;
  3654. }
  3655. /**
  3656. * Switches the current user to another role for the current session and only
  3657. * in the given context.
  3658. *
  3659. * The caller *must* check
  3660. * - that this op is allowed
  3661. * - that the requested role can be switched to in this context (use get_switchable_roles)
  3662. * - that the requested role is NOT $CFG->defaultuserroleid
  3663. *
  3664. * To "unswitch" pass 0 as the roleid.
  3665. *
  3666. * This function *will* modify $USER->access - beware
  3667. *
  3668. * @param integer $roleid the role to switch to.
  3669. * @param context $context the context in which to perform the switch.
  3670. * @return bool success or failure.
  3671. */
  3672. function role_switch($roleid, context $context) {
  3673. global $USER;
  3674. // Add the ghost RA to $USER->access as $USER->access['rsw'][$path] = $roleid.
  3675. // To un-switch just unset($USER->access['rsw'][$path]).
  3676. //
  3677. // Note: it is not possible to switch to roles that do not have course:view
  3678. if (!isset($USER->access)) {
  3679. load_all_capabilities();
  3680. }
  3681. // Add the switch RA
  3682. if ($roleid == 0) {
  3683. unset($USER->access['rsw'][$context->path]);
  3684. return true;
  3685. }
  3686. $USER->access['rsw'][$context->path] = $roleid;
  3687. return true;
  3688. }
  3689. /**
  3690. * Checks if the user has switched roles within the given course.
  3691. *
  3692. * Note: You can only switch roles within the course, hence it takes a course id
  3693. * rather than a context. On that note Petr volunteered to implement this across
  3694. * all other contexts, all requests for this should be forwarded to him ;)
  3695. *
  3696. * @param int $courseid The id of the course to check
  3697. * @return bool True if the user has switched roles within the course.
  3698. */
  3699. function is_role_switched($courseid) {
  3700. global $USER;
  3701. $context = context_course::instance($courseid, MUST_EXIST);
  3702. return (!empty($USER->access['rsw'][$context->path]));
  3703. }
  3704. /**
  3705. * Get any role that has an override on exact context
  3706. *
  3707. * @param context $context The context to check
  3708. * @return array An array of roles
  3709. */
  3710. function get_roles_with_override_on_context(context $context) {
  3711. global $DB;
  3712. return $DB->get_records_sql("SELECT r.*
  3713. FROM {role_capabilities} rc, {role} r
  3714. WHERE rc.roleid = r.id AND rc.contextid = ?",
  3715. array($context->id));
  3716. }
  3717. /**
  3718. * Get all capabilities for this role on this context (overrides)
  3719. *
  3720. * @param stdClass $role
  3721. * @param context $context
  3722. * @return array
  3723. */
  3724. function get_capabilities_from_role_on_context($role, context $context) {
  3725. global $DB;
  3726. return $DB->get_records_sql("SELECT *
  3727. FROM {role_capabilities}
  3728. WHERE contextid = ? AND roleid = ?",
  3729. array($context->id, $role->id));
  3730. }
  3731. /**
  3732. * Find all user assignment of users for this role, on this context
  3733. *
  3734. * @param stdClass $role
  3735. * @param context $context
  3736. * @return array
  3737. */
  3738. function get_users_from_role_on_context($role, context $context) {
  3739. global $DB;
  3740. return $DB->get_records_sql("SELECT *
  3741. FROM {role_assignments}
  3742. WHERE contextid = ? AND roleid = ?",
  3743. array($context->id, $role->id));
  3744. }
  3745. /**
  3746. * Simple function returning a boolean true if user has roles
  3747. * in context or parent contexts, otherwise false.
  3748. *
  3749. * @param int $userid
  3750. * @param int $roleid
  3751. * @param int $contextid empty means any context
  3752. * @return bool
  3753. */
  3754. function user_has_role_assignment($userid, $roleid, $contextid = 0) {
  3755. global $DB;
  3756. if ($contextid) {
  3757. if (!$context = context::instance_by_id($contextid, IGNORE_MISSING)) {
  3758. return false;
  3759. }
  3760. $parents = $context->get_parent_context_ids(true);
  3761. list($contexts, $params) = $DB->get_in_or_equal($parents, SQL_PARAMS_NAMED, 'r');
  3762. $params['userid'] = $userid;
  3763. $params['roleid'] = $roleid;
  3764. $sql = "SELECT COUNT(ra.id)
  3765. FROM {role_assignments} ra
  3766. WHERE ra.userid = :userid AND ra.roleid = :roleid AND ra.contextid $contexts";
  3767. $count = $DB->get_field_sql($sql, $params);
  3768. return ($count > 0);
  3769. } else {
  3770. return $DB->record_exists('role_assignments', array('userid'=>$userid, 'roleid'=>$roleid));
  3771. }
  3772. }
  3773. /**
  3774. * Get localised role name or alias if exists and format the text.
  3775. *
  3776. * @param stdClass $role role object
  3777. * - optional 'coursealias' property should be included for performance reasons if course context used
  3778. * - description property is not required here
  3779. * @param context|bool $context empty means system context
  3780. * @param int $rolenamedisplay type of role name
  3781. * @return string localised role name or course role name alias
  3782. */
  3783. function role_get_name(stdClass $role, $context = null, $rolenamedisplay = ROLENAME_ALIAS) {
  3784. global $DB;
  3785. if ($rolenamedisplay == ROLENAME_SHORT) {
  3786. return $role->shortname;
  3787. }
  3788. if (!$context or !$coursecontext = $context->get_course_context(false)) {
  3789. $coursecontext = null;
  3790. }
  3791. if ($coursecontext and !property_exists($role, 'coursealias') and ($rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH or $rolenamedisplay == ROLENAME_ALIAS_RAW)) {
  3792. $role = clone($role); // Do not modify parameters.
  3793. if ($r = $DB->get_record('role_names', array('roleid'=>$role->id, 'contextid'=>$coursecontext->id))) {
  3794. $role->coursealias = $r->name;
  3795. } else {
  3796. $role->coursealias = null;
  3797. }
  3798. }
  3799. if ($rolenamedisplay == ROLENAME_ALIAS_RAW) {
  3800. if ($coursecontext) {
  3801. return $role->coursealias;
  3802. } else {
  3803. return null;
  3804. }
  3805. }
  3806. if (trim($role->name) !== '') {
  3807. // For filtering always use context where was the thing defined - system for roles here.
  3808. $original = format_string($role->name, true, array('context'=>context_system::instance()));
  3809. } else {
  3810. // Empty role->name means we want to see localised role name based on shortname,
  3811. // only default roles are supposed to be localised.
  3812. switch ($role->shortname) {
  3813. case 'manager': $original = get_string('manager', 'role'); break;
  3814. case 'coursecreator': $original = get_string('coursecreators'); break;
  3815. case 'editingteacher': $original = get_string('defaultcourseteacher'); break;
  3816. case 'teacher': $original = get_string('noneditingteacher'); break;
  3817. case 'student': $original = get_string('defaultcoursestudent'); break;
  3818. case 'guest': $original = get_string('guest'); break;
  3819. case 'user': $original = get_string('authenticateduser'); break;
  3820. case 'frontpage': $original = get_string('frontpageuser', 'role'); break;
  3821. // We should not get here, the role UI should require the name for custom roles!
  3822. default: $original = $role->shortname; break;
  3823. }
  3824. }
  3825. if ($rolenamedisplay == ROLENAME_ORIGINAL) {
  3826. return $original;
  3827. }
  3828. if ($rolenamedisplay == ROLENAME_ORIGINALANDSHORT) {
  3829. return "$original ($role->shortname)";
  3830. }
  3831. if ($rolenamedisplay == ROLENAME_ALIAS) {
  3832. if ($coursecontext and trim($role->coursealias) !== '') {
  3833. return format_string($role->coursealias, true, array('context'=>$coursecontext));
  3834. } else {
  3835. return $original;
  3836. }
  3837. }
  3838. if ($rolenamedisplay == ROLENAME_BOTH) {
  3839. if ($coursecontext and trim($role->coursealias) !== '') {
  3840. return format_string($role->coursealias, true, array('context'=>$coursecontext)) . " ($original)";
  3841. } else {
  3842. return $original;
  3843. }
  3844. }
  3845. throw new coding_exception('Invalid $rolenamedisplay parameter specified in role_get_name()');
  3846. }
  3847. /**
  3848. * Returns localised role description if available.
  3849. * If the name is empty it tries to find the default role name using
  3850. * hardcoded list of default role names or other methods in the future.
  3851. *
  3852. * @param stdClass $role
  3853. * @return string localised role name
  3854. */
  3855. function role_get_description(stdClass $role) {
  3856. if (!html_is_blank($role->description)) {
  3857. return format_text($role->description, FORMAT_HTML, array('context'=>context_system::instance()));
  3858. }
  3859. switch ($role->shortname) {
  3860. case 'manager': return get_string('managerdescription', 'role');
  3861. case 'coursecreator': return get_string('coursecreatorsdescription');
  3862. case 'editingteacher': return get_string('defaultcourseteacherdescription');
  3863. case 'teacher': return get_string('noneditingteacherdescription');
  3864. case 'student': return get_string('defaultcoursestudentdescription');
  3865. case 'guest': return get_string('guestdescription');
  3866. case 'user': return get_string('authenticateduserdescription');
  3867. case 'frontpage': return get_string('frontpageuserdescription', 'role');
  3868. default: return '';
  3869. }
  3870. }
  3871. /**
  3872. * Get all the localised role names for a context.
  3873. *
  3874. * In new installs default roles have empty names, this function
  3875. * add localised role names using current language pack.
  3876. *
  3877. * @param context $context the context, null means system context
  3878. * @param array of role objects with a ->localname field containing the context-specific role name.
  3879. * @param int $rolenamedisplay
  3880. * @param bool $returnmenu true means id=>localname, false means id=>rolerecord
  3881. * @return array Array of context-specific role names, or role objects with a ->localname field added.
  3882. */
  3883. function role_get_names(context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
  3884. return role_fix_names(get_all_roles($context), $context, $rolenamedisplay, $returnmenu);
  3885. }
  3886. /**
  3887. * Prepare list of roles for display, apply aliases and localise default role names.
  3888. *
  3889. * @param array $roleoptions array roleid => roleobject (with optional coursealias), strings are accepted for backwards compatibility only
  3890. * @param context $context the context, null means system context
  3891. * @param int $rolenamedisplay
  3892. * @param bool $returnmenu null means keep the same format as $roleoptions, true means id=>localname, false means id=>rolerecord
  3893. * @return array Array of context-specific role names, or role objects with a ->localname field added.
  3894. */
  3895. function role_fix_names($roleoptions, context $context = null, $rolenamedisplay = ROLENAME_ALIAS, $returnmenu = null) {
  3896. global $DB;
  3897. if (empty($roleoptions)) {
  3898. return array();
  3899. }
  3900. if (!$context or !$coursecontext = $context->get_course_context(false)) {
  3901. $coursecontext = null;
  3902. }
  3903. // We usually need all role columns...
  3904. $first = reset($roleoptions);
  3905. if ($returnmenu === null) {
  3906. $returnmenu = !is_object($first);
  3907. }
  3908. if (!is_object($first) or !property_exists($first, 'shortname')) {
  3909. $allroles = get_all_roles($context);
  3910. foreach ($roleoptions as $rid => $unused) {
  3911. $roleoptions[$rid] = $allroles[$rid];
  3912. }
  3913. }
  3914. // Inject coursealias if necessary.
  3915. if ($coursecontext and ($rolenamedisplay == ROLENAME_ALIAS_RAW or $rolenamedisplay == ROLENAME_ALIAS or $rolenamedisplay == ROLENAME_BOTH)) {
  3916. $first = reset($roleoptions);
  3917. if (!property_exists($first, 'coursealias')) {
  3918. $aliasnames = $DB->get_records('role_names', array('contextid'=>$coursecontext->id));
  3919. foreach ($aliasnames as $alias) {
  3920. if (isset($roleoptions[$alias->roleid])) {
  3921. $roleoptions[$alias->roleid]->coursealias = $alias->name;
  3922. }
  3923. }
  3924. }
  3925. }
  3926. // Add localname property.
  3927. foreach ($roleoptions as $rid => $role) {
  3928. $roleoptions[$rid]->localname = role_get_name($role, $coursecontext, $rolenamedisplay);
  3929. }
  3930. if (!$returnmenu) {
  3931. return $roleoptions;
  3932. }
  3933. $menu = array();
  3934. foreach ($roleoptions as $rid => $role) {
  3935. $menu[$rid] = $role->localname;
  3936. }
  3937. return $menu;
  3938. }
  3939. /**
  3940. * Aids in detecting if a new line is required when reading a new capability
  3941. *
  3942. * This function helps admin/roles/manage.php etc to detect if a new line should be printed
  3943. * when we read in a new capability.
  3944. * Most of the time, if the 2 components are different we should print a new line, (e.g. course system->rss client)
  3945. * but when we are in grade, all reports/import/export capabilities should be together
  3946. *
  3947. * @param string $cap component string a
  3948. * @param string $comp component string b
  3949. * @param int $contextlevel
  3950. * @return bool whether 2 component are in different "sections"
  3951. */
  3952. function component_level_changed($cap, $comp, $contextlevel) {
  3953. if (strstr($cap->component, '/') && strstr($comp, '/')) {
  3954. $compsa = explode('/', $cap->component);
  3955. $compsb = explode('/', $comp);
  3956. // list of system reports
  3957. if (($compsa[0] == 'report') && ($compsb[0] == 'report')) {
  3958. return false;
  3959. }
  3960. // we are in gradebook, still
  3961. if (($compsa[0] == 'gradeexport' || $compsa[0] == 'gradeimport' || $compsa[0] == 'gradereport') &&
  3962. ($compsb[0] == 'gradeexport' || $compsb[0] == 'gradeimport' || $compsb[0] == 'gradereport')) {
  3963. return false;
  3964. }
  3965. if (($compsa[0] == 'coursereport') && ($compsb[0] == 'coursereport')) {
  3966. return false;
  3967. }
  3968. }
  3969. return ($cap->component != $comp || $cap->contextlevel != $contextlevel);
  3970. }
  3971. /**
  3972. * Fix the roles.sortorder field in the database, so it contains sequential integers,
  3973. * and return an array of roleids in order.
  3974. *
  3975. * @param array $allroles array of roles, as returned by get_all_roles();
  3976. * @return array $role->sortorder =-> $role->id with the keys in ascending order.
  3977. */
  3978. function fix_role_sortorder($allroles) {
  3979. global $DB;
  3980. $rolesort = array();
  3981. $i = 0;
  3982. foreach ($allroles as $role) {
  3983. $rolesort[$i] = $role->id;
  3984. if ($role->sortorder != $i) {
  3985. $r = new stdClass();
  3986. $r->id = $role->id;
  3987. $r->sortorder = $i;
  3988. $DB->update_record('role', $r);
  3989. $allroles[$role->id]->sortorder = $i;
  3990. }
  3991. $i++;
  3992. }
  3993. return $rolesort;
  3994. }
  3995. /**
  3996. * Switch the sort order of two roles (used in admin/roles/manage.php).
  3997. *
  3998. * @param stdClass $first The first role. Actually, only ->sortorder is used.
  3999. * @param stdClass $second The second role. Actually, only ->sortorder is used.
  4000. * @return boolean success or failure
  4001. */
  4002. function switch_roles($first, $second) {
  4003. global $DB;
  4004. $temp = $DB->get_field('role', 'MAX(sortorder) + 1', array());
  4005. $result = $DB->set_field('role', 'sortorder', $temp, array('sortorder' => $first->sortorder));
  4006. $result = $result && $DB->set_field('role', 'sortorder', $first->sortorder, array('sortorder' => $second->sortorder));
  4007. $result = $result && $DB->set_field('role', 'sortorder', $second->sortorder, array('sortorder' => $temp));
  4008. return $result;
  4009. }
  4010. /**
  4011. * Duplicates all the base definitions of a role
  4012. *
  4013. * @param stdClass $sourcerole role to copy from
  4014. * @param int $targetrole id of role to copy to
  4015. */
  4016. function role_cap_duplicate($sourcerole, $targetrole) {
  4017. global $DB;
  4018. $systemcontext = context_system::instance();
  4019. $caps = $DB->get_records_sql("SELECT *
  4020. FROM {role_capabilities}
  4021. WHERE roleid = ? AND contextid = ?",
  4022. array($sourcerole->id, $systemcontext->id));
  4023. // adding capabilities
  4024. foreach ($caps as $cap) {
  4025. unset($cap->id);
  4026. $cap->roleid = $targetrole;
  4027. $DB->insert_record('role_capabilities', $cap);
  4028. }
  4029. // Reset any cache of this role, including MUC.
  4030. accesslib_clear_role_cache($targetrole);
  4031. }
  4032. /**
  4033. * Returns two lists, this can be used to find out if user has capability.
  4034. * Having any needed role and no forbidden role in this context means
  4035. * user has this capability in this context.
  4036. * Use get_role_names_with_cap_in_context() if you need role names to display in the UI
  4037. *
  4038. * @param stdClass $context
  4039. * @param string $capability
  4040. * @return array($neededroles, $forbiddenroles)
  4041. */
  4042. function get_roles_with_cap_in_context($context, $capability) {
  4043. global $DB;
  4044. $ctxids = trim($context->path, '/'); // kill leading slash
  4045. $ctxids = str_replace('/', ',', $ctxids);
  4046. $sql = "SELECT rc.id, rc.roleid, rc.permission, ctx.depth
  4047. FROM {role_capabilities} rc
  4048. JOIN {context} ctx ON ctx.id = rc.contextid
  4049. JOIN {capabilities} cap ON rc.capability = cap.name
  4050. WHERE rc.capability = :cap AND ctx.id IN ($ctxids)
  4051. ORDER BY rc.roleid ASC, ctx.depth DESC";
  4052. $params = array('cap'=>$capability);
  4053. if (!$capdefs = $DB->get_records_sql($sql, $params)) {
  4054. // no cap definitions --> no capability
  4055. return array(array(), array());
  4056. }
  4057. $forbidden = array();
  4058. $needed = array();
  4059. foreach ($capdefs as $def) {
  4060. if (isset($forbidden[$def->roleid])) {
  4061. continue;
  4062. }
  4063. if ($def->permission == CAP_PROHIBIT) {
  4064. $forbidden[$def->roleid] = $def->roleid;
  4065. unset($needed[$def->roleid]);
  4066. continue;
  4067. }
  4068. if (!isset($needed[$def->roleid])) {
  4069. if ($def->permission == CAP_ALLOW) {
  4070. $needed[$def->roleid] = true;
  4071. } else if ($def->permission == CAP_PREVENT) {
  4072. $needed[$def->roleid] = false;
  4073. }
  4074. }
  4075. }
  4076. unset($capdefs);
  4077. // remove all those roles not allowing
  4078. foreach ($needed as $key=>$value) {
  4079. if (!$value) {
  4080. unset($needed[$key]);
  4081. } else {
  4082. $needed[$key] = $key;
  4083. }
  4084. }
  4085. return array($needed, $forbidden);
  4086. }
  4087. /**
  4088. * Returns an array of role IDs that have ALL of the the supplied capabilities
  4089. * Uses get_roles_with_cap_in_context(). Returns $allowed minus $forbidden
  4090. *
  4091. * @param stdClass $context
  4092. * @param array $capabilities An array of capabilities
  4093. * @return array of roles with all of the required capabilities
  4094. */
  4095. function get_roles_with_caps_in_context($context, $capabilities) {
  4096. $neededarr = array();
  4097. $forbiddenarr = array();
  4098. foreach ($capabilities as $caprequired) {
  4099. list($neededarr[], $forbiddenarr[]) = get_roles_with_cap_in_context($context, $caprequired);
  4100. }
  4101. $rolesthatcanrate = array();
  4102. if (!empty($neededarr)) {
  4103. foreach ($neededarr as $needed) {
  4104. if (empty($rolesthatcanrate)) {
  4105. $rolesthatcanrate = $needed;
  4106. } else {
  4107. //only want roles that have all caps
  4108. $rolesthatcanrate = array_intersect_key($rolesthatcanrate,$needed);
  4109. }
  4110. }
  4111. }
  4112. if (!empty($forbiddenarr) && !empty($rolesthatcanrate)) {
  4113. foreach ($forbiddenarr as $forbidden) {
  4114. //remove any roles that are forbidden any of the caps
  4115. $rolesthatcanrate = array_diff($rolesthatcanrate, $forbidden);
  4116. }
  4117. }
  4118. return $rolesthatcanrate;
  4119. }
  4120. /**
  4121. * Returns an array of role names that have ALL of the the supplied capabilities
  4122. * Uses get_roles_with_caps_in_context(). Returns $allowed minus $forbidden
  4123. *
  4124. * @param stdClass $context
  4125. * @param array $capabilities An array of capabilities
  4126. * @return array of roles with all of the required capabilities
  4127. */
  4128. function get_role_names_with_caps_in_context($context, $capabilities) {
  4129. global $DB;
  4130. $rolesthatcanrate = get_roles_with_caps_in_context($context, $capabilities);
  4131. $allroles = $DB->get_records('role', null, 'sortorder DESC');
  4132. $roles = array();
  4133. foreach ($rolesthatcanrate as $r) {
  4134. $roles[$r] = $allroles[$r];
  4135. }
  4136. return role_fix_names($roles, $context, ROLENAME_ALIAS, true);
  4137. }
  4138. /**
  4139. * This function verifies the prohibit comes from this context
  4140. * and there are no more prohibits in parent contexts.
  4141. *
  4142. * @param int $roleid
  4143. * @param context $context
  4144. * @param string $capability name
  4145. * @return bool
  4146. */
  4147. function prohibit_is_removable($roleid, context $context, $capability) {
  4148. global $DB;
  4149. $ctxids = trim($context->path, '/'); // kill leading slash
  4150. $ctxids = str_replace('/', ',', $ctxids);
  4151. $params = array('roleid'=>$roleid, 'cap'=>$capability, 'prohibit'=>CAP_PROHIBIT);
  4152. $sql = "SELECT ctx.id
  4153. FROM {role_capabilities} rc
  4154. JOIN {context} ctx ON ctx.id = rc.contextid
  4155. JOIN {capabilities} cap ON rc.capability = cap.name
  4156. WHERE rc.roleid = :roleid AND rc.permission = :prohibit AND rc.capability = :cap AND ctx.id IN ($ctxids)
  4157. ORDER BY ctx.depth DESC";
  4158. if (!$prohibits = $DB->get_records_sql($sql, $params)) {
  4159. // no prohibits == nothing to remove
  4160. return true;
  4161. }
  4162. if (count($prohibits) > 1) {
  4163. // more prohibits can not be removed
  4164. return false;
  4165. }
  4166. return !empty($prohibits[$context->id]);
  4167. }
  4168. /**
  4169. * More user friendly role permission changing,
  4170. * it should produce as few overrides as possible.
  4171. *
  4172. * @param int $roleid
  4173. * @param stdClass $context
  4174. * @param string $capname capability name
  4175. * @param int $permission
  4176. * @return void
  4177. */
  4178. function role_change_permission($roleid, $context, $capname, $permission) {
  4179. global $DB;
  4180. if ($permission == CAP_INHERIT) {
  4181. unassign_capability($capname, $roleid, $context->id);
  4182. return;
  4183. }
  4184. $ctxids = trim($context->path, '/'); // kill leading slash
  4185. $ctxids = str_replace('/', ',', $ctxids);
  4186. $params = array('roleid'=>$roleid, 'cap'=>$capname);
  4187. $sql = "SELECT ctx.id, rc.permission, ctx.depth
  4188. FROM {role_capabilities} rc
  4189. JOIN {context} ctx ON ctx.id = rc.contextid
  4190. JOIN {capabilities} cap ON rc.capability = cap.name
  4191. WHERE rc.roleid = :roleid AND rc.capability = :cap AND ctx.id IN ($ctxids)
  4192. ORDER BY ctx.depth DESC";
  4193. if ($existing = $DB->get_records_sql($sql, $params)) {
  4194. foreach ($existing as $e) {
  4195. if ($e->permission == CAP_PROHIBIT) {
  4196. // prohibit can not be overridden, no point in changing anything
  4197. return;
  4198. }
  4199. }
  4200. $lowest = array_shift($existing);
  4201. if ($lowest->permission == $permission) {
  4202. // permission already set in this context or parent - nothing to do
  4203. return;
  4204. }
  4205. if ($existing) {
  4206. $parent = array_shift($existing);
  4207. if ($parent->permission == $permission) {
  4208. // permission already set in parent context or parent - just unset in this context
  4209. // we do this because we want as few overrides as possible for performance reasons
  4210. unassign_capability($capname, $roleid, $context->id);
  4211. return;
  4212. }
  4213. }
  4214. } else {
  4215. if ($permission == CAP_PREVENT) {
  4216. // nothing means role does not have permission
  4217. return;
  4218. }
  4219. }
  4220. // assign the needed capability
  4221. assign_capability($capname, $permission, $roleid, $context->id, true);
  4222. }
  4223. /**
  4224. * Basic moodle context abstraction class.
  4225. *
  4226. * Google confirms that no other important framework is using "context" class,
  4227. * we could use something else like mcontext or moodle_context, but we need to type
  4228. * this very often which would be annoying and it would take too much space...
  4229. *
  4230. * This class is derived from stdClass for backwards compatibility with
  4231. * odl $context record that was returned from DML $DB->get_record()
  4232. *
  4233. * @package core_access
  4234. * @category access
  4235. * @copyright Petr Skoda {@link http://skodak.org}
  4236. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4237. * @since Moodle 2.2
  4238. *
  4239. * @property-read int $id context id
  4240. * @property-read int $contextlevel CONTEXT_SYSTEM, CONTEXT_COURSE, etc.
  4241. * @property-read int $instanceid id of related instance in each context
  4242. * @property-read string $path path to context, starts with system context
  4243. * @property-read int $depth
  4244. */
  4245. abstract class context extends stdClass implements IteratorAggregate {
  4246. /**
  4247. * The context id
  4248. * Can be accessed publicly through $context->id
  4249. * @var int
  4250. */
  4251. protected $_id;
  4252. /**
  4253. * The context level
  4254. * Can be accessed publicly through $context->contextlevel
  4255. * @var int One of CONTEXT_* e.g. CONTEXT_COURSE, CONTEXT_MODULE
  4256. */
  4257. protected $_contextlevel;
  4258. /**
  4259. * Id of the item this context is related to e.g. COURSE_CONTEXT => course.id
  4260. * Can be accessed publicly through $context->instanceid
  4261. * @var int
  4262. */
  4263. protected $_instanceid;
  4264. /**
  4265. * The path to the context always starting from the system context
  4266. * Can be accessed publicly through $context->path
  4267. * @var string
  4268. */
  4269. protected $_path;
  4270. /**
  4271. * The depth of the context in relation to parent contexts
  4272. * Can be accessed publicly through $context->depth
  4273. * @var int
  4274. */
  4275. protected $_depth;
  4276. /**
  4277. * Whether this context is locked or not.
  4278. *
  4279. * Can be accessed publicly through $context->locked.
  4280. *
  4281. * @var int
  4282. */
  4283. protected $_locked;
  4284. /**
  4285. * @var array Context caching info
  4286. */
  4287. private static $cache_contextsbyid = array();
  4288. /**
  4289. * @var array Context caching info
  4290. */
  4291. private static $cache_contexts = array();
  4292. /**
  4293. * Context count
  4294. * Why do we do count contexts? Because count($array) is horribly slow for large arrays
  4295. * @var int
  4296. */
  4297. protected static $cache_count = 0;
  4298. /**
  4299. * @var array Context caching info
  4300. */
  4301. protected static $cache_preloaded = array();
  4302. /**
  4303. * @var context_system The system context once initialised
  4304. */
  4305. protected static $systemcontext = null;
  4306. /**
  4307. * Resets the cache to remove all data.
  4308. * @static
  4309. */
  4310. protected static function reset_caches() {
  4311. self::$cache_contextsbyid = array();
  4312. self::$cache_contexts = array();
  4313. self::$cache_count = 0;
  4314. self::$cache_preloaded = array();
  4315. self::$systemcontext = null;
  4316. }
  4317. /**
  4318. * Adds a context to the cache. If the cache is full, discards a batch of
  4319. * older entries.
  4320. *
  4321. * @static
  4322. * @param context $context New context to add
  4323. * @return void
  4324. */
  4325. protected static function cache_add(context $context) {
  4326. if (isset(self::$cache_contextsbyid[$context->id])) {
  4327. // already cached, no need to do anything - this is relatively cheap, we do all this because count() is slow
  4328. return;
  4329. }
  4330. if (self::$cache_count >= CONTEXT_CACHE_MAX_SIZE) {
  4331. $i = 0;
  4332. foreach (self::$cache_contextsbyid as $ctx) {
  4333. $i++;
  4334. if ($i <= 100) {
  4335. // we want to keep the first contexts to be loaded on this page, hopefully they will be needed again later
  4336. continue;
  4337. }
  4338. if ($i > (CONTEXT_CACHE_MAX_SIZE / 3)) {
  4339. // we remove oldest third of the contexts to make room for more contexts
  4340. break;
  4341. }
  4342. unset(self::$cache_contextsbyid[$ctx->id]);
  4343. unset(self::$cache_contexts[$ctx->contextlevel][$ctx->instanceid]);
  4344. self::$cache_count--;
  4345. }
  4346. }
  4347. self::$cache_contexts[$context->contextlevel][$context->instanceid] = $context;
  4348. self::$cache_contextsbyid[$context->id] = $context;
  4349. self::$cache_count++;
  4350. }
  4351. /**
  4352. * Removes a context from the cache.
  4353. *
  4354. * @static
  4355. * @param context $context Context object to remove
  4356. * @return void
  4357. */
  4358. protected static function cache_remove(context $context) {
  4359. if (!isset(self::$cache_contextsbyid[$context->id])) {
  4360. // not cached, no need to do anything - this is relatively cheap, we do all this because count() is slow
  4361. return;
  4362. }
  4363. unset(self::$cache_contexts[$context->contextlevel][$context->instanceid]);
  4364. unset(self::$cache_contextsbyid[$context->id]);
  4365. self::$cache_count--;
  4366. if (self::$cache_count < 0) {
  4367. self::$cache_count = 0;
  4368. }
  4369. }
  4370. /**
  4371. * Gets a context from the cache.
  4372. *
  4373. * @static
  4374. * @param int $contextlevel Context level
  4375. * @param int $instance Instance ID
  4376. * @return context|bool Context or false if not in cache
  4377. */
  4378. protected static function cache_get($contextlevel, $instance) {
  4379. if (isset(self::$cache_contexts[$contextlevel][$instance])) {
  4380. return self::$cache_contexts[$contextlevel][$instance];
  4381. }
  4382. return false;
  4383. }
  4384. /**
  4385. * Gets a context from the cache based on its id.
  4386. *
  4387. * @static
  4388. * @param int $id Context ID
  4389. * @return context|bool Context or false if not in cache
  4390. */
  4391. protected static function cache_get_by_id($id) {
  4392. if (isset(self::$cache_contextsbyid[$id])) {
  4393. return self::$cache_contextsbyid[$id];
  4394. }
  4395. return false;
  4396. }
  4397. /**
  4398. * Preloads context information from db record and strips the cached info.
  4399. *
  4400. * @static
  4401. * @param stdClass $rec
  4402. * @return void (modifies $rec)
  4403. */
  4404. protected static function preload_from_record(stdClass $rec) {
  4405. $notenoughdata = false;
  4406. $notenoughdata = $notenoughdata || empty($rec->ctxid);
  4407. $notenoughdata = $notenoughdata || empty($rec->ctxlevel);
  4408. $notenoughdata = $notenoughdata || !isset($rec->ctxinstance);
  4409. $notenoughdata = $notenoughdata || empty($rec->ctxpath);
  4410. $notenoughdata = $notenoughdata || empty($rec->ctxdepth);
  4411. $notenoughdata = $notenoughdata || !isset($rec->ctxlocked);
  4412. if ($notenoughdata) {
  4413. // The record does not have enough data, passed here repeatedly or context does not exist yet.
  4414. if (isset($rec->ctxid) && !isset($rec->ctxlocked)) {
  4415. debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
  4416. }
  4417. return;
  4418. }
  4419. $record = (object) [
  4420. 'id' => $rec->ctxid,
  4421. 'contextlevel' => $rec->ctxlevel,
  4422. 'instanceid' => $rec->ctxinstance,
  4423. 'path' => $rec->ctxpath,
  4424. 'depth' => $rec->ctxdepth,
  4425. 'locked' => $rec->ctxlocked,
  4426. ];
  4427. unset($rec->ctxid);
  4428. unset($rec->ctxlevel);
  4429. unset($rec->ctxinstance);
  4430. unset($rec->ctxpath);
  4431. unset($rec->ctxdepth);
  4432. unset($rec->ctxlocked);
  4433. return context::create_instance_from_record($record);
  4434. }
  4435. // ====== magic methods =======
  4436. /**
  4437. * Magic setter method, we do not want anybody to modify properties from the outside
  4438. * @param string $name
  4439. * @param mixed $value
  4440. */
  4441. public function __set($name, $value) {
  4442. debugging('Can not change context instance properties!');
  4443. }
  4444. /**
  4445. * Magic method getter, redirects to read only values.
  4446. * @param string $name
  4447. * @return mixed
  4448. */
  4449. public function __get($name) {
  4450. switch ($name) {
  4451. case 'id':
  4452. return $this->_id;
  4453. case 'contextlevel':
  4454. return $this->_contextlevel;
  4455. case 'instanceid':
  4456. return $this->_instanceid;
  4457. case 'path':
  4458. return $this->_path;
  4459. case 'depth':
  4460. return $this->_depth;
  4461. case 'locked':
  4462. return $this->is_locked();
  4463. default:
  4464. debugging('Invalid context property accessed! '.$name);
  4465. return null;
  4466. }
  4467. }
  4468. /**
  4469. * Full support for isset on our magic read only properties.
  4470. * @param string $name
  4471. * @return bool
  4472. */
  4473. public function __isset($name) {
  4474. switch ($name) {
  4475. case 'id':
  4476. return isset($this->_id);
  4477. case 'contextlevel':
  4478. return isset($this->_contextlevel);
  4479. case 'instanceid':
  4480. return isset($this->_instanceid);
  4481. case 'path':
  4482. return isset($this->_path);
  4483. case 'depth':
  4484. return isset($this->_depth);
  4485. case 'locked':
  4486. // Locked is always set.
  4487. return true;
  4488. default:
  4489. return false;
  4490. }
  4491. }
  4492. /**
  4493. * All properties are read only, sorry.
  4494. * @param string $name
  4495. */
  4496. public function __unset($name) {
  4497. debugging('Can not unset context instance properties!');
  4498. }
  4499. // ====== implementing method from interface IteratorAggregate ======
  4500. /**
  4501. * Create an iterator because magic vars can't be seen by 'foreach'.
  4502. *
  4503. * Now we can convert context object to array using convert_to_array(),
  4504. * and feed it properly to json_encode().
  4505. */
  4506. public function getIterator() {
  4507. $ret = array(
  4508. 'id' => $this->id,
  4509. 'contextlevel' => $this->contextlevel,
  4510. 'instanceid' => $this->instanceid,
  4511. 'path' => $this->path,
  4512. 'depth' => $this->depth,
  4513. 'locked' => $this->locked,
  4514. );
  4515. return new ArrayIterator($ret);
  4516. }
  4517. // ====== general context methods ======
  4518. /**
  4519. * Constructor is protected so that devs are forced to
  4520. * use context_xxx::instance() or context::instance_by_id().
  4521. *
  4522. * @param stdClass $record
  4523. */
  4524. protected function __construct(stdClass $record) {
  4525. $this->_id = (int)$record->id;
  4526. $this->_contextlevel = (int)$record->contextlevel;
  4527. $this->_instanceid = $record->instanceid;
  4528. $this->_path = $record->path;
  4529. $this->_depth = $record->depth;
  4530. if (isset($record->locked)) {
  4531. $this->_locked = $record->locked;
  4532. } else if (!during_initial_install() && !moodle_needs_upgrading()) {
  4533. debugging('Locked value missing. Code is possibly not usings the getter properly.', DEBUG_DEVELOPER);
  4534. }
  4535. }
  4536. /**
  4537. * This function is also used to work around 'protected' keyword problems in context_helper.
  4538. * @static
  4539. * @param stdClass $record
  4540. * @return context instance
  4541. */
  4542. protected static function create_instance_from_record(stdClass $record) {
  4543. $classname = context_helper::get_class_for_level($record->contextlevel);
  4544. if ($context = context::cache_get_by_id($record->id)) {
  4545. return $context;
  4546. }
  4547. $context = new $classname($record);
  4548. context::cache_add($context);
  4549. return $context;
  4550. }
  4551. /**
  4552. * Copy prepared new contexts from temp table to context table,
  4553. * we do this in db specific way for perf reasons only.
  4554. * @static
  4555. */
  4556. protected static function merge_context_temp_table() {
  4557. global $DB;
  4558. /* MDL-11347:
  4559. * - mysql does not allow to use FROM in UPDATE statements
  4560. * - using two tables after UPDATE works in mysql, but might give unexpected
  4561. * results in pg 8 (depends on configuration)
  4562. * - using table alias in UPDATE does not work in pg < 8.2
  4563. *
  4564. * Different code for each database - mostly for performance reasons
  4565. */
  4566. $dbfamily = $DB->get_dbfamily();
  4567. if ($dbfamily == 'mysql') {
  4568. $updatesql = "UPDATE {context} ct, {context_temp} temp
  4569. SET ct.path = temp.path,
  4570. ct.depth = temp.depth,
  4571. ct.locked = temp.locked
  4572. WHERE ct.id = temp.id";
  4573. } else if ($dbfamily == 'oracle') {
  4574. $updatesql = "UPDATE {context} ct
  4575. SET (ct.path, ct.depth, ct.locked) =
  4576. (SELECT temp.path, temp.depth, temp.locked
  4577. FROM {context_temp} temp
  4578. WHERE temp.id=ct.id)
  4579. WHERE EXISTS (SELECT 'x'
  4580. FROM {context_temp} temp
  4581. WHERE temp.id = ct.id)";
  4582. } else if ($dbfamily == 'postgres' or $dbfamily == 'mssql') {
  4583. $updatesql = "UPDATE {context}
  4584. SET path = temp.path,
  4585. depth = temp.depth,
  4586. locked = temp.locked
  4587. FROM {context_temp} temp
  4588. WHERE temp.id={context}.id";
  4589. } else {
  4590. // sqlite and others
  4591. $updatesql = "UPDATE {context}
  4592. SET path = (SELECT path FROM {context_temp} WHERE id = {context}.id),
  4593. depth = (SELECT depth FROM {context_temp} WHERE id = {context}.id),
  4594. locked = (SELECT locked FROM {context_temp} WHERE id = {context}.id)
  4595. WHERE id IN (SELECT id FROM {context_temp})";
  4596. }
  4597. $DB->execute($updatesql);
  4598. }
  4599. /**
  4600. * Get a context instance as an object, from a given context id.
  4601. *
  4602. * @static
  4603. * @param int $id context id
  4604. * @param int $strictness IGNORE_MISSING means compatible mode, false returned if record not found, debug message if more found;
  4605. * MUST_EXIST means throw exception if no record found
  4606. * @return context|bool the context object or false if not found
  4607. */
  4608. public static function instance_by_id($id, $strictness = MUST_EXIST) {
  4609. global $DB;
  4610. if (get_called_class() !== 'context' and get_called_class() !== 'context_helper') {
  4611. // some devs might confuse context->id and instanceid, better prevent these mistakes completely
  4612. throw new coding_exception('use only context::instance_by_id() for real context levels use ::instance() methods');
  4613. }
  4614. if ($id == SYSCONTEXTID) {
  4615. return context_system::instance(0, $strictness);
  4616. }
  4617. if (is_array($id) or is_object($id) or empty($id)) {
  4618. throw new coding_exception('Invalid context id specified context::instance_by_id()');
  4619. }
  4620. if ($context = context::cache_get_by_id($id)) {
  4621. return $context;
  4622. }
  4623. if ($record = $DB->get_record('context', array('id'=>$id), '*', $strictness)) {
  4624. return context::create_instance_from_record($record);
  4625. }
  4626. return false;
  4627. }
  4628. /**
  4629. * Update context info after moving context in the tree structure.
  4630. *
  4631. * @param context $newparent
  4632. * @return void
  4633. */
  4634. public function update_moved(context $newparent) {
  4635. global $DB;
  4636. $frompath = $this->_path;
  4637. $newpath = $newparent->path . '/' . $this->_id;
  4638. $trans = $DB->start_delegated_transaction();
  4639. $setdepth = '';
  4640. if (($newparent->depth +1) != $this->_depth) {
  4641. $diff = $newparent->depth - $this->_depth + 1;
  4642. $setdepth = ", depth = depth + $diff";
  4643. }
  4644. $sql = "UPDATE {context}
  4645. SET path = ?
  4646. $setdepth
  4647. WHERE id = ?";
  4648. $params = array($newpath, $this->_id);
  4649. $DB->execute($sql, $params);
  4650. $this->_path = $newpath;
  4651. $this->_depth = $newparent->depth + 1;
  4652. $sql = "UPDATE {context}
  4653. SET path = ".$DB->sql_concat("?", $DB->sql_substr("path", strlen($frompath)+1))."
  4654. $setdepth
  4655. WHERE path LIKE ?";
  4656. $params = array($newpath, "{$frompath}/%");
  4657. $DB->execute($sql, $params);
  4658. $this->mark_dirty();
  4659. context::reset_caches();
  4660. $trans->allow_commit();
  4661. }
  4662. /**
  4663. * Set whether this context has been locked or not.
  4664. *
  4665. * @param bool $locked
  4666. * @return $this
  4667. */
  4668. public function set_locked(bool $locked) {
  4669. global $DB;
  4670. if ($this->_locked == $locked) {
  4671. return $this;
  4672. }
  4673. $this->_locked = $locked;
  4674. $DB->set_field('context', 'locked', (int) $locked, ['id' => $this->id]);
  4675. $this->mark_dirty();
  4676. if ($locked) {
  4677. $eventname = '\\core\\event\\context_locked';
  4678. } else {
  4679. $eventname = '\\core\\event\\context_unlocked';
  4680. }
  4681. $event = $eventname::create(['context' => $this, 'objectid' => $this->id]);
  4682. $event->trigger();
  4683. self::reset_caches();
  4684. return $this;
  4685. }
  4686. /**
  4687. * Remove all context path info and optionally rebuild it.
  4688. *
  4689. * @param bool $rebuild
  4690. * @return void
  4691. */
  4692. public function reset_paths($rebuild = true) {
  4693. global $DB;
  4694. if ($this->_path) {
  4695. $this->mark_dirty();
  4696. }
  4697. $DB->set_field_select('context', 'depth', 0, "path LIKE '%/$this->_id/%'");
  4698. $DB->set_field_select('context', 'path', NULL, "path LIKE '%/$this->_id/%'");
  4699. if ($this->_contextlevel != CONTEXT_SYSTEM) {
  4700. $DB->set_field('context', 'depth', 0, array('id'=>$this->_id));
  4701. $DB->set_field('context', 'path', NULL, array('id'=>$this->_id));
  4702. $this->_depth = 0;
  4703. $this->_path = null;
  4704. }
  4705. if ($rebuild) {
  4706. context_helper::build_all_paths(false);
  4707. }
  4708. context::reset_caches();
  4709. }
  4710. /**
  4711. * Delete all data linked to content, do not delete the context record itself
  4712. */
  4713. public function delete_content() {
  4714. global $CFG, $DB;
  4715. blocks_delete_all_for_context($this->_id);
  4716. filter_delete_all_for_context($this->_id);
  4717. require_once($CFG->dirroot . '/comment/lib.php');
  4718. comment::delete_comments(array('contextid'=>$this->_id));
  4719. require_once($CFG->dirroot.'/rating/lib.php');
  4720. $delopt = new stdclass();
  4721. $delopt->contextid = $this->_id;
  4722. $rm = new rating_manager();
  4723. $rm->delete_ratings($delopt);
  4724. // delete all files attached to this context
  4725. $fs = get_file_storage();
  4726. $fs->delete_area_files($this->_id);
  4727. // Delete all repository instances attached to this context.
  4728. require_once($CFG->dirroot . '/repository/lib.php');
  4729. repository::delete_all_for_context($this->_id);
  4730. // delete all advanced grading data attached to this context
  4731. require_once($CFG->dirroot.'/grade/grading/lib.php');
  4732. grading_manager::delete_all_for_context($this->_id);
  4733. // now delete stuff from role related tables, role_unassign_all
  4734. // and unenrol should be called earlier to do proper cleanup
  4735. $DB->delete_records('role_assignments', array('contextid'=>$this->_id));
  4736. $DB->delete_records('role_names', array('contextid'=>$this->_id));
  4737. $this->delete_capabilities();
  4738. }
  4739. /**
  4740. * Unassign all capabilities from a context.
  4741. */
  4742. public function delete_capabilities() {
  4743. global $DB;
  4744. $ids = $DB->get_fieldset_select('role_capabilities', 'DISTINCT roleid', 'contextid = ?', array($this->_id));
  4745. if ($ids) {
  4746. $DB->delete_records('role_capabilities', array('contextid' => $this->_id));
  4747. // Reset any cache of these roles, including MUC.
  4748. accesslib_clear_role_cache($ids);
  4749. }
  4750. }
  4751. /**
  4752. * Delete the context content and the context record itself
  4753. */
  4754. public function delete() {
  4755. global $DB;
  4756. if ($this->_contextlevel <= CONTEXT_SYSTEM) {
  4757. throw new coding_exception('Cannot delete system context');
  4758. }
  4759. // double check the context still exists
  4760. if (!$DB->record_exists('context', array('id'=>$this->_id))) {
  4761. context::cache_remove($this);
  4762. return;
  4763. }
  4764. $this->delete_content();
  4765. $DB->delete_records('context', array('id'=>$this->_id));
  4766. // purge static context cache if entry present
  4767. context::cache_remove($this);
  4768. // Inform search engine to delete data related to this context.
  4769. \core_search\manager::context_deleted($this);
  4770. }
  4771. // ====== context level related methods ======
  4772. /**
  4773. * Utility method for context creation
  4774. *
  4775. * @static
  4776. * @param int $contextlevel
  4777. * @param int $instanceid
  4778. * @param string $parentpath
  4779. * @return stdClass context record
  4780. */
  4781. protected static function insert_context_record($contextlevel, $instanceid, $parentpath) {
  4782. global $DB;
  4783. $record = new stdClass();
  4784. $record->contextlevel = $contextlevel;
  4785. $record->instanceid = $instanceid;
  4786. $record->depth = 0;
  4787. $record->path = null; //not known before insert
  4788. $record->locked = 0;
  4789. $record->id = $DB->insert_record('context', $record);
  4790. // now add path if known - it can be added later
  4791. if (!is_null($parentpath)) {
  4792. $record->path = $parentpath.'/'.$record->id;
  4793. $record->depth = substr_count($record->path, '/');
  4794. $DB->update_record('context', $record);
  4795. }
  4796. return $record;
  4797. }
  4798. /**
  4799. * Returns human readable context identifier.
  4800. *
  4801. * @param boolean $withprefix whether to prefix the name of the context with the
  4802. * type of context, e.g. User, Course, Forum, etc.
  4803. * @param boolean $short whether to use the short name of the thing. Only applies
  4804. * to course contexts
  4805. * @return string the human readable context name.
  4806. */
  4807. public function get_context_name($withprefix = true, $short = false) {
  4808. // must be implemented in all context levels
  4809. throw new coding_exception('can not get name of abstract context');
  4810. }
  4811. /**
  4812. * Whether the current context is locked.
  4813. *
  4814. * @return bool
  4815. */
  4816. public function is_locked() {
  4817. if ($this->_locked) {
  4818. return true;
  4819. }
  4820. if ($parent = $this->get_parent_context()) {
  4821. return $parent->is_locked();
  4822. }
  4823. return false;
  4824. }
  4825. /**
  4826. * Returns the most relevant URL for this context.
  4827. *
  4828. * @return moodle_url
  4829. */
  4830. public abstract function get_url();
  4831. /**
  4832. * Returns array of relevant context capability records.
  4833. *
  4834. * @return array
  4835. */
  4836. public abstract function get_capabilities();
  4837. /**
  4838. * Recursive function which, given a context, find all its children context ids.
  4839. *
  4840. * For course category contexts it will return immediate children and all subcategory contexts.
  4841. * It will NOT recurse into courses or subcategories categories.
  4842. * If you want to do that, call it on the returned courses/categories.
  4843. *
  4844. * When called for a course context, it will return the modules and blocks
  4845. * displayed in the course page and blocks displayed on the module pages.
  4846. *
  4847. * If called on a user/course/module context it _will_ populate the cache with the appropriate
  4848. * contexts ;-)
  4849. *
  4850. * @return array Array of child records
  4851. */
  4852. public function get_child_contexts() {
  4853. global $DB;
  4854. if (empty($this->_path) or empty($this->_depth)) {
  4855. debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
  4856. return array();
  4857. }
  4858. $sql = "SELECT ctx.*
  4859. FROM {context} ctx
  4860. WHERE ctx.path LIKE ?";
  4861. $params = array($this->_path.'/%');
  4862. $records = $DB->get_records_sql($sql, $params);
  4863. $result = array();
  4864. foreach ($records as $record) {
  4865. $result[$record->id] = context::create_instance_from_record($record);
  4866. }
  4867. return $result;
  4868. }
  4869. /**
  4870. * Returns parent contexts of this context in reversed order, i.e. parent first,
  4871. * then grand parent, etc.
  4872. *
  4873. * @param bool $includeself true means include self too
  4874. * @return array of context instances
  4875. */
  4876. public function get_parent_contexts($includeself = false) {
  4877. if (!$contextids = $this->get_parent_context_ids($includeself)) {
  4878. return array();
  4879. }
  4880. // Preload the contexts to reduce DB calls.
  4881. context_helper::preload_contexts_by_id($contextids);
  4882. $result = array();
  4883. foreach ($contextids as $contextid) {
  4884. $parent = context::instance_by_id($contextid, MUST_EXIST);
  4885. $result[$parent->id] = $parent;
  4886. }
  4887. return $result;
  4888. }
  4889. /**
  4890. * Returns parent context ids of this context in reversed order, i.e. parent first,
  4891. * then grand parent, etc.
  4892. *
  4893. * @param bool $includeself true means include self too
  4894. * @return array of context ids
  4895. */
  4896. public function get_parent_context_ids($includeself = false) {
  4897. if (empty($this->_path)) {
  4898. return array();
  4899. }
  4900. $parentcontexts = trim($this->_path, '/'); // kill leading slash
  4901. $parentcontexts = explode('/', $parentcontexts);
  4902. if (!$includeself) {
  4903. array_pop($parentcontexts); // and remove its own id
  4904. }
  4905. return array_reverse($parentcontexts);
  4906. }
  4907. /**
  4908. * Returns parent context paths of this context.
  4909. *
  4910. * @param bool $includeself true means include self too
  4911. * @return array of context paths
  4912. */
  4913. public function get_parent_context_paths($includeself = false) {
  4914. if (empty($this->_path)) {
  4915. return array();
  4916. }
  4917. $contextids = explode('/', $this->_path);
  4918. $path = '';
  4919. $paths = array();
  4920. foreach ($contextids as $contextid) {
  4921. if ($contextid) {
  4922. $path .= '/' . $contextid;
  4923. $paths[$contextid] = $path;
  4924. }
  4925. }
  4926. if (!$includeself) {
  4927. unset($paths[$this->_id]);
  4928. }
  4929. return $paths;
  4930. }
  4931. /**
  4932. * Returns parent context
  4933. *
  4934. * @return context
  4935. */
  4936. public function get_parent_context() {
  4937. if (empty($this->_path) or $this->_id == SYSCONTEXTID) {
  4938. return false;
  4939. }
  4940. $parentcontexts = trim($this->_path, '/'); // kill leading slash
  4941. $parentcontexts = explode('/', $parentcontexts);
  4942. array_pop($parentcontexts); // self
  4943. $contextid = array_pop($parentcontexts); // immediate parent
  4944. return context::instance_by_id($contextid, MUST_EXIST);
  4945. }
  4946. /**
  4947. * Is this context part of any course? If yes return course context.
  4948. *
  4949. * @param bool $strict true means throw exception if not found, false means return false if not found
  4950. * @return context_course context of the enclosing course, null if not found or exception
  4951. */
  4952. public function get_course_context($strict = true) {
  4953. if ($strict) {
  4954. throw new coding_exception('Context does not belong to any course.');
  4955. } else {
  4956. return false;
  4957. }
  4958. }
  4959. /**
  4960. * Returns sql necessary for purging of stale context instances.
  4961. *
  4962. * @static
  4963. * @return string cleanup SQL
  4964. */
  4965. protected static function get_cleanup_sql() {
  4966. throw new coding_exception('get_cleanup_sql() method must be implemented in all context levels');
  4967. }
  4968. /**
  4969. * Rebuild context paths and depths at context level.
  4970. *
  4971. * @static
  4972. * @param bool $force
  4973. * @return void
  4974. */
  4975. protected static function build_paths($force) {
  4976. throw new coding_exception('build_paths() method must be implemented in all context levels');
  4977. }
  4978. /**
  4979. * Create missing context instances at given level
  4980. *
  4981. * @static
  4982. * @return void
  4983. */
  4984. protected static function create_level_instances() {
  4985. throw new coding_exception('create_level_instances() method must be implemented in all context levels');
  4986. }
  4987. /**
  4988. * Reset all cached permissions and definitions if the necessary.
  4989. * @return void
  4990. */
  4991. public function reload_if_dirty() {
  4992. global $ACCESSLIB_PRIVATE, $USER;
  4993. // Load dirty contexts list if needed
  4994. if (CLI_SCRIPT) {
  4995. if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
  4996. // we do not load dirty flags in CLI and cron
  4997. $ACCESSLIB_PRIVATE->dirtycontexts = array();
  4998. }
  4999. } else {
  5000. if (!isset($USER->access['time'])) {
  5001. // Nothing has been loaded yet, so we do not need to check dirty flags now.
  5002. return;
  5003. }
  5004. // From skodak: No idea why -2 is there, server cluster time difference maybe...
  5005. $changedsince = $USER->access['time'] - 2;
  5006. if (!isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
  5007. $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $changedsince);
  5008. }
  5009. if (!isset($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
  5010. $ACCESSLIB_PRIVATE->dirtyusers[$USER->id] = get_cache_flag('accesslib/dirtyusers', $USER->id, $changedsince);
  5011. }
  5012. }
  5013. $dirty = false;
  5014. if (!empty($ACCESSLIB_PRIVATE->dirtyusers[$USER->id])) {
  5015. $dirty = true;
  5016. } else if (!empty($ACCESSLIB_PRIVATE->dirtycontexts)) {
  5017. $paths = $this->get_parent_context_paths(true);
  5018. foreach ($paths as $path) {
  5019. if (isset($ACCESSLIB_PRIVATE->dirtycontexts[$path])) {
  5020. $dirty = true;
  5021. break;
  5022. }
  5023. }
  5024. }
  5025. if ($dirty) {
  5026. // Reload all capabilities of USER and others - preserving loginas, roleswitches, etc.
  5027. // Then cleanup any marks of dirtyness... at least from our short term memory!
  5028. reload_all_capabilities();
  5029. }
  5030. }
  5031. /**
  5032. * Mark a context as dirty (with timestamp) so as to force reloading of the context.
  5033. */
  5034. public function mark_dirty() {
  5035. global $CFG, $USER, $ACCESSLIB_PRIVATE;
  5036. if (during_initial_install()) {
  5037. return;
  5038. }
  5039. // only if it is a non-empty string
  5040. if (is_string($this->_path) && $this->_path !== '') {
  5041. set_cache_flag('accesslib/dirtycontexts', $this->_path, 1, time()+$CFG->sessiontimeout);
  5042. if (isset($ACCESSLIB_PRIVATE->dirtycontexts)) {
  5043. $ACCESSLIB_PRIVATE->dirtycontexts[$this->_path] = 1;
  5044. } else {
  5045. if (CLI_SCRIPT) {
  5046. $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
  5047. } else {
  5048. if (isset($USER->access['time'])) {
  5049. $ACCESSLIB_PRIVATE->dirtycontexts = get_cache_flags('accesslib/dirtycontexts', $USER->access['time']-2);
  5050. } else {
  5051. $ACCESSLIB_PRIVATE->dirtycontexts = array($this->_path => 1);
  5052. }
  5053. // flags not loaded yet, it will be done later in $context->reload_if_dirty()
  5054. }
  5055. }
  5056. }
  5057. }
  5058. }
  5059. /**
  5060. * Context maintenance and helper methods.
  5061. *
  5062. * This is "extends context" is a bloody hack that tires to work around the deficiencies
  5063. * in the "protected" keyword in PHP, this helps us to hide all the internals of context
  5064. * level implementation from the rest of code, the code completion returns what developers need.
  5065. *
  5066. * Thank you Tim Hunt for helping me with this nasty trick.
  5067. *
  5068. * @package core_access
  5069. * @category access
  5070. * @copyright Petr Skoda {@link http://skodak.org}
  5071. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5072. * @since Moodle 2.2
  5073. */
  5074. class context_helper extends context {
  5075. /**
  5076. * @var array An array mapping context levels to classes
  5077. */
  5078. private static $alllevels;
  5079. /**
  5080. * Instance does not make sense here, only static use
  5081. */
  5082. protected function __construct() {
  5083. }
  5084. /**
  5085. * Reset internal context levels array.
  5086. */
  5087. public static function reset_levels() {
  5088. self::$alllevels = null;
  5089. }
  5090. /**
  5091. * Initialise context levels, call before using self::$alllevels.
  5092. */
  5093. private static function init_levels() {
  5094. global $CFG;
  5095. if (isset(self::$alllevels)) {
  5096. return;
  5097. }
  5098. self::$alllevels = array(
  5099. CONTEXT_SYSTEM => 'context_system',
  5100. CONTEXT_USER => 'context_user',
  5101. CONTEXT_COURSECAT => 'context_coursecat',
  5102. CONTEXT_COURSE => 'context_course',
  5103. CONTEXT_MODULE => 'context_module',
  5104. CONTEXT_BLOCK => 'context_block',
  5105. );
  5106. if (empty($CFG->custom_context_classes)) {
  5107. return;
  5108. }
  5109. $levels = $CFG->custom_context_classes;
  5110. if (!is_array($levels)) {
  5111. $levels = @unserialize($levels);
  5112. }
  5113. if (!is_array($levels)) {
  5114. debugging('Invalid $CFG->custom_context_classes detected, value ignored.', DEBUG_DEVELOPER);
  5115. return;
  5116. }
  5117. // Unsupported custom levels, use with care!!!
  5118. foreach ($levels as $level => $classname) {
  5119. self::$alllevels[$level] = $classname;
  5120. }
  5121. ksort(self::$alllevels);
  5122. }
  5123. /**
  5124. * Returns a class name of the context level class
  5125. *
  5126. * @static
  5127. * @param int $contextlevel (CONTEXT_SYSTEM, etc.)
  5128. * @return string class name of the context class
  5129. */
  5130. public static function get_class_for_level($contextlevel) {
  5131. self::init_levels();
  5132. if (isset(self::$alllevels[$contextlevel])) {
  5133. return self::$alllevels[$contextlevel];
  5134. } else {
  5135. throw new coding_exception('Invalid context level specified');
  5136. }
  5137. }
  5138. /**
  5139. * Returns a list of all context levels
  5140. *
  5141. * @static
  5142. * @return array int=>string (level=>level class name)
  5143. */
  5144. public static function get_all_levels() {
  5145. self::init_levels();
  5146. return self::$alllevels;
  5147. }
  5148. /**
  5149. * Remove stale contexts that belonged to deleted instances.
  5150. * Ideally all code should cleanup contexts properly, unfortunately accidents happen...
  5151. *
  5152. * @static
  5153. * @return void
  5154. */
  5155. public static function cleanup_instances() {
  5156. global $DB;
  5157. self::init_levels();
  5158. $sqls = array();
  5159. foreach (self::$alllevels as $level=>$classname) {
  5160. $sqls[] = $classname::get_cleanup_sql();
  5161. }
  5162. $sql = implode(" UNION ", $sqls);
  5163. // it is probably better to use transactions, it might be faster too
  5164. $transaction = $DB->start_delegated_transaction();
  5165. $rs = $DB->get_recordset_sql($sql);
  5166. foreach ($rs as $record) {
  5167. $context = context::create_instance_from_record($record);
  5168. $context->delete();
  5169. }
  5170. $rs->close();
  5171. $transaction->allow_commit();
  5172. }
  5173. /**
  5174. * Create all context instances at the given level and above.
  5175. *
  5176. * @static
  5177. * @param int $contextlevel null means all levels
  5178. * @param bool $buildpaths
  5179. * @return void
  5180. */
  5181. public static function create_instances($contextlevel = null, $buildpaths = true) {
  5182. self::init_levels();
  5183. foreach (self::$alllevels as $level=>$classname) {
  5184. if ($contextlevel and $level > $contextlevel) {
  5185. // skip potential sub-contexts
  5186. continue;
  5187. }
  5188. $classname::create_level_instances();
  5189. if ($buildpaths) {
  5190. $classname::build_paths(false);
  5191. }
  5192. }
  5193. }
  5194. /**
  5195. * Rebuild paths and depths in all context levels.
  5196. *
  5197. * @static
  5198. * @param bool $force false means add missing only
  5199. * @return void
  5200. */
  5201. public static function build_all_paths($force = false) {
  5202. self::init_levels();
  5203. foreach (self::$alllevels as $classname) {
  5204. $classname::build_paths($force);
  5205. }
  5206. // reset static course cache - it might have incorrect cached data
  5207. accesslib_clear_all_caches(true);
  5208. }
  5209. /**
  5210. * Resets the cache to remove all data.
  5211. * @static
  5212. */
  5213. public static function reset_caches() {
  5214. context::reset_caches();
  5215. }
  5216. /**
  5217. * Returns all fields necessary for context preloading from user $rec.
  5218. *
  5219. * This helps with performance when dealing with hundreds of contexts.
  5220. *
  5221. * @static
  5222. * @param string $tablealias context table alias in the query
  5223. * @return array (table.column=>alias, ...)
  5224. */
  5225. public static function get_preload_record_columns($tablealias) {
  5226. return [
  5227. "$tablealias.id" => "ctxid",
  5228. "$tablealias.path" => "ctxpath",
  5229. "$tablealias.depth" => "ctxdepth",
  5230. "$tablealias.contextlevel" => "ctxlevel",
  5231. "$tablealias.instanceid" => "ctxinstance",
  5232. "$tablealias.locked" => "ctxlocked",
  5233. ];
  5234. }
  5235. /**
  5236. * Returns all fields necessary for context preloading from user $rec.
  5237. *
  5238. * This helps with performance when dealing with hundreds of contexts.
  5239. *
  5240. * @static
  5241. * @param string $tablealias context table alias in the query
  5242. * @return string
  5243. */
  5244. public static function get_preload_record_columns_sql($tablealias) {
  5245. return "$tablealias.id AS ctxid, " .
  5246. "$tablealias.path AS ctxpath, " .
  5247. "$tablealias.depth AS ctxdepth, " .
  5248. "$tablealias.contextlevel AS ctxlevel, " .
  5249. "$tablealias.instanceid AS ctxinstance, " .
  5250. "$tablealias.locked AS ctxlocked";
  5251. }
  5252. /**
  5253. * Preloads context information from db record and strips the cached info.
  5254. *
  5255. * The db request has to contain all columns from context_helper::get_preload_record_columns().
  5256. *
  5257. * @static
  5258. * @param stdClass $rec
  5259. * @return void (modifies $rec)
  5260. */
  5261. public static function preload_from_record(stdClass $rec) {
  5262. context::preload_from_record($rec);
  5263. }
  5264. /**
  5265. * Preload a set of contexts using their contextid.
  5266. *
  5267. * @param array $contextids
  5268. */
  5269. public static function preload_contexts_by_id(array $contextids) {
  5270. global $DB;
  5271. // Determine which contexts are not already cached.
  5272. $tofetch = [];
  5273. foreach ($contextids as $contextid) {
  5274. if (!self::cache_get_by_id($contextid)) {
  5275. $tofetch[] = $contextid;
  5276. }
  5277. }
  5278. if (count($tofetch) > 1) {
  5279. // There are at least two to fetch.
  5280. // There is no point only fetching a single context as this would be no more efficient than calling the existing code.
  5281. list($insql, $inparams) = $DB->get_in_or_equal($tofetch, SQL_PARAMS_NAMED);
  5282. $ctxs = $DB->get_records_select('context', "id {$insql}", $inparams, '',
  5283. \context_helper::get_preload_record_columns_sql('{context}'));
  5284. foreach ($ctxs as $ctx) {
  5285. self::preload_from_record($ctx);
  5286. }
  5287. }
  5288. }
  5289. /**
  5290. * Preload all contexts instances from course.
  5291. *
  5292. * To be used if you expect multiple queries for course activities...
  5293. *
  5294. * @static
  5295. * @param int $courseid
  5296. */
  5297. public static function preload_course($courseid) {
  5298. // Users can call this multiple times without doing any harm
  5299. if (isset(context::$cache_preloaded[$courseid])) {
  5300. return;
  5301. }
  5302. $coursecontext = context_course::instance($courseid);
  5303. $coursecontext->get_child_contexts();
  5304. context::$cache_preloaded[$courseid] = true;
  5305. }
  5306. /**
  5307. * Delete context instance
  5308. *
  5309. * @static
  5310. * @param int $contextlevel
  5311. * @param int $instanceid
  5312. * @return void
  5313. */
  5314. public static function delete_instance($contextlevel, $instanceid) {
  5315. global $DB;
  5316. // double check the context still exists
  5317. if ($record = $DB->get_record('context', array('contextlevel'=>$contextlevel, 'instanceid'=>$instanceid))) {
  5318. $context = context::create_instance_from_record($record);
  5319. $context->delete();
  5320. } else {
  5321. // we should try to purge the cache anyway
  5322. }
  5323. }
  5324. /**
  5325. * Returns the name of specified context level
  5326. *
  5327. * @static
  5328. * @param int $contextlevel
  5329. * @return string name of the context level
  5330. */
  5331. public static function get_level_name($contextlevel) {
  5332. $classname = context_helper::get_class_for_level($contextlevel);
  5333. return $classname::get_level_name();
  5334. }
  5335. /**
  5336. * not used
  5337. */
  5338. public function get_url() {
  5339. }
  5340. /**
  5341. * not used
  5342. */
  5343. public function get_capabilities() {
  5344. }
  5345. }
  5346. /**
  5347. * System context class
  5348. *
  5349. * @package core_access
  5350. * @category access
  5351. * @copyright Petr Skoda {@link http://skodak.org}
  5352. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5353. * @since Moodle 2.2
  5354. */
  5355. class context_system extends context {
  5356. /**
  5357. * Please use context_system::instance() if you need the instance of context.
  5358. *
  5359. * @param stdClass $record
  5360. */
  5361. protected function __construct(stdClass $record) {
  5362. parent::__construct($record);
  5363. if ($record->contextlevel != CONTEXT_SYSTEM) {
  5364. throw new coding_exception('Invalid $record->contextlevel in context_system constructor.');
  5365. }
  5366. }
  5367. /**
  5368. * Returns human readable context level name.
  5369. *
  5370. * @static
  5371. * @return string the human readable context level name.
  5372. */
  5373. public static function get_level_name() {
  5374. return get_string('coresystem');
  5375. }
  5376. /**
  5377. * Returns human readable context identifier.
  5378. *
  5379. * @param boolean $withprefix does not apply to system context
  5380. * @param boolean $short does not apply to system context
  5381. * @return string the human readable context name.
  5382. */
  5383. public function get_context_name($withprefix = true, $short = false) {
  5384. return self::get_level_name();
  5385. }
  5386. /**
  5387. * Returns the most relevant URL for this context.
  5388. *
  5389. * @return moodle_url
  5390. */
  5391. public function get_url() {
  5392. return new moodle_url('/');
  5393. }
  5394. /**
  5395. * Returns array of relevant context capability records.
  5396. *
  5397. * @return array
  5398. */
  5399. public function get_capabilities() {
  5400. global $DB;
  5401. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  5402. $params = array();
  5403. $sql = "SELECT *
  5404. FROM {capabilities}";
  5405. return $DB->get_records_sql($sql.' '.$sort, $params);
  5406. }
  5407. /**
  5408. * Create missing context instances at system context
  5409. * @static
  5410. */
  5411. protected static function create_level_instances() {
  5412. // nothing to do here, the system context is created automatically in installer
  5413. self::instance(0);
  5414. }
  5415. /**
  5416. * Returns system context instance.
  5417. *
  5418. * @static
  5419. * @param int $instanceid should be 0
  5420. * @param int $strictness
  5421. * @param bool $cache
  5422. * @return context_system context instance
  5423. */
  5424. public static function instance($instanceid = 0, $strictness = MUST_EXIST, $cache = true) {
  5425. global $DB;
  5426. if ($instanceid != 0) {
  5427. debugging('context_system::instance(): invalid $id parameter detected, should be 0');
  5428. }
  5429. if (defined('SYSCONTEXTID') and $cache) { // dangerous: define this in config.php to eliminate 1 query/page
  5430. if (!isset(context::$systemcontext)) {
  5431. $record = new stdClass();
  5432. $record->id = SYSCONTEXTID;
  5433. $record->contextlevel = CONTEXT_SYSTEM;
  5434. $record->instanceid = 0;
  5435. $record->path = '/'.SYSCONTEXTID;
  5436. $record->depth = 1;
  5437. $record->locked = 0;
  5438. context::$systemcontext = new context_system($record);
  5439. }
  5440. return context::$systemcontext;
  5441. }
  5442. try {
  5443. // We ignore the strictness completely because system context must exist except during install.
  5444. $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST);
  5445. } catch (dml_exception $e) {
  5446. //table or record does not exist
  5447. if (!during_initial_install()) {
  5448. // do not mess with system context after install, it simply must exist
  5449. throw $e;
  5450. }
  5451. $record = null;
  5452. }
  5453. if (!$record) {
  5454. $record = new stdClass();
  5455. $record->contextlevel = CONTEXT_SYSTEM;
  5456. $record->instanceid = 0;
  5457. $record->depth = 1;
  5458. $record->path = null; // Not known before insert.
  5459. $record->locked = 0;
  5460. try {
  5461. if ($DB->count_records('context')) {
  5462. // contexts already exist, this is very weird, system must be first!!!
  5463. return null;
  5464. }
  5465. if (defined('SYSCONTEXTID')) {
  5466. // this would happen only in unittest on sites that went through weird 1.7 upgrade
  5467. $record->id = SYSCONTEXTID;
  5468. $DB->import_record('context', $record);
  5469. $DB->get_manager()->reset_sequence('context');
  5470. } else {
  5471. $record->id = $DB->insert_record('context', $record);
  5472. }
  5473. } catch (dml_exception $e) {
  5474. // can not create context - table does not exist yet, sorry
  5475. return null;
  5476. }
  5477. }
  5478. if ($record->instanceid != 0) {
  5479. // this is very weird, somebody must be messing with context table
  5480. debugging('Invalid system context detected');
  5481. }
  5482. if ($record->depth != 1 or $record->path != '/'.$record->id) {
  5483. // fix path if necessary, initial install or path reset
  5484. $record->depth = 1;
  5485. $record->path = '/'.$record->id;
  5486. $DB->update_record('context', $record);
  5487. }
  5488. if (empty($record->locked)) {
  5489. $record->locked = 0;
  5490. }
  5491. if (!defined('SYSCONTEXTID')) {
  5492. define('SYSCONTEXTID', $record->id);
  5493. }
  5494. context::$systemcontext = new context_system($record);
  5495. return context::$systemcontext;
  5496. }
  5497. /**
  5498. * Returns all site contexts except the system context, DO NOT call on production servers!!
  5499. *
  5500. * Contexts are not cached.
  5501. *
  5502. * @return array
  5503. */
  5504. public function get_child_contexts() {
  5505. global $DB;
  5506. debugging('Fetching of system context child courses is strongly discouraged on production servers (it may eat all available memory)!');
  5507. // Just get all the contexts except for CONTEXT_SYSTEM level
  5508. // and hope we don't OOM in the process - don't cache
  5509. $sql = "SELECT c.*
  5510. FROM {context} c
  5511. WHERE contextlevel > ".CONTEXT_SYSTEM;
  5512. $records = $DB->get_records_sql($sql);
  5513. $result = array();
  5514. foreach ($records as $record) {
  5515. $result[$record->id] = context::create_instance_from_record($record);
  5516. }
  5517. return $result;
  5518. }
  5519. /**
  5520. * Returns sql necessary for purging of stale context instances.
  5521. *
  5522. * @static
  5523. * @return string cleanup SQL
  5524. */
  5525. protected static function get_cleanup_sql() {
  5526. $sql = "
  5527. SELECT c.*
  5528. FROM {context} c
  5529. WHERE 1=2
  5530. ";
  5531. return $sql;
  5532. }
  5533. /**
  5534. * Rebuild context paths and depths at system context level.
  5535. *
  5536. * @static
  5537. * @param bool $force
  5538. */
  5539. protected static function build_paths($force) {
  5540. global $DB;
  5541. /* note: ignore $force here, we always do full test of system context */
  5542. // exactly one record must exist
  5543. $record = $DB->get_record('context', array('contextlevel'=>CONTEXT_SYSTEM), '*', MUST_EXIST);
  5544. if ($record->instanceid != 0) {
  5545. debugging('Invalid system context detected');
  5546. }
  5547. if (defined('SYSCONTEXTID') and $record->id != SYSCONTEXTID) {
  5548. debugging('Invalid SYSCONTEXTID detected');
  5549. }
  5550. if ($record->depth != 1 or $record->path != '/'.$record->id) {
  5551. // fix path if necessary, initial install or path reset
  5552. $record->depth = 1;
  5553. $record->path = '/'.$record->id;
  5554. $DB->update_record('context', $record);
  5555. }
  5556. }
  5557. /**
  5558. * Set whether this context has been locked or not.
  5559. *
  5560. * @param bool $locked
  5561. * @return $this
  5562. */
  5563. public function set_locked(bool $locked) {
  5564. throw new \coding_exception('It is not possible to lock the system context');
  5565. return $this;
  5566. }
  5567. }
  5568. /**
  5569. * User context class
  5570. *
  5571. * @package core_access
  5572. * @category access
  5573. * @copyright Petr Skoda {@link http://skodak.org}
  5574. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5575. * @since Moodle 2.2
  5576. */
  5577. class context_user extends context {
  5578. /**
  5579. * Please use context_user::instance($userid) if you need the instance of context.
  5580. * Alternatively if you know only the context id use context::instance_by_id($contextid)
  5581. *
  5582. * @param stdClass $record
  5583. */
  5584. protected function __construct(stdClass $record) {
  5585. parent::__construct($record);
  5586. if ($record->contextlevel != CONTEXT_USER) {
  5587. throw new coding_exception('Invalid $record->contextlevel in context_user constructor.');
  5588. }
  5589. }
  5590. /**
  5591. * Returns human readable context level name.
  5592. *
  5593. * @static
  5594. * @return string the human readable context level name.
  5595. */
  5596. public static function get_level_name() {
  5597. return get_string('user');
  5598. }
  5599. /**
  5600. * Returns human readable context identifier.
  5601. *
  5602. * @param boolean $withprefix whether to prefix the name of the context with User
  5603. * @param boolean $short does not apply to user context
  5604. * @return string the human readable context name.
  5605. */
  5606. public function get_context_name($withprefix = true, $short = false) {
  5607. global $DB;
  5608. $name = '';
  5609. if ($user = $DB->get_record('user', array('id'=>$this->_instanceid, 'deleted'=>0))) {
  5610. if ($withprefix){
  5611. $name = get_string('user').': ';
  5612. }
  5613. $name .= fullname($user);
  5614. }
  5615. return $name;
  5616. }
  5617. /**
  5618. * Returns the most relevant URL for this context.
  5619. *
  5620. * @return moodle_url
  5621. */
  5622. public function get_url() {
  5623. global $COURSE;
  5624. if ($COURSE->id == SITEID) {
  5625. $url = new moodle_url('/user/profile.php', array('id'=>$this->_instanceid));
  5626. } else {
  5627. $url = new moodle_url('/user/view.php', array('id'=>$this->_instanceid, 'courseid'=>$COURSE->id));
  5628. }
  5629. return $url;
  5630. }
  5631. /**
  5632. * Returns array of relevant context capability records.
  5633. *
  5634. * @return array
  5635. */
  5636. public function get_capabilities() {
  5637. global $DB;
  5638. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  5639. $extracaps = array('moodle/grade:viewall');
  5640. list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
  5641. $sql = "SELECT *
  5642. FROM {capabilities}
  5643. WHERE contextlevel = ".CONTEXT_USER."
  5644. OR name $extra";
  5645. return $records = $DB->get_records_sql($sql.' '.$sort, $params);
  5646. }
  5647. /**
  5648. * Returns user context instance.
  5649. *
  5650. * @static
  5651. * @param int $userid id from {user} table
  5652. * @param int $strictness
  5653. * @return context_user context instance
  5654. */
  5655. public static function instance($userid, $strictness = MUST_EXIST) {
  5656. global $DB;
  5657. if ($context = context::cache_get(CONTEXT_USER, $userid)) {
  5658. return $context;
  5659. }
  5660. if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_USER, 'instanceid' => $userid))) {
  5661. if ($user = $DB->get_record('user', array('id' => $userid, 'deleted' => 0), 'id', $strictness)) {
  5662. $record = context::insert_context_record(CONTEXT_USER, $user->id, '/'.SYSCONTEXTID, 0);
  5663. }
  5664. }
  5665. if ($record) {
  5666. $context = new context_user($record);
  5667. context::cache_add($context);
  5668. return $context;
  5669. }
  5670. return false;
  5671. }
  5672. /**
  5673. * Create missing context instances at user context level
  5674. * @static
  5675. */
  5676. protected static function create_level_instances() {
  5677. global $DB;
  5678. $sql = "SELECT ".CONTEXT_USER.", u.id
  5679. FROM {user} u
  5680. WHERE u.deleted = 0
  5681. AND NOT EXISTS (SELECT 'x'
  5682. FROM {context} cx
  5683. WHERE u.id = cx.instanceid AND cx.contextlevel=".CONTEXT_USER.")";
  5684. $contextdata = $DB->get_recordset_sql($sql);
  5685. foreach ($contextdata as $context) {
  5686. context::insert_context_record(CONTEXT_USER, $context->id, null);
  5687. }
  5688. $contextdata->close();
  5689. }
  5690. /**
  5691. * Returns sql necessary for purging of stale context instances.
  5692. *
  5693. * @static
  5694. * @return string cleanup SQL
  5695. */
  5696. protected static function get_cleanup_sql() {
  5697. $sql = "
  5698. SELECT c.*
  5699. FROM {context} c
  5700. LEFT OUTER JOIN {user} u ON (c.instanceid = u.id AND u.deleted = 0)
  5701. WHERE u.id IS NULL AND c.contextlevel = ".CONTEXT_USER."
  5702. ";
  5703. return $sql;
  5704. }
  5705. /**
  5706. * Rebuild context paths and depths at user context level.
  5707. *
  5708. * @static
  5709. * @param bool $force
  5710. */
  5711. protected static function build_paths($force) {
  5712. global $DB;
  5713. // First update normal users.
  5714. $path = $DB->sql_concat('?', 'id');
  5715. $pathstart = '/' . SYSCONTEXTID . '/';
  5716. $params = array($pathstart);
  5717. if ($force) {
  5718. $where = "depth <> 2 OR path IS NULL OR path <> ({$path})";
  5719. $params[] = $pathstart;
  5720. } else {
  5721. $where = "depth = 0 OR path IS NULL";
  5722. }
  5723. $sql = "UPDATE {context}
  5724. SET depth = 2,
  5725. path = {$path}
  5726. WHERE contextlevel = " . CONTEXT_USER . "
  5727. AND ($where)";
  5728. $DB->execute($sql, $params);
  5729. }
  5730. }
  5731. /**
  5732. * Course category context class
  5733. *
  5734. * @package core_access
  5735. * @category access
  5736. * @copyright Petr Skoda {@link http://skodak.org}
  5737. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5738. * @since Moodle 2.2
  5739. */
  5740. class context_coursecat extends context {
  5741. /**
  5742. * Please use context_coursecat::instance($coursecatid) if you need the instance of context.
  5743. * Alternatively if you know only the context id use context::instance_by_id($contextid)
  5744. *
  5745. * @param stdClass $record
  5746. */
  5747. protected function __construct(stdClass $record) {
  5748. parent::__construct($record);
  5749. if ($record->contextlevel != CONTEXT_COURSECAT) {
  5750. throw new coding_exception('Invalid $record->contextlevel in context_coursecat constructor.');
  5751. }
  5752. }
  5753. /**
  5754. * Returns human readable context level name.
  5755. *
  5756. * @static
  5757. * @return string the human readable context level name.
  5758. */
  5759. public static function get_level_name() {
  5760. return get_string('category');
  5761. }
  5762. /**
  5763. * Returns human readable context identifier.
  5764. *
  5765. * @param boolean $withprefix whether to prefix the name of the context with Category
  5766. * @param boolean $short does not apply to course categories
  5767. * @return string the human readable context name.
  5768. */
  5769. public function get_context_name($withprefix = true, $short = false) {
  5770. global $DB;
  5771. $name = '';
  5772. if ($category = $DB->get_record('course_categories', array('id'=>$this->_instanceid))) {
  5773. if ($withprefix){
  5774. $name = get_string('category').': ';
  5775. }
  5776. $name .= format_string($category->name, true, array('context' => $this));
  5777. }
  5778. return $name;
  5779. }
  5780. /**
  5781. * Returns the most relevant URL for this context.
  5782. *
  5783. * @return moodle_url
  5784. */
  5785. public function get_url() {
  5786. return new moodle_url('/course/index.php', array('categoryid' => $this->_instanceid));
  5787. }
  5788. /**
  5789. * Returns array of relevant context capability records.
  5790. *
  5791. * @return array
  5792. */
  5793. public function get_capabilities() {
  5794. global $DB;
  5795. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  5796. $params = array();
  5797. $sql = "SELECT *
  5798. FROM {capabilities}
  5799. WHERE contextlevel IN (".CONTEXT_COURSECAT.",".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
  5800. return $DB->get_records_sql($sql.' '.$sort, $params);
  5801. }
  5802. /**
  5803. * Returns course category context instance.
  5804. *
  5805. * @static
  5806. * @param int $categoryid id from {course_categories} table
  5807. * @param int $strictness
  5808. * @return context_coursecat context instance
  5809. */
  5810. public static function instance($categoryid, $strictness = MUST_EXIST) {
  5811. global $DB;
  5812. if ($context = context::cache_get(CONTEXT_COURSECAT, $categoryid)) {
  5813. return $context;
  5814. }
  5815. if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSECAT, 'instanceid' => $categoryid))) {
  5816. if ($category = $DB->get_record('course_categories', array('id' => $categoryid), 'id,parent', $strictness)) {
  5817. if ($category->parent) {
  5818. $parentcontext = context_coursecat::instance($category->parent);
  5819. $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, $parentcontext->path);
  5820. } else {
  5821. $record = context::insert_context_record(CONTEXT_COURSECAT, $category->id, '/'.SYSCONTEXTID, 0);
  5822. }
  5823. }
  5824. }
  5825. if ($record) {
  5826. $context = new context_coursecat($record);
  5827. context::cache_add($context);
  5828. return $context;
  5829. }
  5830. return false;
  5831. }
  5832. /**
  5833. * Returns immediate child contexts of category and all subcategories,
  5834. * children of subcategories and courses are not returned.
  5835. *
  5836. * @return array
  5837. */
  5838. public function get_child_contexts() {
  5839. global $DB;
  5840. if (empty($this->_path) or empty($this->_depth)) {
  5841. debugging('Can not find child contexts of context '.$this->_id.' try rebuilding of context paths');
  5842. return array();
  5843. }
  5844. $sql = "SELECT ctx.*
  5845. FROM {context} ctx
  5846. WHERE ctx.path LIKE ? AND (ctx.depth = ? OR ctx.contextlevel = ?)";
  5847. $params = array($this->_path.'/%', $this->depth+1, CONTEXT_COURSECAT);
  5848. $records = $DB->get_records_sql($sql, $params);
  5849. $result = array();
  5850. foreach ($records as $record) {
  5851. $result[$record->id] = context::create_instance_from_record($record);
  5852. }
  5853. return $result;
  5854. }
  5855. /**
  5856. * Create missing context instances at course category context level
  5857. * @static
  5858. */
  5859. protected static function create_level_instances() {
  5860. global $DB;
  5861. $sql = "SELECT ".CONTEXT_COURSECAT.", cc.id
  5862. FROM {course_categories} cc
  5863. WHERE NOT EXISTS (SELECT 'x'
  5864. FROM {context} cx
  5865. WHERE cc.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSECAT.")";
  5866. $contextdata = $DB->get_recordset_sql($sql);
  5867. foreach ($contextdata as $context) {
  5868. context::insert_context_record(CONTEXT_COURSECAT, $context->id, null);
  5869. }
  5870. $contextdata->close();
  5871. }
  5872. /**
  5873. * Returns sql necessary for purging of stale context instances.
  5874. *
  5875. * @static
  5876. * @return string cleanup SQL
  5877. */
  5878. protected static function get_cleanup_sql() {
  5879. $sql = "
  5880. SELECT c.*
  5881. FROM {context} c
  5882. LEFT OUTER JOIN {course_categories} cc ON c.instanceid = cc.id
  5883. WHERE cc.id IS NULL AND c.contextlevel = ".CONTEXT_COURSECAT."
  5884. ";
  5885. return $sql;
  5886. }
  5887. /**
  5888. * Rebuild context paths and depths at course category context level.
  5889. *
  5890. * @static
  5891. * @param bool $force
  5892. */
  5893. protected static function build_paths($force) {
  5894. global $DB;
  5895. if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSECAT." AND (depth = 0 OR path IS NULL)")) {
  5896. if ($force) {
  5897. $ctxemptyclause = $emptyclause = '';
  5898. } else {
  5899. $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
  5900. $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
  5901. }
  5902. $base = '/'.SYSCONTEXTID;
  5903. // Normal top level categories
  5904. $sql = "UPDATE {context}
  5905. SET depth=2,
  5906. path=".$DB->sql_concat("'$base/'", 'id')."
  5907. WHERE contextlevel=".CONTEXT_COURSECAT."
  5908. AND EXISTS (SELECT 'x'
  5909. FROM {course_categories} cc
  5910. WHERE cc.id = {context}.instanceid AND cc.depth=1)
  5911. $emptyclause";
  5912. $DB->execute($sql);
  5913. // Deeper categories - one query per depthlevel
  5914. $maxdepth = $DB->get_field_sql("SELECT MAX(depth) FROM {course_categories}");
  5915. for ($n=2; $n<=$maxdepth; $n++) {
  5916. $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
  5917. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
  5918. FROM {context} ctx
  5919. JOIN {course_categories} cc ON (cc.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSECAT." AND cc.depth = $n)
  5920. JOIN {context} pctx ON (pctx.instanceid = cc.parent AND pctx.contextlevel = ".CONTEXT_COURSECAT.")
  5921. WHERE pctx.path IS NOT NULL AND pctx.depth > 0
  5922. $ctxemptyclause";
  5923. $trans = $DB->start_delegated_transaction();
  5924. $DB->delete_records('context_temp');
  5925. $DB->execute($sql);
  5926. context::merge_context_temp_table();
  5927. $DB->delete_records('context_temp');
  5928. $trans->allow_commit();
  5929. }
  5930. }
  5931. }
  5932. }
  5933. /**
  5934. * Course context class
  5935. *
  5936. * @package core_access
  5937. * @category access
  5938. * @copyright Petr Skoda {@link http://skodak.org}
  5939. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5940. * @since Moodle 2.2
  5941. */
  5942. class context_course extends context {
  5943. /**
  5944. * Please use context_course::instance($courseid) if you need the instance of context.
  5945. * Alternatively if you know only the context id use context::instance_by_id($contextid)
  5946. *
  5947. * @param stdClass $record
  5948. */
  5949. protected function __construct(stdClass $record) {
  5950. parent::__construct($record);
  5951. if ($record->contextlevel != CONTEXT_COURSE) {
  5952. throw new coding_exception('Invalid $record->contextlevel in context_course constructor.');
  5953. }
  5954. }
  5955. /**
  5956. * Returns human readable context level name.
  5957. *
  5958. * @static
  5959. * @return string the human readable context level name.
  5960. */
  5961. public static function get_level_name() {
  5962. return get_string('course');
  5963. }
  5964. /**
  5965. * Returns human readable context identifier.
  5966. *
  5967. * @param boolean $withprefix whether to prefix the name of the context with Course
  5968. * @param boolean $short whether to use the short name of the thing.
  5969. * @return string the human readable context name.
  5970. */
  5971. public function get_context_name($withprefix = true, $short = false) {
  5972. global $DB;
  5973. $name = '';
  5974. if ($this->_instanceid == SITEID) {
  5975. $name = get_string('frontpage', 'admin');
  5976. } else {
  5977. if ($course = $DB->get_record('course', array('id'=>$this->_instanceid))) {
  5978. if ($withprefix){
  5979. $name = get_string('course').': ';
  5980. }
  5981. if ($short){
  5982. $name .= format_string($course->shortname, true, array('context' => $this));
  5983. } else {
  5984. $name .= format_string(get_course_display_name_for_list($course));
  5985. }
  5986. }
  5987. }
  5988. return $name;
  5989. }
  5990. /**
  5991. * Returns the most relevant URL for this context.
  5992. *
  5993. * @return moodle_url
  5994. */
  5995. public function get_url() {
  5996. if ($this->_instanceid != SITEID) {
  5997. return new moodle_url('/course/view.php', array('id'=>$this->_instanceid));
  5998. }
  5999. return new moodle_url('/');
  6000. }
  6001. /**
  6002. * Returns array of relevant context capability records.
  6003. *
  6004. * @return array
  6005. */
  6006. public function get_capabilities() {
  6007. global $DB;
  6008. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  6009. $params = array();
  6010. $sql = "SELECT *
  6011. FROM {capabilities}
  6012. WHERE contextlevel IN (".CONTEXT_COURSE.",".CONTEXT_MODULE.",".CONTEXT_BLOCK.")";
  6013. return $DB->get_records_sql($sql.' '.$sort, $params);
  6014. }
  6015. /**
  6016. * Is this context part of any course? If yes return course context.
  6017. *
  6018. * @param bool $strict true means throw exception if not found, false means return false if not found
  6019. * @return context_course context of the enclosing course, null if not found or exception
  6020. */
  6021. public function get_course_context($strict = true) {
  6022. return $this;
  6023. }
  6024. /**
  6025. * Returns course context instance.
  6026. *
  6027. * @static
  6028. * @param int $courseid id from {course} table
  6029. * @param int $strictness
  6030. * @return context_course context instance
  6031. */
  6032. public static function instance($courseid, $strictness = MUST_EXIST) {
  6033. global $DB;
  6034. if ($context = context::cache_get(CONTEXT_COURSE, $courseid)) {
  6035. return $context;
  6036. }
  6037. if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_COURSE, 'instanceid' => $courseid))) {
  6038. if ($course = $DB->get_record('course', array('id' => $courseid), 'id,category', $strictness)) {
  6039. if ($course->category) {
  6040. $parentcontext = context_coursecat::instance($course->category);
  6041. $record = context::insert_context_record(CONTEXT_COURSE, $course->id, $parentcontext->path);
  6042. } else {
  6043. $record = context::insert_context_record(CONTEXT_COURSE, $course->id, '/'.SYSCONTEXTID, 0);
  6044. }
  6045. }
  6046. }
  6047. if ($record) {
  6048. $context = new context_course($record);
  6049. context::cache_add($context);
  6050. return $context;
  6051. }
  6052. return false;
  6053. }
  6054. /**
  6055. * Create missing context instances at course context level
  6056. * @static
  6057. */
  6058. protected static function create_level_instances() {
  6059. global $DB;
  6060. $sql = "SELECT ".CONTEXT_COURSE.", c.id
  6061. FROM {course} c
  6062. WHERE NOT EXISTS (SELECT 'x'
  6063. FROM {context} cx
  6064. WHERE c.id = cx.instanceid AND cx.contextlevel=".CONTEXT_COURSE.")";
  6065. $contextdata = $DB->get_recordset_sql($sql);
  6066. foreach ($contextdata as $context) {
  6067. context::insert_context_record(CONTEXT_COURSE, $context->id, null);
  6068. }
  6069. $contextdata->close();
  6070. }
  6071. /**
  6072. * Returns sql necessary for purging of stale context instances.
  6073. *
  6074. * @static
  6075. * @return string cleanup SQL
  6076. */
  6077. protected static function get_cleanup_sql() {
  6078. $sql = "
  6079. SELECT c.*
  6080. FROM {context} c
  6081. LEFT OUTER JOIN {course} co ON c.instanceid = co.id
  6082. WHERE co.id IS NULL AND c.contextlevel = ".CONTEXT_COURSE."
  6083. ";
  6084. return $sql;
  6085. }
  6086. /**
  6087. * Rebuild context paths and depths at course context level.
  6088. *
  6089. * @static
  6090. * @param bool $force
  6091. */
  6092. protected static function build_paths($force) {
  6093. global $DB;
  6094. if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_COURSE." AND (depth = 0 OR path IS NULL)")) {
  6095. if ($force) {
  6096. $ctxemptyclause = $emptyclause = '';
  6097. } else {
  6098. $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
  6099. $emptyclause = "AND ({context}.path IS NULL OR {context}.depth = 0)";
  6100. }
  6101. $base = '/'.SYSCONTEXTID;
  6102. // Standard frontpage
  6103. $sql = "UPDATE {context}
  6104. SET depth = 2,
  6105. path = ".$DB->sql_concat("'$base/'", 'id')."
  6106. WHERE contextlevel = ".CONTEXT_COURSE."
  6107. AND EXISTS (SELECT 'x'
  6108. FROM {course} c
  6109. WHERE c.id = {context}.instanceid AND c.category = 0)
  6110. $emptyclause";
  6111. $DB->execute($sql);
  6112. // standard courses
  6113. $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
  6114. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
  6115. FROM {context} ctx
  6116. JOIN {course} c ON (c.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_COURSE." AND c.category <> 0)
  6117. JOIN {context} pctx ON (pctx.instanceid = c.category AND pctx.contextlevel = ".CONTEXT_COURSECAT.")
  6118. WHERE pctx.path IS NOT NULL AND pctx.depth > 0
  6119. $ctxemptyclause";
  6120. $trans = $DB->start_delegated_transaction();
  6121. $DB->delete_records('context_temp');
  6122. $DB->execute($sql);
  6123. context::merge_context_temp_table();
  6124. $DB->delete_records('context_temp');
  6125. $trans->allow_commit();
  6126. }
  6127. }
  6128. }
  6129. /**
  6130. * Course module context class
  6131. *
  6132. * @package core_access
  6133. * @category access
  6134. * @copyright Petr Skoda {@link http://skodak.org}
  6135. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  6136. * @since Moodle 2.2
  6137. */
  6138. class context_module extends context {
  6139. /**
  6140. * Please use context_module::instance($cmid) if you need the instance of context.
  6141. * Alternatively if you know only the context id use context::instance_by_id($contextid)
  6142. *
  6143. * @param stdClass $record
  6144. */
  6145. protected function __construct(stdClass $record) {
  6146. parent::__construct($record);
  6147. if ($record->contextlevel != CONTEXT_MODULE) {
  6148. throw new coding_exception('Invalid $record->contextlevel in context_module constructor.');
  6149. }
  6150. }
  6151. /**
  6152. * Returns human readable context level name.
  6153. *
  6154. * @static
  6155. * @return string the human readable context level name.
  6156. */
  6157. public static function get_level_name() {
  6158. return get_string('activitymodule');
  6159. }
  6160. /**
  6161. * Returns human readable context identifier.
  6162. *
  6163. * @param boolean $withprefix whether to prefix the name of the context with the
  6164. * module name, e.g. Forum, Glossary, etc.
  6165. * @param boolean $short does not apply to module context
  6166. * @return string the human readable context name.
  6167. */
  6168. public function get_context_name($withprefix = true, $short = false) {
  6169. global $DB;
  6170. $name = '';
  6171. if ($cm = $DB->get_record_sql("SELECT cm.*, md.name AS modname
  6172. FROM {course_modules} cm
  6173. JOIN {modules} md ON md.id = cm.module
  6174. WHERE cm.id = ?", array($this->_instanceid))) {
  6175. if ($mod = $DB->get_record($cm->modname, array('id' => $cm->instance))) {
  6176. if ($withprefix){
  6177. $name = get_string('modulename', $cm->modname).': ';
  6178. }
  6179. $name .= format_string($mod->name, true, array('context' => $this));
  6180. }
  6181. }
  6182. return $name;
  6183. }
  6184. /**
  6185. * Returns the most relevant URL for this context.
  6186. *
  6187. * @return moodle_url
  6188. */
  6189. public function get_url() {
  6190. global $DB;
  6191. if ($modname = $DB->get_field_sql("SELECT md.name AS modname
  6192. FROM {course_modules} cm
  6193. JOIN {modules} md ON md.id = cm.module
  6194. WHERE cm.id = ?", array($this->_instanceid))) {
  6195. return new moodle_url('/mod/' . $modname . '/view.php', array('id'=>$this->_instanceid));
  6196. }
  6197. return new moodle_url('/');
  6198. }
  6199. /**
  6200. * Returns array of relevant context capability records.
  6201. *
  6202. * @return array
  6203. */
  6204. public function get_capabilities() {
  6205. global $DB, $CFG;
  6206. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  6207. $cm = $DB->get_record('course_modules', array('id'=>$this->_instanceid));
  6208. $module = $DB->get_record('modules', array('id'=>$cm->module));
  6209. $subcaps = array();
  6210. $modulepath = "{$CFG->dirroot}/mod/{$module->name}";
  6211. if (file_exists("{$modulepath}/db/subplugins.json")) {
  6212. $subplugins = (array) json_decode(file_get_contents("{$modulepath}/db/subplugins.json"))->plugintypes;
  6213. } else if (file_exists("{$modulepath}/db/subplugins.php")) {
  6214. debugging('Use of subplugins.php has been deprecated. ' .
  6215. 'Please update your plugin to provide a subplugins.json file instead.',
  6216. DEBUG_DEVELOPER);
  6217. $subplugins = array(); // should be redefined in the file
  6218. include("{$modulepath}/db/subplugins.php");
  6219. }
  6220. if (!empty($subplugins)) {
  6221. foreach (array_keys($subplugins) as $subplugintype) {
  6222. foreach (array_keys(core_component::get_plugin_list($subplugintype)) as $subpluginname) {
  6223. $subcaps = array_merge($subcaps, array_keys(load_capability_def($subplugintype.'_'.$subpluginname)));
  6224. }
  6225. }
  6226. }
  6227. $modfile = "{$modulepath}/lib.php";
  6228. $extracaps = array();
  6229. if (file_exists($modfile)) {
  6230. include_once($modfile);
  6231. $modfunction = $module->name.'_get_extra_capabilities';
  6232. if (function_exists($modfunction)) {
  6233. $extracaps = $modfunction();
  6234. }
  6235. }
  6236. $extracaps = array_merge($subcaps, $extracaps);
  6237. $extra = '';
  6238. list($extra, $params) = $DB->get_in_or_equal(
  6239. $extracaps, SQL_PARAMS_NAMED, 'cap0', true, '');
  6240. if (!empty($extra)) {
  6241. $extra = "OR name $extra";
  6242. }
  6243. // Fetch the list of modules, and remove this one.
  6244. $components = \core_component::get_component_list();
  6245. $componentnames = $components['mod'];
  6246. unset($componentnames["mod_{$module->name}"]);
  6247. $componentnames = array_keys($componentnames);
  6248. // Exclude all other modules.
  6249. list($notcompsql, $notcompparams) = $DB->get_in_or_equal($componentnames, SQL_PARAMS_NAMED, 'notcomp', false);
  6250. $params = array_merge($params, $notcompparams);
  6251. // Exclude other component submodules.
  6252. $i = 0;
  6253. $ignorecomponents = [];
  6254. foreach ($componentnames as $mod) {
  6255. if ($subplugins = \core_component::get_subplugins($mod)) {
  6256. foreach (array_keys($subplugins) as $subplugintype) {
  6257. $paramname = "notlike{$i}";
  6258. $ignorecomponents[] = $DB->sql_like('component', ":{$paramname}", true, true, true);
  6259. $params[$paramname] = "{$subplugintype}_%";
  6260. $i++;
  6261. }
  6262. }
  6263. }
  6264. $notlikesql = "(" . implode(' AND ', $ignorecomponents) . ")";
  6265. $sql = "SELECT *
  6266. FROM {capabilities}
  6267. WHERE (contextlevel = ".CONTEXT_MODULE."
  6268. AND component {$notcompsql}
  6269. AND {$notlikesql})
  6270. $extra";
  6271. return $DB->get_records_sql($sql.' '.$sort, $params);
  6272. }
  6273. /**
  6274. * Is this context part of any course? If yes return course context.
  6275. *
  6276. * @param bool $strict true means throw exception if not found, false means return false if not found
  6277. * @return context_course context of the enclosing course, null if not found or exception
  6278. */
  6279. public function get_course_context($strict = true) {
  6280. return $this->get_parent_context();
  6281. }
  6282. /**
  6283. * Returns module context instance.
  6284. *
  6285. * @static
  6286. * @param int $cmid id of the record from {course_modules} table; pass cmid there, NOT id in the instance column
  6287. * @param int $strictness
  6288. * @return context_module context instance
  6289. */
  6290. public static function instance($cmid, $strictness = MUST_EXIST) {
  6291. global $DB;
  6292. if ($context = context::cache_get(CONTEXT_MODULE, $cmid)) {
  6293. return $context;
  6294. }
  6295. if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_MODULE, 'instanceid' => $cmid))) {
  6296. if ($cm = $DB->get_record('course_modules', array('id' => $cmid), 'id,course', $strictness)) {
  6297. $parentcontext = context_course::instance($cm->course);
  6298. $record = context::insert_context_record(CONTEXT_MODULE, $cm->id, $parentcontext->path);
  6299. }
  6300. }
  6301. if ($record) {
  6302. $context = new context_module($record);
  6303. context::cache_add($context);
  6304. return $context;
  6305. }
  6306. return false;
  6307. }
  6308. /**
  6309. * Create missing context instances at module context level
  6310. * @static
  6311. */
  6312. protected static function create_level_instances() {
  6313. global $DB;
  6314. $sql = "SELECT ".CONTEXT_MODULE.", cm.id
  6315. FROM {course_modules} cm
  6316. WHERE NOT EXISTS (SELECT 'x'
  6317. FROM {context} cx
  6318. WHERE cm.id = cx.instanceid AND cx.contextlevel=".CONTEXT_MODULE.")";
  6319. $contextdata = $DB->get_recordset_sql($sql);
  6320. foreach ($contextdata as $context) {
  6321. context::insert_context_record(CONTEXT_MODULE, $context->id, null);
  6322. }
  6323. $contextdata->close();
  6324. }
  6325. /**
  6326. * Returns sql necessary for purging of stale context instances.
  6327. *
  6328. * @static
  6329. * @return string cleanup SQL
  6330. */
  6331. protected static function get_cleanup_sql() {
  6332. $sql = "
  6333. SELECT c.*
  6334. FROM {context} c
  6335. LEFT OUTER JOIN {course_modules} cm ON c.instanceid = cm.id
  6336. WHERE cm.id IS NULL AND c.contextlevel = ".CONTEXT_MODULE."
  6337. ";
  6338. return $sql;
  6339. }
  6340. /**
  6341. * Rebuild context paths and depths at module context level.
  6342. *
  6343. * @static
  6344. * @param bool $force
  6345. */
  6346. protected static function build_paths($force) {
  6347. global $DB;
  6348. if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_MODULE." AND (depth = 0 OR path IS NULL)")) {
  6349. if ($force) {
  6350. $ctxemptyclause = '';
  6351. } else {
  6352. $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
  6353. }
  6354. $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
  6355. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
  6356. FROM {context} ctx
  6357. JOIN {course_modules} cm ON (cm.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_MODULE.")
  6358. JOIN {context} pctx ON (pctx.instanceid = cm.course AND pctx.contextlevel = ".CONTEXT_COURSE.")
  6359. WHERE pctx.path IS NOT NULL AND pctx.depth > 0
  6360. $ctxemptyclause";
  6361. $trans = $DB->start_delegated_transaction();
  6362. $DB->delete_records('context_temp');
  6363. $DB->execute($sql);
  6364. context::merge_context_temp_table();
  6365. $DB->delete_records('context_temp');
  6366. $trans->allow_commit();
  6367. }
  6368. }
  6369. }
  6370. /**
  6371. * Block context class
  6372. *
  6373. * @package core_access
  6374. * @category access
  6375. * @copyright Petr Skoda {@link http://skodak.org}
  6376. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  6377. * @since Moodle 2.2
  6378. */
  6379. class context_block extends context {
  6380. /**
  6381. * Please use context_block::instance($blockinstanceid) if you need the instance of context.
  6382. * Alternatively if you know only the context id use context::instance_by_id($contextid)
  6383. *
  6384. * @param stdClass $record
  6385. */
  6386. protected function __construct(stdClass $record) {
  6387. parent::__construct($record);
  6388. if ($record->contextlevel != CONTEXT_BLOCK) {
  6389. throw new coding_exception('Invalid $record->contextlevel in context_block constructor');
  6390. }
  6391. }
  6392. /**
  6393. * Returns human readable context level name.
  6394. *
  6395. * @static
  6396. * @return string the human readable context level name.
  6397. */
  6398. public static function get_level_name() {
  6399. return get_string('block');
  6400. }
  6401. /**
  6402. * Returns human readable context identifier.
  6403. *
  6404. * @param boolean $withprefix whether to prefix the name of the context with Block
  6405. * @param boolean $short does not apply to block context
  6406. * @return string the human readable context name.
  6407. */
  6408. public function get_context_name($withprefix = true, $short = false) {
  6409. global $DB, $CFG;
  6410. $name = '';
  6411. if ($blockinstance = $DB->get_record('block_instances', array('id'=>$this->_instanceid))) {
  6412. global $CFG;
  6413. require_once("$CFG->dirroot/blocks/moodleblock.class.php");
  6414. require_once("$CFG->dirroot/blocks/$blockinstance->blockname/block_$blockinstance->blockname.php");
  6415. $blockname = "block_$blockinstance->blockname";
  6416. if ($blockobject = new $blockname()) {
  6417. if ($withprefix){
  6418. $name = get_string('block').': ';
  6419. }
  6420. $name .= $blockobject->title;
  6421. }
  6422. }
  6423. return $name;
  6424. }
  6425. /**
  6426. * Returns the most relevant URL for this context.
  6427. *
  6428. * @return moodle_url
  6429. */
  6430. public function get_url() {
  6431. $parentcontexts = $this->get_parent_context();
  6432. return $parentcontexts->get_url();
  6433. }
  6434. /**
  6435. * Returns array of relevant context capability records.
  6436. *
  6437. * @return array
  6438. */
  6439. public function get_capabilities() {
  6440. global $DB;
  6441. $sort = 'ORDER BY contextlevel,component,name'; // To group them sensibly for display
  6442. $params = array();
  6443. $bi = $DB->get_record('block_instances', array('id' => $this->_instanceid));
  6444. $extra = '';
  6445. $extracaps = block_method_result($bi->blockname, 'get_extra_capabilities');
  6446. if ($extracaps) {
  6447. list($extra, $params) = $DB->get_in_or_equal($extracaps, SQL_PARAMS_NAMED, 'cap');
  6448. $extra = "OR name $extra";
  6449. }
  6450. $sql = "SELECT *
  6451. FROM {capabilities}
  6452. WHERE (contextlevel = ".CONTEXT_BLOCK."
  6453. AND component = :component)
  6454. $extra";
  6455. $params['component'] = 'block_' . $bi->blockname;
  6456. return $DB->get_records_sql($sql.' '.$sort, $params);
  6457. }
  6458. /**
  6459. * Is this context part of any course? If yes return course context.
  6460. *
  6461. * @param bool $strict true means throw exception if not found, false means return false if not found
  6462. * @return context_course context of the enclosing course, null if not found or exception
  6463. */
  6464. public function get_course_context($strict = true) {
  6465. $parentcontext = $this->get_parent_context();
  6466. return $parentcontext->get_course_context($strict);
  6467. }
  6468. /**
  6469. * Returns block context instance.
  6470. *
  6471. * @static
  6472. * @param int $blockinstanceid id from {block_instances} table.
  6473. * @param int $strictness
  6474. * @return context_block context instance
  6475. */
  6476. public static function instance($blockinstanceid, $strictness = MUST_EXIST) {
  6477. global $DB;
  6478. if ($context = context::cache_get(CONTEXT_BLOCK, $blockinstanceid)) {
  6479. return $context;
  6480. }
  6481. if (!$record = $DB->get_record('context', array('contextlevel' => CONTEXT_BLOCK, 'instanceid' => $blockinstanceid))) {
  6482. if ($bi = $DB->get_record('block_instances', array('id' => $blockinstanceid), 'id,parentcontextid', $strictness)) {
  6483. $parentcontext = context::instance_by_id($bi->parentcontextid);
  6484. $record = context::insert_context_record(CONTEXT_BLOCK, $bi->id, $parentcontext->path);
  6485. }
  6486. }
  6487. if ($record) {
  6488. $context = new context_block($record);
  6489. context::cache_add($context);
  6490. return $context;
  6491. }
  6492. return false;
  6493. }
  6494. /**
  6495. * Block do not have child contexts...
  6496. * @return array
  6497. */
  6498. public function get_child_contexts() {
  6499. return array();
  6500. }
  6501. /**
  6502. * Create missing context instances at block context level
  6503. * @static
  6504. */
  6505. protected static function create_level_instances() {
  6506. global $DB;
  6507. $sql = "SELECT ".CONTEXT_BLOCK.", bi.id
  6508. FROM {block_instances} bi
  6509. WHERE NOT EXISTS (SELECT 'x'
  6510. FROM {context} cx
  6511. WHERE bi.id = cx.instanceid AND cx.contextlevel=".CONTEXT_BLOCK.")";
  6512. $contextdata = $DB->get_recordset_sql($sql);
  6513. foreach ($contextdata as $context) {
  6514. context::insert_context_record(CONTEXT_BLOCK, $context->id, null);
  6515. }
  6516. $contextdata->close();
  6517. }
  6518. /**
  6519. * Returns sql necessary for purging of stale context instances.
  6520. *
  6521. * @static
  6522. * @return string cleanup SQL
  6523. */
  6524. protected static function get_cleanup_sql() {
  6525. $sql = "
  6526. SELECT c.*
  6527. FROM {context} c
  6528. LEFT OUTER JOIN {block_instances} bi ON c.instanceid = bi.id
  6529. WHERE bi.id IS NULL AND c.contextlevel = ".CONTEXT_BLOCK."
  6530. ";
  6531. return $sql;
  6532. }
  6533. /**
  6534. * Rebuild context paths and depths at block context level.
  6535. *
  6536. * @static
  6537. * @param bool $force
  6538. */
  6539. protected static function build_paths($force) {
  6540. global $DB;
  6541. if ($force or $DB->record_exists_select('context', "contextlevel = ".CONTEXT_BLOCK." AND (depth = 0 OR path IS NULL)")) {
  6542. if ($force) {
  6543. $ctxemptyclause = '';
  6544. } else {
  6545. $ctxemptyclause = "AND (ctx.path IS NULL OR ctx.depth = 0)";
  6546. }
  6547. // pctx.path IS NOT NULL prevents fatal problems with broken block instances that point to invalid context parent
  6548. $sql = "INSERT INTO {context_temp} (id, path, depth, locked)
  6549. SELECT ctx.id, ".$DB->sql_concat('pctx.path', "'/'", 'ctx.id').", pctx.depth+1, ctx.locked
  6550. FROM {context} ctx
  6551. JOIN {block_instances} bi ON (bi.id = ctx.instanceid AND ctx.contextlevel = ".CONTEXT_BLOCK.")
  6552. JOIN {context} pctx ON (pctx.id = bi.parentcontextid)
  6553. WHERE (pctx.path IS NOT NULL AND pctx.depth > 0)
  6554. $ctxemptyclause";
  6555. $trans = $DB->start_delegated_transaction();
  6556. $DB->delete_records('context_temp');
  6557. $DB->execute($sql);
  6558. context::merge_context_temp_table();
  6559. $DB->delete_records('context_temp');
  6560. $trans->allow_commit();
  6561. }
  6562. }
  6563. }
  6564. // ============== DEPRECATED FUNCTIONS ==========================================
  6565. // Old context related functions were deprecated in 2.0, it is recommended
  6566. // to use context classes in new code. Old function can be used when
  6567. // creating patches that are supposed to be backported to older stable branches.
  6568. // These deprecated functions will not be removed in near future,
  6569. // before removing devs will be warned with a debugging message first,
  6570. // then we will add error message and only after that we can remove the functions
  6571. // completely.
  6572. /**
  6573. * Runs get_records select on context table and returns the result
  6574. * Does get_records_select on the context table, and returns the results ordered
  6575. * by contextlevel, and then the natural sort order within each level.
  6576. * for the purpose of $select, you need to know that the context table has been
  6577. * aliased to ctx, so for example, you can call get_sorted_contexts('ctx.depth = 3');
  6578. *
  6579. * @param string $select the contents of the WHERE clause. Remember to do ctx.fieldname.
  6580. * @param array $params any parameters required by $select.
  6581. * @return array the requested context records.
  6582. */
  6583. function get_sorted_contexts($select, $params = array()) {
  6584. //TODO: we should probably rewrite all the code that is using this thing, the trouble is we MUST NOT modify the context instances...
  6585. global $DB;
  6586. if ($select) {
  6587. $select = 'WHERE ' . $select;
  6588. }
  6589. return $DB->get_records_sql("
  6590. SELECT ctx.*
  6591. FROM {context} ctx
  6592. LEFT JOIN {user} u ON ctx.contextlevel = " . CONTEXT_USER . " AND u.id = ctx.instanceid
  6593. LEFT JOIN {course_categories} cat ON ctx.contextlevel = " . CONTEXT_COURSECAT . " AND cat.id = ctx.instanceid
  6594. LEFT JOIN {course} c ON ctx.contextlevel = " . CONTEXT_COURSE . " AND c.id = ctx.instanceid
  6595. LEFT JOIN {course_modules} cm ON ctx.contextlevel = " . CONTEXT_MODULE . " AND cm.id = ctx.instanceid
  6596. LEFT JOIN {block_instances} bi ON ctx.contextlevel = " . CONTEXT_BLOCK . " AND bi.id = ctx.instanceid
  6597. $select
  6598. ORDER BY ctx.contextlevel, bi.defaultregion, COALESCE(cat.sortorder, c.sortorder, cm.section, bi.defaultweight), u.lastname, u.firstname, cm.id
  6599. ", $params);
  6600. }
  6601. /**
  6602. * Given context and array of users, returns array of users whose enrolment status is suspended,
  6603. * or enrolment has expired or has not started. Also removes those users from the given array
  6604. *
  6605. * @param context $context context in which suspended users should be extracted.
  6606. * @param array $users list of users.
  6607. * @param array $ignoreusers array of user ids to ignore, e.g. guest
  6608. * @return array list of suspended users.
  6609. */
  6610. function extract_suspended_users($context, &$users, $ignoreusers=array()) {
  6611. global $DB;
  6612. // Get active enrolled users.
  6613. list($sql, $params) = get_enrolled_sql($context, null, null, true);
  6614. $activeusers = $DB->get_records_sql($sql, $params);
  6615. // Move suspended users to a separate array & remove from the initial one.
  6616. $susers = array();
  6617. if (sizeof($activeusers)) {
  6618. foreach ($users as $userid => $user) {
  6619. if (!array_key_exists($userid, $activeusers) && !in_array($userid, $ignoreusers)) {
  6620. $susers[$userid] = $user;
  6621. unset($users[$userid]);
  6622. }
  6623. }
  6624. }
  6625. return $susers;
  6626. }
  6627. /**
  6628. * Given context and array of users, returns array of user ids whose enrolment status is suspended,
  6629. * or enrolment has expired or not started.
  6630. *
  6631. * @param context $context context in which user enrolment is checked.
  6632. * @param bool $usecache Enable or disable (default) the request cache
  6633. * @return array list of suspended user id's.
  6634. */
  6635. function get_suspended_userids(context $context, $usecache = false) {
  6636. global $DB;
  6637. if ($usecache) {
  6638. $cache = cache::make('core', 'suspended_userids');
  6639. $susers = $cache->get($context->id);
  6640. if ($susers !== false) {
  6641. return $susers;
  6642. }
  6643. }
  6644. $coursecontext = $context->get_course_context();
  6645. $susers = array();
  6646. // Front page users are always enrolled, so suspended list is empty.
  6647. if ($coursecontext->instanceid != SITEID) {
  6648. list($sql, $params) = get_enrolled_sql($context, null, null, false, true);
  6649. $susers = $DB->get_fieldset_sql($sql, $params);
  6650. $susers = array_combine($susers, $susers);
  6651. }
  6652. // Cache results for the remainder of this request.
  6653. if ($usecache) {
  6654. $cache->set($context->id, $susers);
  6655. }
  6656. return $susers;
  6657. }
  6658. /**
  6659. * Gets sql for finding users with capability in the given context
  6660. *
  6661. * @param context $context
  6662. * @param string|array $capability Capability name or array of names.
  6663. * If an array is provided then this is the equivalent of a logical 'OR',
  6664. * i.e. the user needs to have one of these capabilities.
  6665. * @return array($sql, $params)
  6666. */
  6667. function get_with_capability_sql(context $context, $capability) {
  6668. static $i = 0;
  6669. $i++;
  6670. $prefix = 'cu' . $i . '_';
  6671. $capjoin = get_with_capability_join($context, $capability, $prefix . 'u.id');
  6672. $sql = "SELECT DISTINCT {$prefix}u.id
  6673. FROM {user} {$prefix}u
  6674. $capjoin->joins
  6675. WHERE {$prefix}u.deleted = 0 AND $capjoin->wheres";
  6676. return array($sql, $capjoin->params);
  6677. }