Created
February 12, 2018 22:48
-
-
Save skwirrel/968e2d3a8616d9e036a85ecdef2955e0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php | |
// This file is part of Moodle - http://moodle.org/ | |
// | |
// Moodle is free software: you can redistribute it and/or modify | |
// it under the terms of the GNU General Public License as published by | |
// the Free Software Foundation, either version 3 of the License, or | |
// (at your option) any later version. | |
// | |
// Moodle is distributed in the hope that it will be useful, | |
// but WITHOUT ANY WARRANTY; without even the implied warranty of | |
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | |
// GNU General Public License for more details. | |
// | |
// You should have received a copy of the GNU General Public License | |
// along with Moodle. If not, see <http://www.gnu.org/licenses/>. | |
/** | |
* Database enrolment plugin. | |
* | |
* This plugin synchronises enrolment and roles with external database table. | |
* | |
* @package enrol_database | |
* @copyright 2010 Petr Skoda {@link http://skodak.org} | |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
*/ | |
defined('MOODLE_INTERNAL') || die(); | |
/** | |
* Database enrolment plugin implementation. | |
* @author Petr Skoda - based on code by Martin Dougiamas, Martin Langhoff and others | |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later | |
*/ | |
class enrol_database_plugin extends enrol_plugin { | |
/** | |
* Is it possible to delete enrol instance via standard UI? | |
* | |
* @param stdClass $instance | |
* @return bool | |
*/ | |
public function can_delete_instance($instance) { | |
$context = context_course::instance($instance->courseid); | |
if (!has_capability('enrol/database:config', $context)) { | |
return false; | |
} | |
if (!enrol_is_enabled('database')) { | |
return true; | |
} | |
if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) { | |
return true; | |
} | |
//TODO: connect to external system and make sure no users are to be enrolled in this course | |
return false; | |
} | |
/** | |
* Is it possible to hide/show enrol instance via standard UI? | |
* | |
* @param stdClass $instance | |
* @return bool | |
*/ | |
public function can_hide_show_instance($instance) { | |
$context = context_course::instance($instance->courseid); | |
return has_capability('enrol/database:config', $context); | |
} | |
/** | |
* Does this plugin allow manual unenrolment of a specific user? | |
* Yes, but only if user suspended... | |
* | |
* @param stdClass $instance course enrol instance | |
* @param stdClass $ue record from user_enrolments table | |
* | |
* @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment | |
*/ | |
public function allow_unenrol_user(stdClass $instance, stdClass $ue) { | |
if ($ue->status == ENROL_USER_SUSPENDED) { | |
return true; | |
} | |
return false; | |
} | |
/** | |
* Forces synchronisation of user enrolments with external database, | |
* does not create new courses. | |
* | |
* @param stdClass $user user record | |
* @return void | |
*/ | |
public function sync_user_enrolments($user) { | |
global $CFG, $DB; | |
// We do not create courses here intentionally because it requires full sync and is slow. | |
if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) { | |
return; | |
} | |
$table = $this->get_config('remoteenroltable'); | |
$coursefield = trim($this->get_config('remotecoursefield')); | |
$userfield = trim($this->get_config('remoteuserfield')); | |
$rolefield = trim($this->get_config('remoterolefield')); | |
$otheruserfield = trim($this->get_config('remoteotheruserfield')); | |
// Lowercased versions - necessary because we normalise the resultset with array_change_key_case(). | |
$coursefield_l = strtolower($coursefield); | |
$userfield_l = strtolower($userfield); | |
$rolefield_l = strtolower($rolefield); | |
$otheruserfieldlower = strtolower($otheruserfield); | |
$localrolefield = $this->get_config('localrolefield'); | |
$localuserfield = $this->get_config('localuserfield'); | |
$localcoursefield = $this->get_config('localcoursefield'); | |
$unenrolaction = $this->get_config('unenrolaction'); | |
$defaultrole = $this->get_config('defaultrole'); | |
$ignorehidden = $this->get_config('ignorehiddencourses'); | |
if (!is_object($user) or !property_exists($user, 'id')) { | |
throw new coding_exception('Invalid $user parameter in sync_user_enrolments()'); | |
} | |
if (!property_exists($user, $localuserfield)) { | |
debugging('Invalid $user parameter in sync_user_enrolments(), missing '.$localuserfield); | |
$user = $DB->get_record('user', array('id'=>$user->id)); | |
} | |
// Create roles mapping. | |
$allroles = get_all_roles(); | |
if (!isset($allroles[$defaultrole])) { | |
$defaultrole = 0; | |
} | |
$roles = array(); | |
foreach ($allroles as $role) { | |
$roles[$role->$localrolefield] = $role->id; | |
} | |
$roleassigns = array(); | |
$enrols = array(); | |
$instances = array(); | |
if (!$extdb = $this->db_init()) { | |
// Can not connect to database, sorry. | |
return; | |
} | |
// Read remote enrols and create instances. | |
$sql = $this->db_get_sql($table, array($userfield=>$user->$localuserfield), array(), false); | |
if ($rs = $extdb->Execute($sql)) { | |
if (!$rs->EOF) { | |
while ($fields = $rs->FetchRow()) { | |
$fields = array_change_key_case($fields, CASE_LOWER); | |
$fields = $this->db_decode($fields); | |
if (empty($fields[$coursefield_l])) { | |
// Missing course info. | |
continue; | |
} | |
if (!$course = $DB->get_record('course', array($localcoursefield=>$fields[$coursefield_l]), 'id,visible')) { | |
continue; | |
} | |
if (!$course->visible and $ignorehidden) { | |
continue; | |
} | |
if (empty($fields[$rolefield_l]) or !isset($roles[$fields[$rolefield_l]])) { | |
if (!$defaultrole) { | |
// Role is mandatory. | |
continue; | |
} | |
$roleid = $defaultrole; | |
} else { | |
$roleid = $roles[$fields[$rolefield_l]]; | |
} | |
$roleassigns[$course->id][$roleid] = $roleid; | |
if (empty($fields[$otheruserfieldlower])) { | |
$enrols[$course->id][$roleid] = $roleid; | |
} | |
if ($instance = $DB->get_record('enrol', array('courseid'=>$course->id, 'enrol'=>'database'), '*', IGNORE_MULTIPLE)) { | |
$instances[$course->id] = $instance; | |
continue; | |
} | |
$enrolid = $this->add_instance($course); | |
$instances[$course->id] = $DB->get_record('enrol', array('id'=>$enrolid)); | |
} | |
} | |
$rs->Close(); | |
$extdb->Close(); | |
} else { | |
// Bad luck, something is wrong with the db connection. | |
$extdb->Close(); | |
return; | |
} | |
// Enrol user into courses and sync roles. | |
foreach ($roleassigns as $courseid => $roles) { | |
if (!isset($instances[$courseid])) { | |
// Ignored. | |
continue; | |
} | |
$instance = $instances[$courseid]; | |
if (isset($enrols[$courseid])) { | |
if ($e = $DB->get_record('user_enrolments', array('userid' => $user->id, 'enrolid' => $instance->id))) { | |
// Reenable enrolment when previously disable enrolment refreshed. | |
if ($e->status == ENROL_USER_SUSPENDED) { | |
$this->update_user_enrol($instance, $user->id, ENROL_USER_ACTIVE); | |
} | |
} else { | |
$roleid = reset($enrols[$courseid]); | |
$this->enrol_user($instance, $user->id, $roleid, 0, 0, ENROL_USER_ACTIVE); | |
} | |
} | |
if (!$context = context_course::instance($instance->courseid, IGNORE_MISSING)) { | |
// Weird. | |
continue; | |
} | |
$current = $DB->get_records('role_assignments', array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_database', 'itemid'=>$instance->id), '', 'id, roleid'); | |
$existing = array(); | |
foreach ($current as $r) { | |
if (isset($roles[$r->roleid])) { | |
$existing[$r->roleid] = $r->roleid; | |
} else { | |
role_unassign($r->roleid, $user->id, $context->id, 'enrol_database', $instance->id); | |
} | |
} | |
foreach ($roles as $rid) { | |
if (!isset($existing[$rid])) { | |
role_assign($rid, $user->id, $context->id, 'enrol_database', $instance->id); | |
} | |
} | |
} | |
// Unenrol as necessary. | |
$sql = "SELECT e.*, c.visible AS cvisible, ue.status AS ustatus | |
FROM {enrol} e | |
JOIN {course} c ON c.id = e.courseid | |
JOIN {role_assignments} ra ON ra.itemid = e.id | |
LEFT JOIN {user_enrolments} ue ON ue.enrolid = e.id AND ue.userid = ra.userid | |
WHERE ra.userid = :userid AND e.enrol = 'database'"; | |
$rs = $DB->get_recordset_sql($sql, array('userid' => $user->id)); | |
foreach ($rs as $instance) { | |
if (!$instance->cvisible and $ignorehidden) { | |
continue; | |
} | |
if (!$context = context_course::instance($instance->courseid, IGNORE_MISSING)) { | |
// Very weird. | |
continue; | |
} | |
if (!empty($enrols[$instance->courseid])) { | |
// We want this user enrolled. | |
continue; | |
} | |
// Deal with enrolments removed from external table | |
if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { | |
$this->unenrol_user($instance, $user->id); | |
} else if ($unenrolaction == ENROL_EXT_REMOVED_KEEP) { | |
// Keep - only adding enrolments. | |
} else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) { | |
// Suspend users. | |
if ($instance->ustatus != ENROL_USER_SUSPENDED) { | |
$this->update_user_enrol($instance, $user->id, ENROL_USER_SUSPENDED); | |
} | |
if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) { | |
if (!empty($roleassigns[$instance->courseid])) { | |
// We want this "other user" to keep their roles. | |
continue; | |
} | |
role_unassign_all(array('contextid'=>$context->id, 'userid'=>$user->id, 'component'=>'enrol_database', 'itemid'=>$instance->id)); | |
} | |
} | |
} | |
$rs->close(); | |
} | |
/** | |
* Forces synchronisation of all enrolments with external database. | |
* | |
* @param progress_trace $trace | |
* @param null|int $onecourse limit sync to one course only (used primarily in restore) | |
* @return int 0 means success, 1 db connect failure, 2 db read failure | |
*/ | |
public function sync_enrolments(progress_trace $trace, $onecourse = null) { | |
global $CFG, $DB; | |
// We do not create courses here intentionally because it requires full sync and is slow. | |
if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) { | |
$trace->output('User enrolment synchronisation skipped.'); | |
$trace->finished(); | |
return 0; | |
} | |
$trace->output('Starting user enrolment synchronisation...'); | |
if (!$extdb = $this->db_init()) { | |
$trace->output('Error while communicating with external enrolment database'); | |
$trace->finished(); | |
return 1; | |
} | |
// We may need a lot of memory here. | |
core_php_time_limit::raise(); | |
raise_memory_limit(MEMORY_HUGE); | |
$table = $this->get_config('remoteenroltable'); | |
$coursefield = trim($this->get_config('remotecoursefield')); | |
$userfield = trim($this->get_config('remoteuserfield')); | |
$rolefield = trim($this->get_config('remoterolefield')); | |
$otheruserfield = trim($this->get_config('remoteotheruserfield')); | |
// Lowercased versions - necessary because we normalise the resultset with array_change_key_case(). | |
$coursefield_l = strtolower($coursefield); | |
$userfield_l = strtolower($userfield); | |
$rolefield_l = strtolower($rolefield); | |
$otheruserfieldlower = strtolower($otheruserfield); | |
$localrolefield = $this->get_config('localrolefield'); | |
$localuserfield = $this->get_config('localuserfield'); | |
$localcoursefield = $this->get_config('localcoursefield'); | |
$unenrolaction = $this->get_config('unenrolaction'); | |
$defaultrole = $this->get_config('defaultrole'); | |
// Create roles mapping. | |
$allroles = get_all_roles(); | |
if (!isset($allroles[$defaultrole])) { | |
$defaultrole = 0; | |
} | |
$roles = array(); | |
foreach ($allroles as $role) { | |
$roles[$role->$localrolefield] = $role->id; | |
} | |
if ($onecourse) { | |
$sql = "SELECT c.id, c.visible, c.$localcoursefield AS mapping, c.shortname, e.id AS enrolid | |
FROM {course} c | |
LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'database') | |
WHERE c.id = :id"; | |
if (!$course = $DB->get_record_sql($sql, array('id'=>$onecourse))) { | |
// Course does not exist, nothing to sync. | |
return 0; | |
} | |
if (empty($course->mapping)) { | |
// We can not map to this course, sorry. | |
return 0; | |
} | |
if (empty($course->enrolid)) { | |
$course->enrolid = $this->add_instance($course); | |
} | |
$existing = array($course->mapping=>$course); | |
// Feel free to unenrol everybody, no safety tricks here. | |
$preventfullunenrol = false; | |
// Course being restored are always hidden, we have to ignore the setting here. | |
$ignorehidden = false; | |
} else { | |
// Get a list of courses to be synced that are in external table. | |
$externalcourses = array(); | |
$sql = $this->db_get_sql($table, array(), array($coursefield), true); | |
if ($rs = $extdb->Execute($sql)) { | |
if (!$rs->EOF) { | |
while ($mapping = $rs->FetchRow()) { | |
$mapping = reset($mapping); | |
$mapping = $this->db_decode($mapping); | |
if (empty($mapping)) { | |
// invalid mapping | |
continue; | |
} | |
$externalcourses[$mapping] = true; | |
} | |
} | |
$rs->Close(); | |
} else { | |
$trace->output('Error reading data from the external enrolment table'); | |
$extdb->Close(); | |
return 2; | |
} | |
$preventfullunenrol = empty($externalcourses); | |
if ($preventfullunenrol and $unenrolaction == ENROL_EXT_REMOVED_UNENROL) { | |
$trace->output('Preventing unenrolment of all current users, because it might result in major data loss, there has to be at least one record in external enrol table, sorry.', 1); | |
} | |
// First find all existing courses with enrol instance. | |
$existing = array(); | |
$sql = "SELECT c.id, c.visible, c.$localcoursefield AS mapping, e.id AS enrolid, c.shortname | |
FROM {course} c | |
JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'database')"; | |
$rs = $DB->get_recordset_sql($sql); // Watch out for idnumber duplicates. | |
foreach ($rs as $course) { | |
if (empty($course->mapping)) { | |
continue; | |
} | |
$existing[$course->mapping] = $course; | |
unset($externalcourses[$course->mapping]); | |
} | |
$rs->close(); | |
// Add necessary enrol instances that are not present yet. | |
$params = array(); | |
$localnotempty = ""; | |
if ($localcoursefield !== 'id') { | |
$localnotempty = "AND c.$localcoursefield <> :lcfe"; | |
$params['lcfe'] = ''; | |
} | |
$sql = "SELECT c.id, c.visible, c.$localcoursefield AS mapping, c.shortname | |
FROM {course} c | |
LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'database') | |
WHERE e.id IS NULL $localnotempty"; | |
$rs = $DB->get_recordset_sql($sql, $params); | |
foreach ($rs as $course) { | |
if (empty($course->mapping)) { | |
continue; | |
} | |
if (!isset($externalcourses[$course->mapping])) { | |
// Course not synced or duplicate. | |
continue; | |
} | |
$course->enrolid = $this->add_instance($course); | |
$existing[$course->mapping] = $course; | |
unset($externalcourses[$course->mapping]); | |
} | |
$rs->close(); | |
// Print list of missing courses. | |
if ($externalcourses) { | |
$list = implode(', ', array_keys($externalcourses)); | |
$trace->output("error: following courses do not exist - $list", 1); | |
unset($list); | |
} | |
// Free memory. | |
unset($externalcourses); | |
$ignorehidden = $this->get_config('ignorehiddencourses'); | |
} | |
// Sync user enrolments. | |
$sqlfields = array($userfield); | |
if ($rolefield) { | |
$sqlfields[] = $rolefield; | |
} | |
if ($otheruserfield) { | |
$sqlfields[] = $otheruserfield; | |
} | |
foreach ($existing as $course) { | |
if ($ignorehidden and !$course->visible) { | |
continue; | |
} | |
if (!$instance = $DB->get_record('enrol', array('id'=>$course->enrolid))) { | |
continue; // Weird! | |
} | |
$context = context_course::instance($course->id); | |
// Get current list of enrolled users with their roles. | |
$currentroles = array(); | |
$currentenrols = array(); | |
$currentstatus = array(); | |
$usermapping = array(); | |
$sql = "SELECT u.$localuserfield AS mapping, u.id AS userid, ue.status, ra.roleid | |
FROM {user} u | |
JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_database' AND ra.itemid = :enrolid) | |
LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid) | |
WHERE u.deleted = 0"; | |
$params = array('enrolid'=>$instance->id); | |
if ($localuserfield === 'username') { | |
$sql .= " AND u.mnethostid = :mnethostid"; | |
$params['mnethostid'] = $CFG->mnet_localhost_id; | |
} | |
$rs = $DB->get_recordset_sql($sql, $params); | |
foreach ($rs as $ue) { | |
$currentroles[$ue->userid][$ue->roleid] = $ue->roleid; | |
$usermapping[$ue->mapping] = $ue->userid; | |
if (isset($ue->status)) { | |
$currentenrols[$ue->userid][$ue->roleid] = $ue->roleid; | |
$currentstatus[$ue->userid] = $ue->status; | |
} | |
} | |
$rs->close(); | |
// Get list of users that need to be enrolled and their roles. | |
$requestedroles = array(); | |
$requestedenrols = array(); | |
$sql = $this->db_get_sql($table, array($coursefield=>$course->mapping), $sqlfields); | |
if ($rs = $extdb->Execute($sql)) { | |
if (!$rs->EOF) { | |
$usersearch = array('deleted' => 0); | |
if ($localuserfield === 'username') { | |
$usersearch['mnethostid'] = $CFG->mnet_localhost_id; | |
} | |
while ($fields = $rs->FetchRow()) { | |
$fields = array_change_key_case($fields, CASE_LOWER); | |
if (empty($fields[$userfield_l])) { | |
$trace->output("error: skipping user without mandatory $localuserfield in course '$course->mapping'", 1); | |
continue; | |
} | |
$mapping = $fields[$userfield_l]; | |
if (!isset($usermapping[$mapping])) { | |
$usersearch[$localuserfield] = $mapping; | |
if (!$user = $DB->get_record('user', $usersearch, 'id', IGNORE_MULTIPLE)) { | |
$trace->output("error: skipping unknown user $localuserfield '$mapping' in course '$course->mapping'", 1); | |
continue; | |
} | |
$usermapping[$mapping] = $user->id; | |
$userid = $user->id; | |
} else { | |
$userid = $usermapping[$mapping]; | |
} | |
if (empty($fields[$rolefield_l]) or !isset($roles[$fields[$rolefield_l]])) { | |
if (!$defaultrole) { | |
$trace->output("error: skipping user '$userid' in course '$course->mapping' - missing course and default role", 1); | |
continue; | |
} | |
$roleid = $defaultrole; | |
} else { | |
$roleid = $roles[$fields[$rolefield_l]]; | |
} | |
$requestedroles[$userid][$roleid] = $roleid; | |
if (empty($fields[$otheruserfieldlower])) { | |
$requestedenrols[$userid][$roleid] = $roleid; | |
} | |
} | |
} | |
$rs->Close(); | |
} else { | |
$trace->output("error: skipping course '$course->mapping' - could not match with external database", 1); | |
continue; | |
} | |
unset($usermapping); | |
// Enrol all users and sync roles. | |
foreach ($requestedenrols as $userid => $userroles) { | |
foreach ($userroles as $roleid) { | |
if (empty($currentenrols[$userid])) { | |
$this->enrol_user($instance, $userid, $roleid, 0, 0, ENROL_USER_ACTIVE); | |
$currentroles[$userid][$roleid] = $roleid; | |
$currentenrols[$userid][$roleid] = $roleid; | |
$currentstatus[$userid] = ENROL_USER_ACTIVE; | |
$trace->output("enrolling: $userid ==> $course->shortname as ".$allroles[$roleid]->shortname, 1); | |
} | |
} | |
// Reenable enrolment when previously disable enrolment refreshed. | |
if ($currentstatus[$userid] == ENROL_USER_SUSPENDED) { | |
$this->update_user_enrol($instance, $userid, ENROL_USER_ACTIVE); | |
$trace->output("unsuspending: $userid ==> $course->shortname", 1); | |
} | |
} | |
foreach ($requestedroles as $userid => $userroles) { | |
// Assign extra roles. | |
foreach ($userroles as $roleid) { | |
if (empty($currentroles[$userid][$roleid])) { | |
role_assign($roleid, $userid, $context->id, 'enrol_database', $instance->id); | |
$currentroles[$userid][$roleid] = $roleid; | |
$trace->output("assigning roles: $userid ==> $course->shortname as ".$allroles[$roleid]->shortname, 1); | |
} | |
} | |
// Unassign removed roles. | |
foreach ($currentroles[$userid] as $cr) { | |
if (empty($userroles[$cr])) { | |
role_unassign($cr, $userid, $context->id, 'enrol_database', $instance->id); | |
unset($currentroles[$userid][$cr]); | |
$trace->output("unsassigning roles: $userid ==> $course->shortname", 1); | |
} | |
} | |
unset($currentroles[$userid]); | |
} | |
foreach ($currentroles as $userid => $userroles) { | |
// These are roles that exist only in Moodle, not the external database | |
// so make sure the unenrol actions will handle them by setting status. | |
$currentstatus += array($userid => ENROL_USER_ACTIVE); | |
} | |
// Deal with enrolments removed from external table. | |
if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL) { | |
if (!$preventfullunenrol) { | |
// Unenrol. | |
foreach ($currentstatus as $userid => $status) { | |
if (isset($requestedenrols[$userid])) { | |
continue; | |
} | |
$this->unenrol_user($instance, $userid); | |
$trace->output("unenrolling: $userid ==> $course->shortname", 1); | |
} | |
} | |
} else if ($unenrolaction == ENROL_EXT_REMOVED_KEEP) { | |
// Keep - only adding enrolments. | |
} else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) { | |
// Suspend enrolments. | |
foreach ($currentstatus as $userid => $status) { | |
if (isset($requestedenrols[$userid])) { | |
continue; | |
} | |
if ($status != ENROL_USER_SUSPENDED) { | |
$this->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED); | |
$trace->output("suspending: $userid ==> $course->shortname", 1); | |
} | |
if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES) { | |
if (isset($requestedroles[$userid])) { | |
// We want this "other user" to keep their roles. | |
continue; | |
} | |
role_unassign_all(array('contextid'=>$context->id, 'userid'=>$userid, 'component'=>'enrol_database', 'itemid'=>$instance->id)); | |
$trace->output("unsassigning all roles: $userid ==> $course->shortname", 1); | |
} | |
} | |
} | |
} | |
// Close db connection. | |
$extdb->Close(); | |
$trace->output('...user enrolment synchronisation finished.'); | |
$trace->finished(); | |
return 0; | |
} | |
/** | |
* Performs a full sync with external database. | |
* | |
* First it creates new courses if necessary, then | |
* enrols and unenrols users. | |
* | |
* @param progress_trace $trace | |
* @return int 0 means success, 1 db connect failure, 4 db read failure | |
*/ | |
public function sync_courses(progress_trace $trace) { | |
global $CFG, $DB; | |
// Make sure we sync either enrolments or courses. | |
if (!$this->get_config('dbtype') or !$this->get_config('newcoursetable') or !$this->get_config('newcoursefullname') or !$this->get_config('newcourseshortname')) { | |
$trace->output('Course synchronisation skipped.'); | |
$trace->finished(); | |
return 0; | |
} | |
$trace->output('Starting course synchronisation...'); | |
// We may need a lot of memory here. | |
core_php_time_limit::raise(); | |
raise_memory_limit(MEMORY_HUGE); | |
if (!$extdb = $this->db_init()) { | |
$trace->output('Error while communicating with external enrolment database'); | |
$trace->finished(); | |
return 1; | |
} | |
$table = $this->get_config('newcoursetable'); | |
$fullname = trim($this->get_config('newcoursefullname')); | |
$shortname = trim($this->get_config('newcourseshortname')); | |
$idnumber = trim($this->get_config('newcourseidnumber')); | |
$category = trim($this->get_config('newcoursecategory')); | |
// Lowercased versions - necessary because we normalise the resultset with array_change_key_case(). | |
$fullname_l = strtolower($fullname); | |
$shortname_l = strtolower($shortname); | |
$idnumber_l = strtolower($idnumber); | |
$category_l = strtolower($category); | |
$localcategoryfield = $this->get_config('localcategoryfield', 'id'); | |
$defaultcategory = $this->get_config('defaultcategory'); | |
if (!$DB->record_exists('course_categories', array('id'=>$defaultcategory))) { | |
$trace->output("default course category does not exist!", 1); | |
$categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1); | |
$first = reset($categories); | |
$defaultcategory = $first->id; | |
} | |
$sqlfields = array($fullname, $shortname); | |
if ($category) { | |
$sqlfields[] = $category; | |
} | |
if ($idnumber) { | |
$sqlfields[] = $idnumber; | |
} | |
$sql = $this->db_get_sql($table, array(), $sqlfields, true); | |
$createcourses = array(); | |
if ($rs = $extdb->Execute($sql)) { | |
if (!$rs->EOF) { | |
while ($fields = $rs->FetchRow()) { | |
$fields = array_change_key_case($fields, CASE_LOWER); | |
$fields = $this->db_decode($fields); | |
if (empty($fields[$shortname_l]) or empty($fields[$fullname_l])) { | |
$trace->output('error: invalid external course record, shortname and fullname are mandatory: ' . json_encode($fields), 1); // Hopefully every geek can read JS, right? | |
continue; | |
} | |
if ($DB->record_exists('course', array('shortname'=>$fields[$shortname_l]))) { | |
// Already exists, skip. | |
continue; | |
} | |
// Allow empty idnumber but not duplicates. | |
if ($idnumber and $fields[$idnumber_l] !== '' and $fields[$idnumber_l] !== null and $DB->record_exists('course', array('idnumber'=>$fields[$idnumber_l]))) { | |
$trace->output('error: duplicate idnumber, can not create course: '.$fields[$shortname_l].' ['.$fields[$idnumber_l].']', 1); | |
continue; | |
} | |
$course = new stdClass(); | |
$course->fullname = $fields[$fullname_l]; | |
$course->shortname = $fields[$shortname_l]; | |
$course->idnumber = $idnumber ? $fields[$idnumber_l] : ''; | |
if ($category) { | |
if (empty($fields[$category_l])) { | |
// Empty category means use default. | |
$course->category = $defaultcategory; | |
} else if ($coursecategory = $DB->get_record('course_categories', array($localcategoryfield=>$fields[$category_l]), 'id')) { | |
// Yay, correctly specified category! | |
$course->category = $coursecategory->id; | |
unset($coursecategory); | |
} else { | |
// Bad luck, better not continue because unwanted ppl might get access to course in different category. | |
$trace->output('error: invalid category '.$localcategoryfield.', can not create course: '.$fields[$shortname_l], 1); | |
continue; | |
} | |
} else { | |
$course->category = $defaultcategory; | |
} | |
$createcourses[] = $course; | |
} | |
} | |
$rs->Close(); | |
} else { | |
$extdb->Close(); | |
$trace->output('Error reading data from the external course table'); | |
$trace->finished(); | |
return 4; | |
} | |
if ($createcourses) { | |
require_once("$CFG->dirroot/course/lib.php"); | |
$templatecourse = $this->get_config('templatecourse'); | |
$template = false; | |
if ($templatecourse) { | |
if ($template = $DB->get_record('course', array('shortname'=>$templatecourse))) { | |
$template = fullclone(course_get_format($template)->get_course()); | |
if (!isset($template->numsections)) { | |
$template->numsections = course_get_format($template)->get_last_section_number(); | |
} | |
unset($template->id); | |
unset($template->fullname); | |
unset($template->shortname); | |
unset($template->idnumber); | |
} else { | |
$trace->output("can not find template for new course!", 1); | |
} | |
} | |
if (!$template) { | |
$courseconfig = get_config('moodlecourse'); | |
$template = new stdClass(); | |
$template->summary = ''; | |
$template->summaryformat = FORMAT_HTML; | |
$template->format = $courseconfig->format; | |
$template->newsitems = $courseconfig->newsitems; | |
$template->showgrades = $courseconfig->showgrades; | |
$template->showreports = $courseconfig->showreports; | |
$template->maxbytes = $courseconfig->maxbytes; | |
$template->groupmode = $courseconfig->groupmode; | |
$template->groupmodeforce = $courseconfig->groupmodeforce; | |
$template->visible = $courseconfig->visible; | |
$template->lang = $courseconfig->lang; | |
$template->groupmodeforce = $courseconfig->groupmodeforce; | |
} | |
foreach ($createcourses as $fields) { | |
$newcourse = clone($template); | |
$newcourse->fullname = $fields->fullname; | |
$newcourse->shortname = $fields->shortname; | |
$newcourse->idnumber = $fields->idnumber; | |
$newcourse->category = $fields->category; | |
// Detect duplicate data once again, above we can not find duplicates | |
// in external data using DB collation rules... | |
if ($DB->record_exists('course', array('shortname' => $newcourse->shortname))) { | |
$trace->output("can not insert new course, duplicate shortname detected: ".$newcourse->shortname, 1); | |
continue; | |
} else if (!empty($newcourse->idnumber) and $DB->record_exists('course', array('idnumber' => $newcourse->idnumber))) { | |
$trace->output("can not insert new course, duplicate idnumber detected: ".$newcourse->idnumber, 1); | |
continue; | |
} | |
$c = create_course($newcourse); | |
$trace->output("creating course: $c->id, $c->fullname, $c->shortname, $c->idnumber, $c->category", 1); | |
} | |
unset($createcourses); | |
unset($template); | |
} | |
// Close db connection. | |
$extdb->Close(); | |
$trace->output('...course synchronisation finished.'); | |
$trace->finished(); | |
return 0; | |
} | |
/** | |
* Synchronise groupings with external database. | |
* | |
* @param progress_trace $trace | |
* @return int 0 means success, 1 db connect failure, 4 db read failure | |
*/ | |
public function sync_groupings(progress_trace $trace, $onecourse = null) { | |
global $CFG,$DB; | |
$localgroupingfield = $this->get_config('localgroupingfield'); | |
$localcoursefield = $this->get_config('localcoursefield'); | |
$remotegroupingtable = trim($this->get_config('remotegroupingtable')); | |
$remotegroupingidnumberfield = trim($this->get_config('remotegroupingidnumberfield')); | |
$remotegroupingcoursefield = trim($this->get_config('remotegroupingcoursefield')); | |
$remotegroupingnamefield = trim($this->get_config('remotegroupingnamefield')); | |
$remotegroupingdescriptionfield = trim($this->get_config('remotegroupingdescriptionfield')); | |
// This process assumes that courses have been synced first. | |
if ( !strlen($remotegroupingtable) ) { | |
return 0; | |
} | |
// Check that all the requisite config parameters have been defined | |
if ( !(strlen($remotegroupingidnumberfield) && strlen($remotegroupingcoursefield)) ) { | |
$trace->output("error: When specifying a remote grouping table to import groupings you must also set the 'remote grouping ID number' field and 'remote grouping course field'.", 1); | |
return 1; | |
} | |
$trace->output('Starting grouping synchronisation...'); | |
require_once("$CFG->dirroot/group/lib.php"); | |
// We may need a lot of memory here. | |
core_php_time_limit::raise(); | |
raise_memory_limit(MEMORY_HUGE); | |
if (!$extdb = $this->db_init()) { | |
$trace->output('Error while communicating with external enrolment database'); | |
$trace->finished(); | |
return 1; | |
} | |
// Get a list (in memory) of all existing (internal) groupings which have an idNumber. | |
// This list will be keyed on combination of the value of what is in remotegroupingcoursefield and remotegroupingidnumberfield. | |
$sqlparams = array(); | |
$sql = " | |
SELECT g.id, c.$localcoursefield AS courseidentifier, g.idnumber AS groupingidnumber, g.id AS groupingid, MD5(g.name) AS namechecksum, MD5(g.description) AS descriptionchecksum | |
FROM {groupings} g | |
INNER JOIN {course} c ON g.courseid=c.id | |
WHERE g.idnumber <> '' | |
"; | |
if ($onecourse) { | |
$sql .= ' AND c.id=?'; | |
$sqlparams[] = $onecourse; | |
} | |
$currentgroupingsresults = $DB->get_records_sql($sql,$sqlparams); | |
$currentgroupings = array(); | |
foreach ($currentgroupingsresults as $grouping) { | |
$key = rawurlencode($grouping->courseidentifier).':'.rawurlencode($grouping->groupingidnumber); | |
$currentgroupings[$key] = array( $grouping->groupingid, $grouping->namechecksum, $grouping->descriptionchecksum ); | |
} | |
// Query the external groupings table to pull out all groupings. | |
$sqlfields = array( | |
'idnumber'=>$remotegroupingidnumberfield, | |
'course'=>$remotegroupingcoursefield, | |
); | |
if (strlen($remotegroupingnamefield)) $sqlfields['name']=$remotegroupingnamefield; | |
if (strlen($remotegroupingdescriptionfield)) $sqlfields['description']=$remotegroupingdescriptionfield; | |
$sql = $this->db_get_sql($remotegroupingtable, array(), $sqlfields); | |
if (!($rs = $extdb->Execute($sql))) { | |
$trace->output("warning: Could not retrieve any groupings from the external database"); | |
$extdb->Close(); | |
return 0; | |
} | |
if ($onecourse) { | |
// get the value used to identify the one course we are syncing... | |
if (!$onecourseidentifier = $DB->get_record('course', array('id'=>$onecourse), $localcoursefield)) { | |
$trace->output("warning: Could not find the course to be synced (id={$onecourse}) when syncing just one course"); | |
$rs->close(); | |
$extdb->Close(); | |
return 0; | |
} | |
$onecourseidentifier = $onecourseidentifier->$localcoursefield; | |
} | |
$invalidatecacheforcourses = array(); | |
$stats = array( 'updated'=>0, 'created'=>0, 'deleted'=>0 ); | |
// Iterate across groupings from the external table. | |
while ($remoterecord = $rs->FetchRow()) { | |
// If we're only doing one course and this grouping isn't in that course then skip it | |
if ($onecourse && $onecourseidentifier!=$remoterecord['course']) continue; | |
if (!strlen($remotegroupingnamefield)) $remoterecord['name']=$remoterecord['idnumber']; | |
// before we do anything check that the course exists | |
$course = $DB->get_record('course', array($localcoursefield=>$remoterecord['course']), 'id'); | |
if (!$course) { | |
$trace->output("warning: Grouping \"{$remoterecord['name']}\" was not imported because it belongs to a non-existant course ({$remoterecord['course']})"); | |
continue; | |
} | |
$courseid = $course->id; | |
// See if external grouping already exists in the internal db. | |
$lookupkey = rawurlencode($remoterecord['course']).':'.rawurlencode($remoterecord['idnumber']); | |
if (!isset($currentgroupings[$lookupkey])) { | |
// This is a new grouping - better create it. | |
// Make sure the grouping name doesn't clash with an existing grouping on this course (without an idnumber). | |
if (groups_get_grouping_by_name($courseid,$remoterecord['name'])) { | |
$trace->output("warning: Grouping \"{$remoterecord['name']}\" (course={$remoterecord['course']}, idnumer={$remoterecord['idnumber']}) was not imported because an existing grouping exists with the same name but no idNumber"); | |
continue; | |
} | |
// So now we can finally create the new grouping. | |
$data = new stdClass(); | |
$data->courseid = $courseid; | |
$data->idnumber = $remoterecord['idnumber']; | |
$data->name = $remoterecord['name']; | |
$data->description = strlen($remotegroupingdescriptionfield) ? $remoterecord['description'] : ''; | |
// TODO MDL-51202 groupings should probably belong to this component. | |
groups_create_grouping($data); | |
$stats['created']++; | |
} else { | |
// Check that we haven't already encountered this grouping during this import... | |
if (!is_array($currentgroupings[$lookupkey])) { | |
$trace->output("warning: Grouping \"{$remoterecord['name']}\" (course={$remoterecord['course']}, idnumer={$remoterecord['idnumber']}) is duplicated in the remote database - skipping this duplicate"); | |
continue; | |
} | |
// Grouping already exists - check if name or description need updating. | |
$updates = array(); | |
// See if grouping name from external table has changed. | |
if (strlen($remotegroupingnamefield)) { | |
if (md5($remoterecord['name'])<>$currentgroupings[$lookupkey][1]) { | |
$updates['name']=$remoterecord['name']; | |
} | |
} | |
// See if grouping description from external table has changed. | |
if (strlen($remotegroupingdescriptionfield)) { | |
if (md5($remoterecord['description'])<>$currentgroupings[$lookupkey][2]) { | |
$updates['description']=$remoterecord['description']; | |
} | |
} | |
if (count($updates)) { | |
$updates['id'] = $currentgroupings[$lookupkey][0]; | |
$DB->update_record('groupings', $updates); | |
$stats['updated']++; | |
// The update_record doesn't seem to invalidate cache so we need to do that expressly. | |
$invalidatecacheforcourses[$courseid]=true; | |
} | |
// Mark this entry as done in the in-memory list of internal groupings. | |
$currentgroupings[$lookupkey] = ''; | |
} | |
} | |
$rs->close(); | |
// Delete any previously imported groupings which are no longer in external db | |
// Iterate across all records remaining in the in-memory list of groupings | |
foreach( $currentgroupings as $record ) { | |
if (!is_array($record)) continue; | |
// See if there are any non-imported (i.e. without an idnumber) groups in this grouping | |
$sql = " | |
SELECT g.id | |
FROM {groupings_groups} gg | |
INNER JOIN {groups} g ON g.id=gg.groupid | |
WHERE gg.groupingid = :groupingid AND g.idnumber='' | |
LIMIT 1 | |
"; | |
if ( $DB->get_record_sql($sql, array('groupingid'=>$record[0])) ) { | |
// DO NOT DELETE THE GROUPING - the fact that the user has appropriated this grouping for some other purpose means we should keep it alive | |
} else { | |
groups_delete_grouping( $record[0] ); | |
$stats['deleted']++; | |
} | |
} | |
// Invalidate the cache for any courses we updated. | |
foreach ($invalidatecacheforcourses as $courseid=>$notused) { | |
cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); | |
} | |
// Close the external DB connection. | |
$extdb->Close(); | |
$trace->output("info: Finished groupings sync. Created: \"{$stats['created']}\", Updated:\"{$stats['updated']}\", Deleted:\"{$stats['deleted']}\""); | |
return 0; | |
} | |
/** | |
* Get a list of the groupingid's for the groupings which a group belongs to | |
* | |
* @param int Group ID to lookup | |
* @return array List of grouping id's | |
*/ | |
public function get_groupings_for_group( $groupid ) { | |
global $DB; | |
$sqlparams = array('groupid'=>$groupid); | |
$sql = " | |
SELECT groupingid | |
FROM {groupings_groups} gg | |
WHERE gg.groupid=:groupid | |
"; | |
$currentgroupingsresults = $DB->get_records_sql($sql,$sqlparams); | |
$currentgroupings = array(); | |
foreach ($currentgroupingsresults as $grouping) { | |
if (!$grouping->groupingid) continue; | |
$currentgroupings[] = $grouping->groupingid; | |
} | |
return $currentgroupings; | |
} | |
/** | |
* Synchronise groups with external database. | |
* | |
* @param progress_trace $trace | |
* @return int 0 means success, 1 db connect failure, 4 db read failure | |
*/ | |
public function sync_groups(progress_trace $trace, $onecourse = null) { | |
global $CFG,$DB; | |
$localgroupingfield = $this->get_config('localgroupingfield'); | |
$localcoursefield = $this->get_config('localcoursefield'); | |
$remotegrouptable = trim($this->get_config('remotegrouptable')); | |
$remotegroupidnumberfield = trim($this->get_config('remotegroupidnumberfield')); | |
$remotegroupcoursefield = trim($this->get_config('remotegroupcoursefield')); | |
$remotegroupnamefield = trim($this->get_config('remotegroupnamefield')); | |
$remotegroupdescriptionfield = trim($this->get_config('remotegroupdescriptionfield')); | |
$remotegroupgroupingfield = trim($this->get_config('remotegroupgroupingfield')); | |
// This process assumes that courses and groupings have been synced first. | |
if ( !strlen($remotegrouptable) ) { | |
return 0; | |
} | |
// Check that all the requisite config parameters have been defined | |
if ( !(strlen($remotegroupidnumberfield) && strlen($remotegroupcoursefield)) ) { | |
$trace->output("error: When specifying a remote group table to import groups you must also set the 'remote group ID number' field and 'remote group course field'.", 1); | |
return 1; | |
} | |
$trace->output('Starting group synchronisation...'); | |
require_once("$CFG->dirroot/group/lib.php"); | |
// We may need a lot of memory here. | |
core_php_time_limit::raise(); | |
raise_memory_limit(MEMORY_HUGE); | |
if (!$extdb = $this->db_init()) { | |
$trace->output('Error while communicating with external enrolment database'); | |
$trace->finished(); | |
return 1; | |
} | |
// Get a list (in memory) of all existing (internal) groups which have an idNumber. | |
// This list will be keyed on combination of the value of what is in remotegroupingcoursefield and remotegroupidnumberfield. | |
$sqlparams = array(); | |
$sql = " | |
SELECT g.id, c.$localcoursefield AS courseidentifier, g.idnumber AS groupidnumber, g.id AS groupid, MD5(g.name) AS namechecksum, MD5(g.description) AS descriptionchecksum | |
FROM {groups} g | |
INNER JOIN {course} c ON g.courseid=c.id | |
WHERE g.idnumber <> '' | |
"; | |
if ($onecourse) { | |
$sql .= ' AND c.id=?'; | |
$sqlparams[] = $onecourse; | |
} | |
$currentgroupsresults = $DB->get_records_sql($sql,$sqlparams); | |
$currentgroups = array(); | |
foreach ($currentgroupsresults as $group) { | |
$key = rawurlencode($group->courseidentifier).':'.rawurlencode($group->groupidnumber); | |
$currentgroups[$key] = array( $group->groupid, $group->namechecksum, $group->descriptionchecksum ); | |
} | |
// Query the external groups table to pull out all groups. | |
$sqlfields = array( | |
'idnumber'=>$remotegroupidnumberfield, | |
'course'=>$remotegroupcoursefield, | |
); | |
if (strlen($remotegroupdescriptionfield)) $sqlfields['description']=$remotegroupdescriptionfield; | |
if (strlen($remotegroupnamefield)) $sqlfields['name']=$remotegroupnamefield; | |
if (strlen($remotegroupgroupingfield)) $sqlfields['grouping']=$remotegroupgroupingfield; | |
$sql = $this->db_get_sql($remotegrouptable, array(), $sqlfields); | |
if (!($rs = $extdb->Execute($sql))) { | |
$trace->output("warning: Could not retrieve any groups from the external database"); | |
$extdb->Close(); | |
return 0; | |
} | |
if ($onecourse) { | |
// get the value used to identify the one course we are syncing... | |
if (!$onecourseidentifier = $DB->get_record('course', array('id'=>$onecourse), $localcoursefield)) { | |
$trace->output("warning: Could not find the course to be synced (id={$onecourse}) when syncing just one course"); | |
$rs->close(); | |
$extdb->Close(); | |
return 0; | |
} | |
$onecourseidentifier = $onecourseidentifier->$localcoursefield; | |
} | |
$invalidatecacheforcourses = array(); | |
$stats = array( 'updated'=>0, 'created'=>0, 'deleted'=>0 ); | |
// Iterate across groups from the external table. | |
while ($remoterecord = $rs->FetchRow()) { | |
// If we're only doing one course and this group isn't in that course then skip it | |
if ($onecourse && $onecourseidentifier!=$remoterecord['course']) continue; | |
if (!strlen($remotegroupnamefield)) $remoterecord['name']=$remoterecord['idnumber']; | |
// before we do anything check that the course exists | |
$course = $DB->get_record('course', array($localcoursefield=>$remoterecord['course']), 'id'); | |
if (!$course) { | |
$trace->output("warning: Group \"{$remoterecord['name']}\" was not imported because it belongs to a non-existant course ({$remoterecord['course']})"); | |
continue; | |
} | |
$courseid = $course->id; | |
$groupingid=0; | |
if (strlen($remotegroupgroupingfield) && strlen($remoterecord['grouping'])) { | |
// Check that the grouping referenced in the external group table actually exists in the internal course table. | |
$grouping = $DB->get_record('groupings', array('courseid'=>$course->id, $localgroupingfield=>$remoterecord['grouping']), 'id'); | |
if (!$grouping) { | |
$trace->output("warning: Group \"{$remoterecord['name']}\" won't be added to the \"{$remoterecord['grouping']}\" grouping because that grouping doesn't exist on this course ({$remoterecord['course']})"); | |
} else { | |
$groupingid=$grouping->id; | |
} | |
} | |
// See if external group already exists in the internal db. | |
$lookupkey = rawurlencode($remoterecord['course']).':'.rawurlencode($remoterecord['idnumber']); | |
if (!isset($currentgroups[$lookupkey])) { | |
// This is a new group - better create it. | |
// Make sure the group name doesn't clash with an existing group on this course (without an idnumber). | |
if (groups_get_group_by_name($courseid,$remoterecord['name'])) { | |
$trace->output("warning: Group \"{$remoterecord['name']}\" (course={$remoterecord['course']}, idnumer={$remoterecord['idnumber']}) was not imported because an existing group exists with the same name but no idNumber"); | |
continue; | |
} | |
// So now we can finally create the new group. | |
$data = new stdClass(); | |
$data->courseid = $courseid; | |
$data->idnumber = $remoterecord['idnumber']; | |
$data->name = $remoterecord['name']; | |
$data->description = strlen($remotegroupdescriptionfield) ? $remoterecord['description'] : ''; | |
// TODO MDL-51202 groups should belong to this component. | |
$groupid = groups_create_group($data); | |
$stats['created']++; | |
if ($groupingid) { | |
// Need to add this new group to the specified grouping. | |
groups_assign_grouping($groupingid, $groupid); | |
} | |
} else { | |
// Check that we haven't already encountered this group during this import... | |
if (!is_array($currentgroups[$lookupkey])) { | |
$trace->output("warning: Grouping \"{$remoterecord['name']}\" (course={$remoterecord['course']}, idnumer={$remoterecord['idnumber']}) is duplicated in the remote database - skipping this duplicate"); | |
continue; | |
} | |
// Pull out the group ID for easy access. | |
$groupid = $currentgroups[$lookupkey][0]; | |
// Group already exists - check if name or description need updating. | |
$updates = array(); | |
// See if group name from external table has changed. | |
if (strlen($remotegroupnamefield)) { | |
if (md5($remoterecord['name'])<>$currentgroups[$lookupkey][1]) { | |
$updates['name']=$remoterecord['name']; | |
} | |
} | |
// See if group description from external table has changed. | |
if (strlen($remotegroupdescriptionfield)) { | |
if (md5($remoterecord['description'])<>$currentgroups[$lookupkey][2]) { | |
$updates['description']=$remoterecord['description']; | |
} | |
} | |
$updated = false; | |
if (count($updates)) { | |
$updates['id'] = $groupid; | |
$DB->update_record('groups', $updates); | |
$updated = true; | |
// The update_record doesn't seem to invalidate cache so we need to do that expressly. | |
$invalidatecacheforcourses[$courseid]=true; | |
} | |
if (strlen($remotegroupgroupingfield)) { | |
// Need to sort out the groupings. | |
// First get a list of current grouping memberships for this group. | |
$groupingmembershipstodelete = $this->get_groupings_for_group($groupid); | |
if ($groupingid) { | |
// See if this group is already in this grouping. | |
if (in_array( $groupingid, $groupingmembershipstodelete )) { | |
// Don't delete the membership for this group. | |
$groupingmembershipstodelete = array_diff( $groupingmembershipstodelete, array( $groupingid ) ); | |
} else { | |
// Group isn't currently a member of this grouping - better add it in. | |
groups_assign_grouping($groupingid, $groupid); | |
$updated = true; | |
} | |
} | |
// Now delete any unwanted grouping memberships. | |
foreach ( $groupingmembershipstodelete as $groupingidtodelete ) { | |
groups_unassign_grouping($groupingidtodelete, $groupid); | |
$updated = true; | |
} | |
} | |
// Mark this entry as done in the in-memory list of internal groupings. | |
$currentgroups[$lookupkey] = ''; | |
if ($updated) $stats['updated']++; | |
} | |
} | |
$rs->close(); | |
// Delete any previously imported groups which are no longer in external db | |
// Iterate across all records remaining in the in-memory list of groups | |
foreach( $currentgroups as $record ) { | |
if (!is_array($record)) continue; | |
groups_delete_group( $record[0] ); | |
$stats['deleted']++; | |
} | |
// Invalidate the cache for any courses we updated. | |
foreach ($invalidatecacheforcourses as $courseid=>$notused) { | |
cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($courseid)); | |
} | |
// Close the external DB connection. | |
$extdb->Close(); | |
$trace->output("info: Finished group sync. Created: \"{$stats['created']}\", Updated:\"{$stats['updated']}\", Deleted:\"{$stats['deleted']}\""); | |
return 0; | |
} | |
/** | |
* Synchronise group membership with external database. | |
* | |
* @param progress_trace $trace | |
* @return int 0 means success, 1 db connect failure, 4 db read failure | |
*/ | |
public function sync_group_membership(progress_trace $trace) { | |
global $DB; | |
$localgroupingfield = $this->get_config('localgroupingfield'); | |
$localgroupfield = $this->get_config('localgroupfield'); | |
$localuserfield = $this->get_config('localuserfield'); | |
$localcoursefield = $this->get_config('localcoursefield'); | |
$remotegroupmembershiptable = trim($this->get_config('remotegroupmembershiptable')); | |
$remotegroupmembershipuserfield = trim($this->get_config('remotegroupmembershipuserfield')); | |
$remotegroupmembershipgroupfield = trim($this->get_config('remotegroupmembershipgroupfield')); | |
$remotegroupmembershipcoursefield = trim($this->get_config('remotegroupmembershipcoursefield')); | |
// This process assumes that courses and groupings and groups have been synced first. | |
if ( !strlen($remotegroupmembershiptable) ) { | |
return 0; | |
} | |
// Check that all the requisite config parameters have been defined | |
// We don't need course ID if we are using group ID's since these are globally unique | |
if ( !(strlen($remotegroupidnumberfield) && (strlen($remotegroupcoursefield) || $remotegroupidnumberfield=='id')) ) { | |
$trace->output("error: When specifying a remote group table to import group membership you must also set the 'remote group membership group field' and, if this isn't 'id', then also the 'remote group membership course field'.", 1); | |
return 1; | |
} | |
$trace->output('Starting group membership synchronisation...'); | |
require_once("$CFG->dirroot/group/lib.php"); | |
// We may need a lot of memory here. | |
core_php_time_limit::raise(); | |
raise_memory_limit(MEMORY_HUGE); | |
if (!$extdb = $this->db_init()) { | |
$trace->output('Error while communicating with external enrolment database'); | |
$trace->finished(); | |
return 1; | |
} | |
// Get a list (in memory) of all existing (internal) group memberships for any groups with an idnumber | |
// THIS IS AS FAR AS I HAVE GOT SO FAR | |
// NEED TO COMPLETE THIS FUNCTION | |
} | |
protected function db_get_sql($table, array $conditions, array $fields, $distinct = false, $sort = "") { | |
if ($fields) { | |
$fieldlist = ''; | |
foreach ($fields as $key=>$value) { | |
$fieldlist .= $value; | |
// If key is non-numeric then use this as an alias | |
if (is_string($key)) { | |
$fieldlist .= " AS $key"; | |
} | |
$fieldlist .= ','; | |
} | |
$fields = substr($fieldlist,0,-1); | |
} else { | |
$fields = '*'; | |
} | |
$where = array(); | |
if ($conditions) { | |
foreach ($conditions as $key=>$value) { | |
$value = $this->db_encode($this->db_addslashes($value)); | |
$where[] = "$key = '$value'"; | |
} | |
} | |
$where = $where ? "WHERE ".implode(" AND ", $where) : ""; | |
$sort = $sort ? "ORDER BY $sort" : ""; | |
$distinct = $distinct ? "DISTINCT" : ""; | |
$sql = "SELECT $distinct $fields | |
FROM $table | |
$where | |
$sort"; | |
return $sql; | |
} | |
/** | |
* Tries to make connection to the external database. | |
* | |
* @return null|ADONewConnection | |
*/ | |
protected function db_init() { | |
global $CFG; | |
require_once($CFG->libdir.'/adodb/adodb.inc.php'); | |
// Connect to the external database (forcing new connection). | |
$extdb = ADONewConnection($this->get_config('dbtype')); | |
if ($this->get_config('debugdb')) { | |
$extdb->debug = true; | |
ob_start(); // Start output buffer to allow later use of the page headers. | |
} | |
// The dbtype my contain the new connection URL, so make sure we are not connected yet. | |
if (!$extdb->IsConnected()) { | |
$result = $extdb->Connect($this->get_config('dbhost'), $this->get_config('dbuser'), $this->get_config('dbpass'), $this->get_config('dbname'), true); | |
if (!$result) { | |
return null; | |
} | |
} | |
$extdb->SetFetchMode(ADODB_FETCH_ASSOC); | |
if ($this->get_config('dbsetupsql')) { | |
$extdb->Execute($this->get_config('dbsetupsql')); | |
} | |
return $extdb; | |
} | |
protected function db_addslashes($text) { | |
// Use custom made function for now - it is better to not rely on adodb or php defaults. | |
if ($this->get_config('dbsybasequoting')) { | |
$text = str_replace('\\', '\\\\', $text); | |
$text = str_replace(array('\'', '"', "\0"), array('\\\'', '\\"', '\\0'), $text); | |
} else { | |
$text = str_replace("'", "''", $text); | |
} | |
return $text; | |
} | |
protected function db_encode($text) { | |
$dbenc = $this->get_config('dbencoding'); | |
if (empty($dbenc) or $dbenc == 'utf-8') { | |
return $text; | |
} | |
if (is_array($text)) { | |
foreach($text as $k=>$value) { | |
$text[$k] = $this->db_encode($value); | |
} | |
return $text; | |
} else { | |
return core_text::convert($text, 'utf-8', $dbenc); | |
} | |
} | |
protected function db_decode($text) { | |
$dbenc = $this->get_config('dbencoding'); | |
if (empty($dbenc) or $dbenc == 'utf-8') { | |
return $text; | |
} | |
if (is_array($text)) { | |
foreach($text as $k=>$value) { | |
$text[$k] = $this->db_decode($value); | |
} | |
return $text; | |
} else { | |
return core_text::convert($text, $dbenc, 'utf-8'); | |
} | |
} | |
/** | |
* Automatic enrol sync executed during restore. | |
* @param stdClass $course course record | |
*/ | |
public function restore_sync_course($course) { | |
$trace = new null_progress_trace(); | |
$this->sync_enrolments($trace, $course->id); | |
} | |
/** | |
* Restore instance and map settings. | |
* | |
* @param restore_enrolments_structure_step $step | |
* @param stdClass $data | |
* @param stdClass $course | |
* @param int $oldid | |
*/ | |
public function restore_instance(restore_enrolments_structure_step $step, stdClass $data, $course, $oldid) { | |
global $DB; | |
if ($instance = $DB->get_record('enrol', array('courseid'=>$course->id, 'enrol'=>$this->get_name()))) { | |
$instanceid = $instance->id; | |
} else { | |
$instanceid = $this->add_instance($course); | |
} | |
$step->set_mapping('enrol', $oldid, $instanceid); | |
} | |
/** | |
* Restore user enrolment. | |
* | |
* @param restore_enrolments_structure_step $step | |
* @param stdClass $data | |
* @param stdClass $instance | |
* @param int $oldinstancestatus | |
* @param int $userid | |
*/ | |
public function restore_user_enrolment(restore_enrolments_structure_step $step, $data, $instance, $userid, $oldinstancestatus) { | |
global $DB; | |
if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL) { | |
// Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers. | |
return; | |
} | |
if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id, 'userid'=>$userid))) { | |
$this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED); | |
} | |
} | |
/** | |
* Restore role assignment. | |
* | |
* @param stdClass $instance | |
* @param int $roleid | |
* @param int $userid | |
* @param int $contextid | |
*/ | |
public function restore_role_assignment($instance, $roleid, $userid, $contextid) { | |
if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES) { | |
// Role assignments were already synchronised in restore_instance(), we do not want any leftovers. | |
return; | |
} | |
role_assign($roleid, $userid, $contextid, 'enrol_'.$this->get_name(), $instance->id); | |
} | |
/** | |
* Test plugin settings, print info to output. | |
*/ | |
public function test_settings() { | |
global $CFG, $OUTPUT; | |
// NOTE: this is not localised intentionally, admins are supposed to understand English at least a bit... | |
raise_memory_limit(MEMORY_HUGE); | |
$this->load_config(); | |
$enroltable = $this->get_config('remoteenroltable'); | |
$coursetable = $this->get_config('newcoursetable'); | |
if (empty($enroltable)) { | |
echo $OUTPUT->notification('External enrolment table not specified.', 'notifyproblem'); | |
} | |
if (empty($coursetable)) { | |
echo $OUTPUT->notification('External course table not specified.', 'notifyproblem'); | |
} | |
if (empty($coursetable) and empty($enroltable)) { | |
return; | |
} | |
$olddebug = $CFG->debug; | |
$olddisplay = ini_get('display_errors'); | |
ini_set('display_errors', '1'); | |
$CFG->debug = DEBUG_DEVELOPER; | |
$olddebugdb = $this->config->debugdb; | |
$this->config->debugdb = 1; | |
error_reporting($CFG->debug); | |
$adodb = $this->db_init(); | |
if (!$adodb or !$adodb->IsConnected()) { | |
$this->config->debugdb = $olddebugdb; | |
$CFG->debug = $olddebug; | |
ini_set('display_errors', $olddisplay); | |
error_reporting($CFG->debug); | |
ob_end_flush(); | |
echo $OUTPUT->notification('Cannot connect the database.', 'notifyproblem'); | |
return; | |
} | |
if (!empty($enroltable)) { | |
$rs = $adodb->Execute("SELECT * | |
FROM $enroltable"); | |
if (!$rs) { | |
echo $OUTPUT->notification('Can not read external enrol table.', 'notifyproblem'); | |
} else if ($rs->EOF) { | |
echo $OUTPUT->notification('External enrol table is empty.', 'notifyproblem'); | |
$rs->Close(); | |
} else { | |
$fields_obj = $rs->FetchObj(); | |
$columns = array_keys((array)$fields_obj); | |
echo $OUTPUT->notification('External enrolment table contains following columns:<br />'.implode(', ', $columns), 'notifysuccess'); | |
$rs->Close(); | |
} | |
} | |
if (!empty($coursetable)) { | |
$rs = $adodb->Execute("SELECT * | |
FROM $coursetable"); | |
if (!$rs) { | |
echo $OUTPUT->notification('Can not read external course table.', 'notifyproblem'); | |
} else if ($rs->EOF) { | |
echo $OUTPUT->notification('External course table is empty.', 'notifyproblem'); | |
$rs->Close(); | |
} else { | |
$fields_obj = $rs->FetchObj(); | |
$columns = array_keys((array)$fields_obj); | |
echo $OUTPUT->notification('External course table contains following columns:<br />'.implode(', ', $columns), 'notifysuccess'); | |
$rs->Close(); | |
} | |
} | |
$adodb->Close(); | |
$this->config->debugdb = $olddebugdb; | |
$CFG->debug = $olddebug; | |
ini_set('display_errors', $olddisplay); | |
error_reporting($CFG->debug); | |
ob_end_flush(); | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment