Commit 90f53d9b authored by Cecilia Vela Gurovic's avatar Cecilia Vela Gurovic Committed by Gerrit Code Review

Merge "Bug 1855383: Adding the framework for custom user roles"

parents 041517e3 5e4899cf
......@@ -81,14 +81,15 @@ if (method_exists($authobj, 'change_password')) {
'defaultvalue' => $user->passwordchange,
);
}
$roleelements = array();
if ($USER->get('admin')) {
$elements['staff'] = array(
$roleelements['staff'] = array(
'type' => 'switchbox',
'title' => get_string('sitestaff','admin'),
'defaultvalue' => $user->staff,
'help' => true,
);
$elements['admin'] = array(
$roleelements['admin'] = array(
'type' => 'switchbox',
'title' => get_string('siteadmin','admin'),
'defaultvalue' => $user->admin,
......@@ -105,6 +106,25 @@ $elements['email'] = array(
'email' => true,
),
);
if ($user->roles && isset($user->roles['_site'])) {
foreach ($user->roles['_site'] as $rk => $role) {
$roleelements['userroles_' . $role->id] = array(
'type' => 'switchbox',
'title' => get_string($rk),
'defaultvalue' => $role->active,
);
}
}
if (!empty($roleelements)) {
$elements['roles'] = array(
'type' => 'fieldset',
'collapsible' => true,
'collapsed' => false,
'legend' => get_string('userroles', 'artefact.internal'),
'class' => 'dropdown-group js-dropdown-group',
'elements' => $roleelements,
);
}
$elements['maildisabled'] = array(
'type' => 'switchbox',
'defaultvalue' => get_account_preference($user->id, 'maildisabled'),
......@@ -654,6 +674,16 @@ function edituser_site_submit(Pieform $form, $values) {
);
}
}
$userobj = new User();
$userobj = $userobj->find_by_id($user->id);
foreach ($values as $index => $value) {
if (preg_match('/^userroles_(.*)/', $index, $matches)) {
if ($value != $form->get_element($index)['defaultvalue']) {
$userobj->update_role($matches[1], (int) $value);
}
}
}
unset($userobj);
db_commit();
$SESSION->add_ok_msg(get_string('usersitesettingschanged', 'admin'));
......@@ -806,6 +836,31 @@ $allinstitutions = get_records_assoc('institution', '', '', 'displayname', 'name
$institutionloop = 0;
$institutionlength = count($institutions);
foreach ($institutions as $i) {
$roleelements = array(
$i->institution.'_staff' => array(
'name' => $i->institution.'_staff',
'type' => 'switchbox',
'title' => get_string('institutionstaff','admin'),
'defaultvalue' => $i->staff,
),
$i->institution.'_admin' => array(
'name' => $i->institution.'_admin',
'type' => 'switchbox',
'title' => get_string('institutionadmin','admin'),
'description' => get_string('institutionadmindescription1','admin'),
'defaultvalue' => $i->admin,
),
);
if ($user->roles && isset($user->roles[$i->institution])) {
foreach ($user->roles[$i->institution] as $rk => $role) {
$roleelements[$i->institution . '_roles_' . $role->id] = array(
'name' => $i->institution . '_roles_' . $role->id,
'type' => 'switchbox',
'title' => get_string($rk),
'defaultvalue' => $role->active,
);
}
}
$elements[$i->institution.'_settings'] = array(
'type' => 'fieldset',
'legend' => get_string('institutionsettings', 'admin').' - '.$i->displayname,
......@@ -830,16 +885,13 @@ foreach ($institutions as $i) {
'description' => get_string('institutionstudentiddescription', 'admin'),
'defaultvalue' => $i->studentid,
),
$i->institution.'_staff' => array(
'type' => 'switchbox',
'title' => get_string('institutionstaff','admin'),
'defaultvalue' => $i->staff,
),
$i->institution.'_admin' => array(
'type' => 'switchbox',
'title' => get_string('institutionadmin','admin'),
'description' => get_string('institutionadmindescription1','admin'),
'defaultvalue' => $i->admin,
$i->institution.'_roles' => array(
'type' => 'fieldset',
'collapsible' => true,
'collapsed' => false,
'legend' => get_string('userroles', 'artefact.internal'),
'class' => 'dropdown-group js-dropdown-group',
'elements' => $roleelements,
),
$i->institution.'_submit' => array(
'type' => 'submit',
......@@ -940,6 +992,13 @@ function edituser_institution_submit(Pieform $form, $values) {
db_begin();
delete_records('usr_institution', 'usr', $user->id, 'institution', $i->institution);
insert_record('usr_institution', $newuser);
foreach ($values as $index => $value) {
if (preg_match('/^' . $i->institution . '_roles_(.*)/', $index, $matches)) {
if ($value != $form->get_element($index)['defaultvalue']) {
$user->update_role($matches[1], (int) $value);
}
}
}
if ($newuser->admin) {
activity_add_admin_defaults(array($user->id));
}
......
......@@ -360,6 +360,15 @@ function uploadcsv_validate(Pieform $form, $values) {
$csverrors->add($i, get_string('uploadcsverrorexpirydateinpast', 'admin', $i, $expirydate));
}
}
if (array_key_exists('userroles', $formatkeylookup) && !empty($line[$formatkeylookup['userroles']])) {
$userroles = explode(',', $line[$formatkeylookup['userroles']]);
foreach ($userroles as $roleid => $role) {
$classname = 'UserRole' . ucfirst($role);
if (!class_exists($classname)) {
$csverrors->add($i, get_string('uploadcsverroruserrolemissing', 'admin', $i, $role, ucfirst($role)));
}
}
}
}
// If the admin is trying to overwrite existing users, identified by username,
......@@ -573,6 +582,19 @@ function uploadcsv_submit(Pieform $form, $values) {
}
continue;
}
if ($field == 'userroles') {
if (!empty($record[$formatkeylookup[$field]])) {
$userroles = explode(',', $record[$formatkeylookup[$field]]);
foreach ($userroles as $roleid => $role) {
$userroles[$roleid] = array('role' => $role,
'institution' => '_site',
'active' => 1,
'provisioner' => 'csv');
}
$profilefields->{$field} = $userroles;
}
continue;
}
$profilefields->{$field} = $record[$formatkeylookup[$field]];
}
......
......@@ -118,6 +118,10 @@ foreach ( $element_list as $element => $type ) {
$defaultoption = call_static_method($classname, 'defaultoption');
$items[$element]['defaultvalue'] = $defaultoption;
}
if ($type == 'html' && is_callable(array($classname, 'defaulthtml'))) {
$defaultvalue = call_static_method($classname, 'defaulthtml');
$items[$element]['value'] = $defaultvalue;
}
if ($element == 'socialprofile') {
$items[$element] = ArtefactTypeSocialprofile::render_profile_element();
}
......@@ -209,7 +213,7 @@ $profileform = pieform(array(
function get_desired_fields(&$allfields, $section) {
global $USER;
$desiredfields = array('about' => array('firstname', 'lastname', 'studentid', 'preferredname', 'introduction'),
$desiredfields = array('about' => array('firstname', 'lastname', 'studentid', 'preferredname', 'userroles', 'introduction'),
'contact' => array('email', 'maildisabled', 'officialwebsite', 'personalwebsite', 'blogaddress', 'address', 'town', 'city', 'country', 'homenumber', 'businessnumber', 'mobilenumber', 'faxnumber'),
'social' => array('socialprofile'),
);
......@@ -446,7 +450,7 @@ function profileform_submit(Pieform $form, $values) {
$USER->commit();
}
}
else if (in_array($element, array('maildisabled', 'socialprofile'))) {
else if (in_array($element, array('maildisabled', 'socialprofile', 'userroles'))) {
continue;
}
else {
......
......@@ -76,6 +76,8 @@ $string['pinterest.input'] = 'Pinterest username';
$string['pinterest'] = 'Pinterest';
$string['occupation'] = 'Occupation';
$string['industry'] = 'Industry';
$string['userroles'] = 'User roles';
$string['nospecialroles'] = '<span class="text-midtone">No special roles</span>';
// Field names for view user and search user display
$string['name'] = 'Name';
......
......@@ -39,6 +39,7 @@ class PluginArtefactInternal extends PluginArtefact {
'industry',
'html',
'socialprofile',
'userroles',
);
if (class_exists('PluginArtefactInternalLocal', false)) {
$localtypes = PluginArtefactInternalLocal::get_artefact_types();
......@@ -69,6 +70,7 @@ class PluginArtefactInternal extends PluginArtefact {
'occupation',
'industry',
'socialprofile',
'userroles',
);
if (class_exists('PluginArtefactInternalLocal', false)) {
$localtypes = PluginArtefactInternalLocal::get_profile_artefact_types();
......@@ -92,6 +94,7 @@ class PluginArtefactInternal extends PluginArtefact {
'mobilenumber',
'faxnumber',
'socialprofile',
'userroles',
);
if (class_exists('PluginArtefactInternalLocal', false)) {
$localtypes = PluginArtefactInternalLocal::get_contactinfo_artefact_types();
......@@ -496,6 +499,7 @@ class ArtefactTypeProfile extends ArtefactType {
'occupation' => 'text',
'industry' => 'text',
'maildisabled' => 'html',
'userroles' => 'html',
);
$social = array();
if (get_record('blocktype_installed', 'active', 1, 'name', 'socialprofile')) {
......@@ -1344,3 +1348,46 @@ class ArtefactTypeSocialprofile extends ArtefactTypeProfileField {
}
}
class ArtefactTypeUserroles extends ArtefactTypeProfileField {
public function render_self($options) {
return array('html' => self::defaulthtml());
}
function defaulthtml() {
if ($roles = self::get_multiple()) {
$rolestr = '';
foreach ($roles as $k => $role) {
if ($k !== 0) {
$rolestr .= ', ';
}
$rolestr .= get_string($role, 'artefact.internal');
}
}
else {
$rolestr = get_string('nospecialroles', 'artefact.internal');
}
return $rolestr;
}
function format_result($raw) {
return get_string("userroles.{$raw}");
}
function usersearch_column_structure() {
return array('name' => 'userroles', 'sort' => false, 'template' => 'admin/users/searchuserroles.tpl');
}
function can_be_multiple() {
return true;
}
function get_multiple($userid = null) {
global $USER;
if (!$userid) {
$userid = $USER->get('id');
}
return get_column('usr_roles', 'role', 'usr', $userid);
}
}
......@@ -70,6 +70,7 @@ class User {
'accountprefs' => array(),
'activityprefs' => array(),
'institutions' => array(),
'roles' => array(),
'grouproles' => array(),
'institutiontheme' => null,
'admininstitutions' => array(),
......@@ -118,6 +119,7 @@ class User {
$this->populate($user);
$this->reset_institutions();
$this->reset_roles();
$this->reset_grouproles();
return $this;
}
......@@ -718,6 +720,14 @@ class User {
public function leave_institution($institution) {
if ($institution != 'mahara' && $this->in_institution($institution)) {
// Make inactive any usr_roles for this institution
foreach ($this->roles as $inst => $roles) {
if ($inst == $institution) {
foreach ($roles as $role) {
$this->update_role($role->id, 0);
}
}
}
require_once('institution.php');
$institution = new Institution($institution);
$institution->removeMember($this->to_stdclass());
......@@ -988,6 +998,87 @@ class User {
return $this->institutiontheme;
}
public function get_roletypes() {
$types = array();
foreach (get_declared_classes() as $class) {
if (is_subclass_of($class, 'UserRole')) {
$types[] = $class;
}
}
return $types;
}
public function apply_userrole_method($method, $data) {
$checks = array();
foreach ($this->get_roletypes() as $classname) {
if (method_exists($classname, $method)) {
$ur = new $classname;
$checks[$classname] = $ur->$method($data);
}
}
return $checks;
}
public function reset_roles() {
$sql = "SELECT id, role, usr, provisioner, institution, active FROM {usr_roles} WHERE usr = ?";
$usrroles = get_records_sql_array($sql, array($this->get('id')));
$roles = array();
if ($usrroles) {
foreach ($usrroles as $r) {
if (empty($r->institution)) {
$roles['_site'][$r->role] = $r;
}
else {
$roles[$r->institution][$r->role] = $r;
}
}
}
$this->set('roles', $roles);
}
public function set_roles($roles) {
foreach ($roles as $key => $role) {
$role = (object) $role;
if (isset($role->role) && !empty($role->role)) {
$classname = 'UserRole' . ucfirst($role->role);
$role->usr = $this->id;
$r = new $classname($role);
$r->commit();
}
}
$this->reset_roles();
}
public function update_role($roleid, $state) {
set_field('usr_roles', 'active', $state, 'id', $roleid);
$this->reset_roles();
}
public function get_roles() {
$this->reset_roles();
return $this->roles;
}
public function get_role($role, $institution = '_site', $provisioner = null, $isactive = null) {
$this->reset_roles();
if (isset($this->roles[$institution]) && isset($this->roles[$institution][$role])) {
if ($provisioner !== null && $this->roles[$institution][$role]->provisioner != $provisioner) {
return false;
}
if ($isactive !== null && $this->roles[$institution][$role]->active != (bool)$isactive) {
return false;
}
$classname = 'UserRole' . ucfirst($role);
if (class_exists($classname)) {
return new $classname($this->roles[$institution][$role]);
}
else {
// @TODO - should we remove the role from the db?
}
}
return false;
}
public function reset_grouproles() {
$sql = "SELECT gm.* FROM {group_member} gm
JOIN {group} g ON g.id = gm.group
......@@ -1937,6 +2028,7 @@ class LiveUser extends User {
}
$this->reset_institutions();
$this->reset_roles();
$this->reset_grouproles();
$this->load_views();
$this->store_sessionid();
......@@ -2130,6 +2222,106 @@ class LiveUser extends User {
}
}
abstract class UserRole {
protected $role;
protected $id;
protected $usr;
protected $provisioner='internal';
protected $institution=null;
protected $active;
public function __construct($role, $data=null) {
$this->role = $role;
$id = false;
if (is_object($data) && !empty($data->id)) {
$id = $data->id;
}
else if (is_array($data) && !empty($data['id'])) {
$id = $data['id'];
}
else if (!empty($data) && is_numeric($data)) {
$id = $data;
}
if ($id) {
$data = get_record('usr_roles', 'id', $id);
if (!$data) {
throw new MaharaException('No UserRole with the ID: ' . $id);
}
else if ($data->role != $this->role) {
throw new MaharaException('Fetched data roletype "' . $data->role . '" does not match UserRole class roletype "' . $this->role . '"');
}
}
$data = empty($data) ? array() : (array)$data;
foreach ($data as $field => $value) {
if (property_exists($this, $field)) {
$this->{$field} = $value;
}
}
}
public function get($field) {
if (!property_exists($this, $field)) {
throw new InvalidArgumentException("Field $field wasn't found in class " . get_class($this));
}
return $this->{$field};
}
public function set($field, $value) {
if (property_exists($this, $field)) {
$this->{$field} = $value;
return true;
}
throw new InvalidArgumentException("Field $field wasn't found in class " . get_class($this));
}
public function commit() {
if (empty($this->role)) {
throw new MaharaException('UserRole data needs to contain a role');
}
if (empty($this->usr) || !get_field('usr', 'username', 'deleted', 0, 'id', $this->usr)) {
throw new MaharaException('UserRole data needs to contain a valid usr id');
}
$fordb = new stdClass();
$fordb->usr = $this->usr;
$fordb->role = $this->role;
$fordb->institution = (isset($this->institution) && $this->institution != '_site') ? $this->institution : null;
$fordb->provisioner = isset($this->provisioner) ? $this->provisioner : 'internal';
$fordb->active = isset($this->active) ? $this->active : 1;
if (!empty($this->id)) {
$whereobj = new stdClass();
$whereobj->id = $this->id;
}
else {
$whereobj = clone $fordb;
}
$fordb->ctime = isset($this->ctime) ? $this->ctime : db_format_timestamp(time());
ensure_record_exists('usr_roles', $whereobj, $fordb);
}
public function activate() {
if (!empty($this->id)) {
set_field('usr_roles', 'active', 1, 'id', $this->id);
}
$this->active = 1;
}
public function deactivate() {
if (!empty($this->id)) {
set_field('usr_roles', 'active', 0, 'id', $this->id);
}
$this->active = 0;
}
public function delete() {
if (!empty($this->id)) {
delete_records('usr_roles', 'id', $this->id);
}
}
}
/**
* Indicates whether the site is closed for a user
* @param boolean $isuseradmin Whether the user we're checking for is an admin
......
......@@ -57,7 +57,17 @@ if ($forums) {
foreach ($forums as $forum) {
$forum->feedlink = get_config('wwwroot') . 'interaction/forum/atom.php?type=f&id=' . $forum->id;
$allowunsubscribe = get_config_plugin_instance('interaction_forum', $forum->id, 'allowunsubscribe');
if ($allowunsubscribe) {
// Check if any UserRoles are in play
$checks = $USER->apply_userrole_method('interaction_unsubscribe', array('forum' => $forum->id, 'userid' => $USER->get('id')));
foreach ($checks as $check) {
if ($check['can_unsubscribe'] === false) {
// A UserRole is stopping us from unsubscribing
$allowunsubscribe = false;
break;
}
}
}
if ($membership) {
$forum->subscribe = pieform(array(
'name' => 'subscribe_forum' . ($i == 0 ? '' : $i),
......
......@@ -120,6 +120,7 @@ Attachments:
%s";
$string['forumsettings'] = 'Forum settings';
$string['forumsuccessfulsubscribe'] = 'Forum subscribed successfully';
$string['forumfailunsubscribe'] = 'You are not allowed to unsubscribe.';
$string['forumsuccessfulunsubscribe'] = 'Forum unsubscribed successfully';
$string['gotoforums'] = 'Go to forums';
$string['groupadmins'] = 'Group administrators';
......
......@@ -244,6 +244,14 @@ EOF;
}
}
// Check if any UserRoles are in play
foreach ($USER->get_roletypes() as $classname) {
if (method_exists($classname, 'interaction_subscribe')) {
$ur = new $classname;
$ur->interaction_subscribe(array('id' => $instance->get('id')));
}
}
// Moderators
delete_records(
'interaction_forum_moderator',
......@@ -1693,12 +1701,27 @@ function subscribe_forum_submit(Pieform $form, $values) {
$SESSION->add_ok_msg(get_string('forumsuccessfulsubscribe', 'interaction.forum'));
}
else {
delete_records(
'interaction_forum_subscription_forum',
'forum', $values['forum'],
'user', $USER->get('id')
);
$SESSION->add_ok_msg(get_string('forumsuccessfulunsubscribe', 'interaction.forum'));
$can_unsubscribe = true;
// Check if any UserRoles are in play
$checks = $USER->apply_userrole_method('interaction_unsubscribe', array('forum' => $values['forum'], 'userid' => $USER->get('id')));
foreach ($checks as $check) {
if ($check['can_unsubscribe'] === false) {
// A UserRole is stopping us from unsubscribing
$can_unsubscribe = false;
break;
}
}
if ($can_unsubscribe) {
delete_records(
'interaction_forum_subscription_forum',
'forum', $values['forum'],
'user', $USER->get('id')
);
$SESSION->add_ok_msg(get_string('forumsuccessfulunsubscribe', 'interaction.forum'));
}
else {
$SESSION->add_error_msg(get_string('forumfailunsubscribe', 'interaction.forum'));
}
}
if ($values['redirect'] == 'index') {
redirect('/interaction/forum/index.php?group=' . $values['group']);
......
......@@ -686,6 +686,7 @@ $string['uploadcsverrorremoteusertaken'] = 'Line %s of the file specifies a remo
$string['uploadcsverrorusernotininstitution'] = 'Error on line %s: The user "%s" is not a member of the institution %s.';
$string['uploadcsverroruserinaninstitution'] = 'Error on line %s: The user "%s" is a member of the following institutions: %s. You cannot update this user\'s authentication method to "No Institution".';
$string['uploadcsverrorinvalidexpirydate'] = 'Error on line %s: The expiry "%s" is invalid. Please use a valid date format.';
$string['uploadcsverroruserrolemissing'] = 'Error on line %s: The class for the user role "%s" is missing. Please make sure the "UserRole%s" class exists and is accessible.';
$string['uploadcsverrorexpirydateinpast'] = 'Error on line %s: The expiry "%s" cannot be in the past.';
$string['uploadcsvpagedescription6'] = '<p>Here you can upload new users via a <acronym title="Comma Separated Values">CSV</acronym> file.</p>
......
......@@ -1440,5 +1440,20 @@
<KEY NAME="newauthfk" TYPE="foreign" FIELDS="new_authinstance" REFTABLE="auth_instance" REFFIELDS="id"/>
</KEYS>
</TABLE>
<TABLE NAME="usr_roles">
<FIELDS>
<FIELD NAME="id" TYPE="int" LENGTH="10" SEQUENCE="true" NOTNULL="true" />
<FIELD NAME="usr" TYPE="int" LENGTH="10" NOTNULL="true" />
<FIELD NAME="role" TYPE="char" LENGTH="255" NOTNULL="true" />
<FIELD NAME="ctime" TYPE="datetime" NOTNULL="true" />
<FIELD NAME="provisioner" TYPE="char" LENGTH="255" NOTNULL="true"/>
<FIELD NAME="institution" TYPE="char" LENGTH="255" NOTNULL="false"/>
<FIELD NAME="active" TYPE="int" LENGTH="1" NOTNULL="true" DEFAULT="1"/>
</FIELDS>
<KEYS>
<KEY NAME="primary" TYPE="primary" FIELDS="id" />
<KEY NAME="usrfk" TYPE="foreign" FIELDS="usr" REFTABLE="usr" REFFIELDS="id"/>
</KEYS>
</TABLE>
</TABLES>
</XMLDB>
......@@ -1614,5 +1614,23 @@ function xmldb_core_upgrade($oldversion=0) {
}
}
if ($oldversion < 2020011700) {
log_debug('Adding in new usr_roles table with different structure');
$table = new XMLDBTable('usr_roles');
if (!table_exists($table)) {
$table->addFieldInfo('id', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL, XMLDB_SEQUENCE);
$table->addFieldInfo('usr', XMLDB_TYPE_INTEGER, 10, null, XMLDB_NOTNULL);
$table->addFieldInfo('role', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL);
$table->addFieldInfo('ctime', XMLDB_TYPE_DATETIME, null, null, XMLDB_NOTNULL);
$table->addFieldInfo('provisioner', XMLDB_TYPE_CHAR, 255, null, XMLDB_NOTNULL);
$table->addFieldInfo('institution', XMLDB_TYPE_CHAR, 255);
$table->addFieldInfo('active', XMLDB_TYPE_INTEGER, 1, null, XMLDB_NOTNULL, null, null, null, 1);
$table->addKeyInfo('primary', XMLDB_KEY_PRIMARY, array('id'));
$table->addKeyInfo('usrfk', XMLDB_KEY_FOREIGN, array('usr'), 'usr', array('id'));
create_table($table);
}
}
return $status;
}
......@@ -319,6 +319,8 @@ function group_user_can_assess_submitted_views($groupid, $userid) {
* AccessDeniedException
*/
function group_create($data) {
global $USER;
if (!is_array($data)) {
throw new InvalidArgumentException("group_create: data must be an array, see the doc comment for this "
. "function for details on its format");
......@@ -491,6 +493,8 @@ function group_create($data) {
)
);
}
// Check if any UserRoles are in play
$USER->apply_userrole_method('group_join', array('groupid' => $id, 'ctime' => $data['ctime']));
// Copy views for the new group
$artefactcopies = array();
......@@ -1072,6 +1076,14 @@ function group_user_can_leave($group, $userid=null) {
return ($result[$group->id][$userid] = false);
}
// Check if any UserRoles are in play
$checks = $USER->apply_userrole_method('group_leave', array('groupid' => $group->id, 'userid' => $userid));
foreach ($checks as $check) {
if ($check['can_leave'] === false) {
return ($result[$group->id][$userid] = false);
}
}
return ($result[$group->id][$userid] = true);
}
......@@ -1887,6 +1899,13 @@ function group_get_membersearch_data($results, $group, $query, $membershiptype,
$role = group_user_access($group);
$userid = $USER->get('id');
foreach ($results['data'] as &$r) {
// Check if any UserRoles are in play
$checks = $USER->apply_userrole_method('group_leave', array('groupid' => $group, 'userid' => $r['id']));
foreach ($checks as $check) {
if ($check['can_leave'] === false) {
continue 2;
}
}
if ($role == 'admin' && ($r['id'] != $userid || group_user_can_leave($group, $r['id']))) {
$r['removeform'] = group_get_removeuser_form($r['id'], $group);
}
......@@ -3248,3 +3267,67 @@ function get_group_access_roles() {
}
return $data;
}