PageRenderTime 82ms CodeModel.GetById 14ms RepoModel.GetById 0ms app.codeStats 1ms

/lib/accesslib.php

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