has_capability('moodle/role:switchroles', $context)) {
// is this role assignable in this context?
// inquiring minds want to know...
- $aroles = get_assignable_roles($context);
+ $aroles = get_switchable_roles($context);
if (is_array($aroles) && isset($aroles[$switchrole])) {
role_switch($switchrole, $context);
// Double check that this role is allowed here
}
/**
- * Gets a list of roles that this user can assign in this context, for the switchrole menu
+ * Gets a list of roles that this user can switch to in a context, for the switchrole menu.
+ * This function just process the contents of the role_allow_switch table. You also need to
+ * test the moodle/role:switchroles to see if the user is allowed to switch in the first place.
*
- * @param object $context the context.
+ * @param object $context a context.
* @return an array $roleid => $rolename.
*/
-function get_assignable_roles_for_switchrole($context) {
+function get_switchable_roles($context) {
global $USER, $DB;
- if (!has_capability('moodle/role:assign', $context)) {
- return array();
- }
-
- $parents = get_parent_contexts($context);
- $parents[] = $context->id;
- $contexts = implode(',' , $parents);
-
- $raafrom = "{role_allow_assign} raa,";
- $raawhere = "AND raa.roleid = ra.roleid AND r.id = raa.allowassign";
- if (has_capability('moodle/site:doanything', get_context_instance(CONTEXT_SYSTEM))) {
- // show all roles allowed in this context to admins
- $raafrom = "";
- $raawhere = "";
- }
-
- if (!$roles = $DB->get_records_sql("
- SELECT ro.*
- FROM {role} ro
- JOIN (SELECT DISTINCT r.id
- FROM {role} r,
- {role_assignments} ra,
- $raafrom
- {role_capabilities} rc
- WHERE ra.userid = :userid AND ra.contextid IN ($contexts)
- $raawhere
- AND r.id = rc.roleid AND rc.capability = :viewcap AND rc.capability <> :anythingcap
- ) inline_view ON ro.id = inline_view.id
- ORDER BY ro.sortorder ASC",
- array('userid'=>$USER->id, 'viewcap'=>'moodle/course:view',
- 'anythingcap'=>'moodle/site:doanything'))) {
- return array();
- }
+ $systemcontext = get_context_instance(CONTEXT_SYSTEM);
- $rolenames = array();
- foreach ($roles as $role) {
- $rolenames[$role->id] = $role->name;
- }
+ $params = array();
+ $extrajoins = '';
+ $extrawhere = '';
+ if (!has_capability('moodle/site:doanything', $systemcontext)) {
+ // Admins are allowed to switch to any role with 'moodle/course:view' in the
+ // role definition, and without 'moodle/site:doanything' anywhere.
+ // Others are subject to the additional constraint that the switch-to role must be allowed by
+ // 'role_allow_switch' for some role they have assigned in this context or any parent.
+ $parents = get_parent_contexts($context);
+ $parents[] = $context->id;
+ $contexts = implode(',' , $parents);
+
+ $extrajoins = "JOIN {role_allow_switch} ras ON ras.allowswitch = rc.roleid
+ JOIN {role_assignments} ra ON ra.roleid = ras.roleid";
+ $extrawhere = "AND ra.userid = :userid
+ AND ra.contextid IN ($contexts)";
+ $params['userid'] = $USER->id;
+ }
+
+ $query = "
+ SELECT r.id, r.name
+ FROM (
+ SELECT DISTINCT rc.roleid
+ FROM {role_capabilities} rc
+ $extrajoins
+ WHERE rc.capability = :viewcap
+ AND rc.contextid = :syscontextid
+ $extrawhere
+ AND NOT EXISTS (
+ SELECT 1 FROM {role_capabilities} irc WHERE irc.roleid = rc.roleid AND
+ irc.capability = :anythingcap)
+ ) idlist
+ JOIN {role} r ON r.id = idlist.roleid
+ ORDER BY r.sortorder";
+ $params['syscontextid'] = $systemcontext->id;
+ $params['viewcap'] = 'moodle/course:view';
+ $params['anythingcap'] = 'moodle/site:doanything';
+
+ $rolenames = $DB->get_records_sql_menu($query, $params);
return role_fix_names($rolenames, $context, ROLENAME_ALIAS);
}
<?xml version="1.0" encoding="UTF-8" ?>
-<XMLDB PATH="lib/db" VERSION="20090129" COMMENT="XMLDB file for core Moodle tables"
+<XMLDB PATH="lib/db" VERSION="20090323" COMMENT="XMLDB file for core Moodle tables"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="../../lib/xmldb/xmldb.xsd"
>
<INDEX NAME="roleid-allowassign" UNIQUE="true" FIELDS="roleid, allowassign"/>
</INDEXES>
</TABLE>
- <TABLE NAME="role_allow_override" COMMENT="this defines what role can override what role" PREVIOUS="role_allow_assign" NEXT="role_assignments">
+ <TABLE NAME="role_allow_override" COMMENT="this defines what role can override what role" PREVIOUS="role_allow_assign" NEXT="role_allow_switch">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="roleid"/>
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="id" NEXT="allowoverride"/>
<INDEX NAME="roleid-allowoverride" UNIQUE="true" FIELDS="roleid, allowoverride"/>
</INDEXES>
</TABLE>
- <TABLE NAME="role_assignments" COMMENT="assigning roles to different context" PREVIOUS="role_allow_override" NEXT="role_capabilities">
+ <TABLE NAME="role_allow_switch" COMMENT="This table stores which which other roles a user is allowed to switch to if they have one role." PREVIOUS="role_allow_override" NEXT="role_assignments">
+ <FIELDS>
+ <FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="roleid"/>
+ <FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="The role the user has." PREVIOUS="id" NEXT="allowswitch"/>
+ <FIELD NAME="allowswitch" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="false" ENUM="false" COMMENT="The id of a role that the user is allowed to switch to as a result of having this role." PREVIOUS="roleid"/>
+ </FIELDS>
+ <KEYS>
+ <KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="roleid"/>
+ <KEY NAME="roleid" TYPE="foreign" FIELDS="roleid" REFTABLE="role" REFFIELDS="id" PREVIOUS="primary" NEXT="allowswitch"/>
+ <KEY NAME="allowswitch" TYPE="foreign" FIELDS="allowswitch" REFTABLE="role" REFFIELDS="id" PREVIOUS="roleid"/>
+ </KEYS>
+ <INDEXES>
+ <INDEX NAME="roleid-allowoverride" UNIQUE="true" FIELDS="roleid, allowswitch" COMMENT="Each pair (roleid, allowswitch) must appear at most once."/>
+ </INDEXES>
+ </TABLE>
+ <TABLE NAME="role_assignments" COMMENT="assigning roles to different context" PREVIOUS="role_allow_switch" NEXT="role_capabilities">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" SEQUENCE="true" ENUM="false" NEXT="roleid"/>
<FIELD NAME="roleid" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="id" NEXT="contextid"/>
<FIELD NAME="time" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="courseid" NEXT="laststarttime"/>
<FIELD NAME="laststarttime" TYPE="int" LENGTH="10" NOTNULL="true" UNSIGNED="true" DEFAULT="0" SEQUENCE="false" ENUM="false" PREVIOUS="time" NEXT="info"/>
<FIELD NAME="info" TYPE="char" LENGTH="255" NOTNULL="true" SEQUENCE="false" ENUM="false" PREVIOUS="laststarttime" NEXT="backuptype"/>
- <FIELD NAME="backuptype" TYPE="char" LENGTH="50" NOTNULL="true" SEQUENCE="false" ENUM="false" PREVIOUS="info"/>
+ <FIELD NAME="backuptype" TYPE="char" LENGTH="50" NOTNULL="true" SEQUENCE="false" ENUM="false" PREVIOUS="info"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" NEXT="courseid"/>
upgrade_main_savepoint($result, 2009030501);
}
+ /// MDL-18132 replace the use a new Role allow switch settings page, instead of
+ /// $CFG->allowuserswitchrolestheycantassign
+ if ($result && $oldversion < 2009032000) {
+ /// First create the new table.
+ $table = new xmldb_table('role_allow_switch');
+
+ /// Adding fields to table role_allow_switch
+ $table->add_field('id', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, XMLDB_SEQUENCE, null, null, null);
+ $table->add_field('roleid', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+ $table->add_field('allowswitch', XMLDB_TYPE_INTEGER, '10', XMLDB_UNSIGNED, XMLDB_NOTNULL, null, null, null, null);
+
+ /// Adding keys to table role_allow_switch
+ $table->add_key('primary', XMLDB_KEY_PRIMARY, array('id'));
+ $table->add_key('roleid', XMLDB_KEY_FOREIGN, array('roleid'), 'role', array('id'));
+ $table->add_key('allowswitch', XMLDB_KEY_FOREIGN, array('allowswitch'), 'role', array('id'));
+
+ /// Adding indexes to table role_allow_switch
+ $table->add_index('roleid-allowoverride', XMLDB_INDEX_UNIQUE, array('roleid', 'allowswitch'));
+
+ /// Conditionally launch create table for role_allow_switch
+ if (!$dbman->table_exists($table)) {
+ $dbman->create_table($table);
+ }
+
+ /// Main savepoint reached
+ upgrade_main_savepoint($result, 2009032000);
+ }
+ if ($result && $oldversion < 2009032001) {
+ /// Copy from role_allow_assign into the new table.
+ $DB->execute('INSERT INTO {role_allow_switch} SELECT * FROM {role_allow_assign}');
+
+ /// Unset the config variable used in 1.9.
+ unset_config('allowuserswitchrolestheycantassign');
+
+ /// Main savepoint reached
+ upgrade_main_savepoint($result, 2009032001);
+ }
+
return $result;
}
}
function test_get_users_by_capability() {
- global $CFG;
- // Warning, this method assumes that the standard roles are set up with
- // the standard definitions.
$tablenames = array('capabilities' , 'context', 'role', 'role_capabilities',
'role_allow_assign', 'role_allow_override', 'role_assignments', 'role_context_levels',
'user', 'groups_members', 'cache_flags', 'events_handlers', 'user_lastaccess', 'course');
$this->drop_test_tables($tablenames);
accesslib_clear_all_caches_for_unit_testing();
}
+
+ function test_get_switchable_roles() {
+ global $USER;
+
+ $tablenames = array('role' , 'role_capabilities', 'role_assignments', 'role_allow_switch',
+ 'capabilities', 'context', 'role_names');
+ $this->create_test_tables($tablenames, 'lib');
+
+ $this->switch_to_test_db();
+ $saveduserid = $USER->id;
+
+ // Ensure SYSCONTEXTID is set.
+ get_context_instance(CONTEXT_SYSTEM);
+
+ $contexts = $this->load_test_data('context',
+ array('contextlevel', 'instanceid', 'path', 'depth'), array(
+ 'sys' => array(CONTEXT_SYSTEM, 0, '/' . SYSCONTEXTID, 1),
+ 'cat' => array(CONTEXT_COURSECAT, 66, '/' . SYSCONTEXTID . '/' . (SYSCONTEXTID + 1), 2),
+ 'cou' => array(CONTEXT_COURSE, 666, '/' . SYSCONTEXTID . '/' . (SYSCONTEXTID + 1) . '/' . (SYSCONTEXTID + 2), 3),
+ 'fp' => array(CONTEXT_COURSE, SITEID, '/' . SYSCONTEXTID . '/' . SITEID, 2)));
+ $this->testdb->set_field('context', 'id', SYSCONTEXTID, array('id' => $contexts['sys']->id));
+ $this->testdb->set_field('context', 'id', SYSCONTEXTID + 1, array('id' => $contexts['cat']->id));
+ $this->testdb->set_field('context', 'id', SYSCONTEXTID + 2, array('id' => $contexts['cou']->id));
+ $syscontext = $contexts['sys'];
+ $syscontext->id = SYSCONTEXTID;
+ $context = $contexts['cou'];
+ $context->id = SYSCONTEXTID + 2;
+
+ $this->load_test_data('capabilities',
+ array('name'), array(
+ array('moodle/site:doanything'),
+ array('moodle/course:view')));
+
+ $roles = $this->load_test_data('role',
+ array( 'name', 'shortname', 'description', 'sortorder'), array(
+ 'admin' => array('admin', 'admin', 'not null', 1),
+ 'r1' => array( 'r1', 'r1', 'not null', 2),
+ 'r2' => array( 'r2', 'r2', 'not null', 3),
+ 'funny' => array('funny', 'funny', 'not null', 4)));
+ $adminid = $roles['admin']->id;
+ $r1id = $roles['r1']->id;
+ $r2id = $roles['r2']->id;
+ $funnyid = $roles['funny']->id; // strange role to test that roles with 'moodle/site:doanything' and 'moodle/course:view' are not returned.
+
+ $this->load_test_data('role_capabilities',
+ array('roleid', 'capability', 'contextid', 'permission'), array(
+ array($adminid, 'moodle/site:doanything', SYSCONTEXTID, CAP_ALLOW),
+ array( $r1id, 'moodle/course:view', SYSCONTEXTID + 1, CAP_ALLOW),
+ array( $r2id, 'moodle/course:view', SYSCONTEXTID, CAP_ALLOW),
+ array($funnyid, 'moodle/site:doanything', SYSCONTEXTID, CAP_ALLOW),
+ array($funnyid, 'moodle/course:view', SYSCONTEXTID, CAP_ALLOW)));
+
+ $this->load_test_data('role_assignments',
+ array('userid', 'contextid', 'roleid'), array(
+ array( 1, SYSCONTEXTID, $adminid),
+ array( 2, SYSCONTEXTID + 1 , $r1id),
+ array( 3, SYSCONTEXTID + 2 , $r2id)));
+
+ $this->load_test_data('role_allow_switch',
+ array('roleid', 'allowswitch'), array(
+ array( $r1id , $r2id),
+ array( $r2id , $r1id),
+ array( $r2id , $r2id),
+ array( $r2id , $funnyid)));
+
+ // Admin should be able to switch to any role with 'moodle/course:view' in any context.
+ $this->switch_global_user_id(1);
+ accesslib_clear_all_caches_for_unit_testing();
+ $this->assert(new ArraysHaveSameValuesExpectation(array($r2id)), array_keys(get_switchable_roles($syscontext)));
+ $this->assert(new ArraysHaveSameValuesExpectation(array($r2id)), array_keys(get_switchable_roles($context)));
+ $this->revert_global_user_id();
+
+ // r1 should be able to switch to r2, but this user only has r1 in $context, not $syscontext.
+ $this->switch_global_user_id(2);
+ accesslib_clear_all_caches_for_unit_testing();
+ $this->assert(new ArraysHaveSameValuesExpectation(array()), array_keys(get_switchable_roles($syscontext)));
+ $this->assert(new ArraysHaveSameValuesExpectation(array($r2id)), array_keys(get_switchable_roles($context)));
+ $this->revert_global_user_id();
+
+ // The table says r2 should be able to switch to all of r1, r2 and funny, however, only r2 passes the tests on which roles can be returnd..
+ $this->switch_global_user_id(3);
+ accesslib_clear_all_caches_for_unit_testing();
+ $this->assert(new ArraysHaveSameValuesExpectation(array()), array_keys(get_switchable_roles($syscontext)));
+ $this->assert(new ArraysHaveSameValuesExpectation(array($r2id)), array_keys(get_switchable_roles($context)));
+ $this->revert_global_user_id();
+
+ // Clean up everything we added.
+ $this->revert_to_real_db();
+ $this->drop_test_tables($tablenames);
+ accesslib_clear_all_caches_for_unit_testing();
+ }
}
?>
class UnitTestCaseUsingDatabase extends UnitTestCase {
private $realdb;
protected $testdb;
+ private $realuserid = null;
private $tables = array();
- /**
- * In the constructor, record the max(id) of each test table into a csv file.
- * If this file already exists, it means that a previous run of unit tests
- * did not complete, and has left data undeleted in the DB. This data is then
- * deleted and the file is retained. Otherwise it is created.
- * @throws moodle_exception if CSV file cannot be created
- */
public function __construct($label = false) {
global $DB, $CFG;
+ // Complain if we get this far and $CFG->unittestprefix is not set.
if (empty($CFG->unittestprefix)) {
throw new coding_exception('You cannot use UnitTestCaseUsingDatabase unless you set $CFG->unittestprefix.');
}
+
+ // Only do this after the above text.
parent::UnitTestCase($label);
+ // Create the test DB instance.
$this->realdb = $DB;
$this->testdb = moodle_database::get_driver_instance($CFG->dbtype, $CFG->dblibrary);
$this->testdb->connect($CFG->dbhost, $CFG->dbuser, $CFG->dbpass, $CFG->dbname, $CFG->unittestprefix);
$DB = $this->realdb;
}
+ /**
+ * Switch $USER->id to a test value.
+ * You must remember to switch back using revert_global_user_id() before the end of the test.
+ *
+ * It might be worth making this method do more robuse $USER switching in future,
+ * however, this is sufficient for my needs at present.
+ */
+ protected function switch_global_user_id($userid) {
+ global $USER;
+ if (!is_null($this->realuserid)) {
+ debugging('switch_global_user_id called when $USER->id was already switched to a different value. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
+ } else {
+ $this->realuserid = $USER->id;
+ }
+ $USER->id = $userid;
+ }
+
+ /**
+ * Revert $USER->id to the real value.
+ */
+ protected function revert_global_user_id() {
+ global $USER;
+ if (is_null($this->realuserid)) {
+ debugging('revert_global_user_id called without switch_global_user_id having been called first. This suggest you are doing something wrong and dangerous. Please review your code immediately.', DEBUG_DEVELOPER);
+ } else {
+ $USER->id = $this->realuserid;
+ $this->realuserid = null;
+ }
+ }
+
/**
* Check that the user has not forgotten to clean anything up, and if they
* have, display a rude message and clean it up for them.
*/
private function emergency_clean_up() {
global $DB;
+ $cleanmore = false;
// Check that they did not forget to drop any test tables.
if (!empty($this->tables)) {
// Check that they did not forget to switch page to the real DB.
if ($DB !== $this->realdb) {
- debugging('You did not switch back to the real database in your UnitTestCaseUsingDatabase.', DEBUG_DEVELOPER);
+ debugging('You did not switch back to the real database using revert_to_real_db in your UnitTestCaseUsingDatabase.', DEBUG_DEVELOPER);
$this->revert_to_real_db();
+ $cleanmore = true;
+ }
+
+ // Check for forgetting to call revert_global_user_id.
+ if (!is_null($this->realuserid)) {
+ debugging('You did not switch back to the real $USER->id using revert_global_user_id in your UnitTestCaseUsingDatabase.', DEBUG_DEVELOPER);
+ $this->revert_global_user_id();
+ $cleanmore = true;
+ }
+
+ if ($cleanmore) {
+ accesslib_clear_all_caches_for_unit_testing();
}
}
}
if (has_capability('moodle/role:switchroles', $context)) {
- if (!$roles = get_assignable_roles_for_switchrole($context)) {
+ if (!$roles = get_switchable_roles($context)) {
return ''; // Nothing to show!
}
// unset default user role - it would not work
// This is compared against the values stored in the database to determine
// whether upgrades should be performed (see lib/db/*.php)
- $version = 2009030501; // YYYYMMDD = date of the last version bump
+ $version = 2009032001; // YYYYMMDD = date of the last version bump
// XX = daily increments
$release = '2.0 dev (Build: 20090323)'; // Human-friendly version name