dirroot.'/enrol/enrol.class.php'); class enrolment_plugin_database { var $log; /* * For the given user, let's go out and look in an external database * for an authoritative list of enrolments, and then adjust the * local Moodle assignments to match. */ function setup_enrolments(&$user) { global $CFG; // NOTE: if $this->enrol_connect() succeeds you MUST remember to call // $this->enrol_disconnect() as it is doing some nasty vodoo with $CFG->prefix $enroldb = $this->enrol_connect(); if (!$enroldb) { error_log('[ENROL_DB] Could not make a connection'); return; } // If we are expecting to get role information from our remote db, then // we execute the below code for every role type. Otherwise we just // execute it once with null (hence the dummy array). $roles = !empty($CFG->enrol_db_remoterolefield) && !empty($CFG->enrol_db_localrolefield) ? get_records('role') : array(null); //error_log('[ENROL_DB] found ' . count($roles) . ' roles:'); foreach($roles as $role) { //error_log('[ENROL_DB] setting up enrolments for '.$role->shortname); /// Get the authoritative list of enrolments from the external database table /// We're using the ADOdb functions natively here and not our datalib functions /// because we didn't want to mess with the $db global $useridfield = $enroldb->quote($user->{$CFG->enrol_localuserfield}); list($have_role, $remote_role_name, $remote_role_value) = $this->role_fields($enroldb, $role); /// Check if a particular role has been forced by the plugin site-wide /// (if we aren't doing a role-based select) if (!$have_role && $CFG->enrol_db_defaultcourseroleid) { $role = get_record('role', 'id', $CFG->enrol_db_defaultcourseroleid); } /// Whether to fetch the default role on a per-course basis (below) or not. $use_default_role = !$role; /* if ($have_role) { error_log('[ENROL_DB] Doing role-specific select from db for role: '.$role->shortname); } elseif ($use_default_role) { error_log('[ENROL_DB] Using course default for roles - assuming that database lists defaults'); } else { error_log('[ENROL_DB] Using config default for roles: '.$role->shortname); }*/ if ($rs = $enroldb->Execute("SELECT {$CFG->enrol_remotecoursefield} FROM {$CFG->enrol_dbtable} WHERE {$CFG->enrol_remoteuserfield} = " . $useridfield . (isset($remote_role_name, $remote_role_value) ? ' AND '.$remote_role_name.' = '.$remote_role_value : ''))) { // We'll use this to see what to add and remove $existing = $role ? get_records_sql(" SELECT * FROM {$CFG->prefix}role_assignments WHERE userid = {$user->id} AND roleid = {$role->id}") : get_records('role_assignments', 'userid', $user->id); if (!$existing) { $existing = array(); } //error_log('[ENROL_DB] Found '.count($existing).' existing roles and '.$rs->RecordCount().' in external database'); if ($rs->RecordCount() > 0) { // We found some courses $courselist = array(); while (!$rs->EOF) { // Make a nice little array of courses to process $courselist[] = $rs->fields[0]; $rs->MoveNext(); } $rs->Close(); foreach ($courselist as $coursefield) { /// Check the list of courses against existing if ($course = get_record('course', $CFG->enrol_localcoursefield, $coursefield)) { /// If there's no role specified, we get the default course role (usually student) if ($use_default_role) { $role = get_default_course_role($course); } $context = get_context_instance(CONTEXT_COURSE, $course->id); // Couldn't get a role or context, skip. if (!$role || !$context) { continue; } // Search the role assignments to see if this user // already has this role in this context. If it is, we // skip to the next course. foreach($existing as $key => $role_assignment) { if ($role_assignment->roleid = $role->id && $role_assignment->contextid = $context->id) { unset($existing[$key]); //error_log('[ENROL_DB] User is already enroled in course '.$course->idnumber); continue 2; } } //error_log('[ENROL_DB] Enrolling user in course '.$course->idnumber); role_assign($role->id, $user->id, 0, $context->id, 0, 0, 0, 'database'); } } } // We've processed all external courses found /// We have some courses left that we might need to unenrol from /// Note: we only process enrolments that we (ie 'database' plugin) made foreach ($existing as $role_assignment) { if ($role_assignment->enrol == 'database') { //error_log('[ENROL_DB] Removing user from context '.$role_assignment->contextid); role_unassign($role_assignment->roleid, $user->id, '', $role_assignment->contextid); } } } else { error_log('[ENROL_DB] Couldn\'t get rows from external db: '.$enroldb->ErrorMsg()); } } $this->enrol_disconnect($enroldb); } /** * sync enrolments with database, create courses if required. * * @param object The role to sync for. If no role is specified, defaults are * used. */ function sync_enrolments($role = null) { global $CFG; global $db; error_reporting(E_ALL); // Connect to the external database $enroldb = $this->enrol_connect(); if (!$enroldb) { notify("enrol/database cannot connect to server"); return false; } if (isset($role)) { echo '=== Syncing enrolments for role: '.$role->shortname." ===\n"; } else { echo "=== Syncing enrolments for default role ===\n"; } // first, pack the sortorder... fix_course_sortorder(); list($have_role, $remote_role_name, $remote_role_value) = $this->role_fields($enroldb, $role); if (!$have_role) { if (!empty($CFG->enrol_db_defaultcourseroleid) and $role = get_record('role', 'id', $CFG->enrol_db_defaultcourseroleid)) { echo "=== Using enrol_db_defaultcourseroleid: {$role->id} ({$role->shortname}) ===\n"; } elseif (isset($role)) { echo "!!! WARNING: Role specified by caller, but no (or invalid) role configuration !!!\n"; } } // get enrolments per-course $sql = "SELECT DISTINCT {$CFG->enrol_remotecoursefield} " . " FROM {$CFG->enrol_dbtable} " . " WHERE {$CFG->enrol_remoteuserfield} IS NOT NULL" . (isset($remote_role_name, $remote_role_value) ? ' AND '.$remote_role_name.' = '.$remote_role_value : ''); $rs = $enroldb->Execute($sql); if (!$rs) { trigger_error($enroldb->ErrorMsg() .' STATEMENT: '. $sql); return false; } if ( $rs->RecordCount() == 0 ) { // no courses! outta here... return true; } begin_sql(); $extcourses = array(); while (!$rs->EOF) { // there are more course records $extcourse = $rs->Fields($CFG->enrol_remotecoursefield); array_push($extcourses, $extcourse); $rs->MoveNext(); // prep the next record print "course $extcourse\n"; // does the course exist in moodle already? $course = false; $course = get_record( 'course', $CFG->enrol_localcoursefield, $extcourse ); if (!is_object($course)) { if (empty($CFG->enrol_db_autocreate)) { // autocreation not allowed print "Course $extcourse does not exist, skipping\n"; continue; // next foreach course } // ok, now then let's create it! print "Creating Course $extcourse..."; // prepare any course properties we actually have $course = new StdClass; $course->{$CFG->enrol_localcoursefield} = $extcourse; $course->fullname = $extcourse; $course->shortname = $extcourse; if ($newcourseid = $this->create_course($course, true) and $course = get_record( 'course', 'id', $newcourseid)) { // we are skipping fix_course_sortorder() print "created\n"; } else { print "failed\n"; continue; // nothing left to do... } } $context = get_context_instance(CONTEXT_COURSE, $course->id); // If we don't have a proper role setup, then we default to the default // role for the current course. if (!$have_role) { $role = get_default_course_role($course); } // get a list of the student ids the are enrolled // in the external db -- hopefully it'll fit in memory... $extenrolments = array(); $sql = "SELECT {$CFG->enrol_remoteuserfield} " . " FROM {$CFG->enrol_dbtable} " . " WHERE {$CFG->enrol_remotecoursefield} = " . $enroldb->quote($extcourse) . ($have_role ? ' AND '.$remote_role_name.' = '.$remote_role_value : ''); $crs = $enroldb->Execute($sql); if (!$crs) { trigger_error($enroldb->ErrorMsg() .' STATEMENT: '. $sql); return false; } if ( $crs->RecordCount() == 0 ) { // shouldn't happen, but cover all bases continue; } // slurp results into an array while (!$crs->EOF) { array_push($extenrolments,$crs->Fields($CFG->enrol_remoteuserfield)); $crs->MoveNext(); } $crs->Close(); // release the handle // // prune enrolments // hopefully they'll fit in the max buffer size for the RDBMS // // TODO: This doesn't work perfectly. If we are operating without // roles in the external DB, then this doesn't handle changes of role // within a course (because the user is still enrolled in the course, // so NOT IN misses the course). // // When the user logs in though, their role list will be updated // correctly. // $to_prune = get_records_sql(" SELECT ra.* FROM {$CFG->prefix}role_assignments ra JOIN {$CFG->prefix}user u ON ra.userid = u.id WHERE ra.enrol = 'database' AND ra.contextid = {$context->id} AND ra.roleid = ". $role->id . ($extenrolments ? " AND u.{$CFG->enrol_localuserfield} NOT IN (".join(", ", array_map(array(&$db, 'quote'), $extenrolments)).")" : '')); if ($to_prune) { foreach ($to_prune as $role_assignment) { if (role_unassign($role->id, $role_assignment->userid, 0, $role_assignment->contextid)){ print "Unassigned {$role->shortname} assignment #{$role_assignment->id} for course {$course->id} ({$course->shortname}); user {$role_assignment->userid}\n"; } else { print "Failed to unassign {$role->shortname} assignment #{$role_assignment->id} for course {$course->id} ({$course->shortname}); user {$role_assignment->userid}\n"; } } } // // insert current enrolments // bad we can't do INSERT IGNORE with postgres... // foreach ($extenrolments as $member) { // Get the user id and whether is enrolled in one fell swoop $sql = " SELECT u.id AS userid, ra.id AS enrolmentid FROM {$CFG->prefix}user u LEFT OUTER JOIN {$CFG->prefix}role_assignments ra ON u.id = ra.userid AND ra.roleid = {$role->id} AND ra.contextid = {$context->id} WHERE u.{$CFG->enrol_localuserfield} = ".$db->quote($member) . " AND (u.deleted IS NULL OR u.deleted=0) "; $ers = $db->Execute($sql); if (!$ers) { trigger_error($db->ErrorMsg() .' STATEMENT: '. $sql); return false; } if ( $ers->RecordCount() == 0 ) { // if this returns empty, it means we don't have the student record. // should not happen -- but skip it anyway trigger_error('weird! no user record entry?'); continue; } $userid = $ers->Fields('userid'); $enrolmentid = $ers->Fields('enrolmentid'); unset($ers); // release the handle if ($enrolmentid) { // already enrolled - skip continue; } if (role_assign($role->id, $userid, 0, $context->id, 0, 0, 0, 'database')){ print "Assigned role {$role->shortname} to user {$userid} in course {$course->id} ({$course->shortname})\n"; } else { print "Failed to assign role {$role->shortname} to user {$userid} in course {$course->id} ({$course->shortname})\n"; } } // end foreach member } // end while course records // // prune enrolments to courses that are no longer in ext auth // // TODO: This doesn't work perfectly. If we are operating without // roles in the external DB, then this doesn't handle changes of role // within a course (because the user is still enrolled in the course, // so NOT IN misses the course). // // When the user logs in though, their role list will be updated // correctly. // $sql = " SELECT ra.roleid, ra.userid, ra.contextid FROM {$CFG->prefix}role_assignments ra LEFT OUTER JOIN ({$CFG->prefix}context cn JOIN {$CFG->prefix}course c ON cn.contextlevel = ".CONTEXT_COURSE." AND cn.instanceid = c.id) ON ra.contextid = cn.id WHERE ra.enrol = 'database'" . ($have_role ? ' AND ra.roleid = '.$role->id : '') . ($extcourses ? " AND (c.id IS NULL OR c.{$CFG->enrol_localcoursefield} NOT IN (" . join(",", array_map(array(&$db, 'quote'), $extcourses)) . "))" : ''); $ers = $db->Execute($sql); if (!$ers) { trigger_error($db->ErrorMsg() .' STATEMENT: '. $sql); return false; } if ( $ers->RecordCount() > 0 ) { while (!$ers->EOF) { $roleid = $ers->Fields('roleid'); $user = $ers->Fields('userid'); $contextid = $ers->Fields('contextid'); $ers->MoveNext(); if (role_unassign($roleid, $user, 0, $contextid)){ print "Unassigned role {$roleid} from user $user in context $contextid\n"; } else { print "Failed unassign role {$roleid} from user $user in context $contextid\n"; } } } unset($ers); // release the handle commit_sql(); // we are done now, a bit of housekeeping fix_course_sortorder(); $this->enrol_disconnect($enroldb); return true; } /// Overide the get_access_icons() function function get_access_icons($course) { } /// Overide the base config_form() function function config_form($frm) { global $CFG; $vars = array('enrol_dbhost', 'enrol_dbuser', 'enrol_dbpass', 'enrol_dbname', 'enrol_dbtable', 'enrol_localcoursefield', 'enrol_localuserfield', 'enrol_remotecoursefield', 'enrol_remoteuserfield', 'enrol_db_autocreate', 'enrol_db_category', 'enrol_db_template', 'enrol_db_localrolefield', 'enrol_db_remoterolefield', 'enrol_remotecoursefield', 'enrol_remoteuserfield'); foreach ($vars as $var) { if (!isset($frm->$var)) { $frm->$var = ''; } } include("$CFG->dirroot/enrol/database/config.html"); } /// Override the base process_config() function function process_config($config) { if (!isset($config->enrol_dbtype)) { $config->enrol_dbtype = 'mysql'; } set_config('enrol_dbtype', $config->enrol_dbtype); if (!isset($config->enrol_dbhost)) { $config->enrol_dbhost = ''; } set_config('enrol_dbhost', $config->enrol_dbhost); if (!isset($config->enrol_dbuser)) { $config->enrol_dbuser = ''; } set_config('enrol_dbuser', $config->enrol_dbuser); if (!isset($config->enrol_dbpass)) { $config->enrol_dbpass = ''; } set_config('enrol_dbpass', $config->enrol_dbpass); if (!isset($config->enrol_dbname)) { $config->enrol_dbname = ''; } set_config('enrol_dbname', $config->enrol_dbname); if (!isset($config->enrol_dbtable)) { $config->enrol_dbtable = ''; } set_config('enrol_dbtable', $config->enrol_dbtable); if (!isset($config->enrol_localcoursefield)) { $config->enrol_localcoursefield = ''; } set_config('enrol_localcoursefield', $config->enrol_localcoursefield); if (!isset($config->enrol_localuserfield)) { $config->enrol_localuserfield = ''; } set_config('enrol_localuserfield', $config->enrol_localuserfield); if (!isset($config->enrol_remotecoursefield)) { $config->enrol_remotecoursefield = ''; } set_config('enrol_remotecoursefield', $config->enrol_remotecoursefield); if (!isset($config->enrol_remoteuserfield)) { $config->enrol_remoteuserfield = ''; } set_config('enrol_remoteuserfield', $config->enrol_remoteuserfield); if (!isset($config->enrol_db_autocreate)) { $config->enrol_db_autocreate = ''; } set_config('enrol_db_autocreate', $config->enrol_db_autocreate); if (!isset($config->enrol_db_category)) { $config->enrol_db_category = ''; } set_config('enrol_db_category', $config->enrol_db_category); if (!isset($config->enrol_db_template)) { $config->enrol_db_template = ''; } set_config('enrol_db_template', $config->enrol_db_template); if (!isset($config->enrol_db_defaultcourseroleid)) { $config->enrol_db_defaultcourseroleid = ''; } set_config('enrol_db_defaultcourseroleid', $config->enrol_db_defaultcourseroleid); if (!isset($config->enrol_db_localrolefield)) { $config->enrol_db_localrolefield = ''; } set_config('enrol_db_localrolefield', $config->enrol_db_localrolefield); if (!isset($config->enrol_db_remoterolefield)) { $config->enrol_db_remoterolefield = ''; } set_config('enrol_db_remoterolefield', $config->enrol_db_remoterolefield); return true; } // will create the moodle course from the template // course_ext is an array as obtained from ldap -- flattened somewhat // NOTE: if you pass true for $skip_fix_course_sortorder // you will want to call fix_course_sortorder() after your are done // with course creation function create_course ($course,$skip_fix_course_sortorder=0){ global $CFG; // define a template if(!empty($CFG->enrol_db_template)){ $template = get_record("course", 'shortname', $CFG->enrol_db_template); $template = (array)$template; } else { $site = get_site(); $template = array( 'startdate' => time() + 3600 * 24, 'summary' => get_string("defaultcoursesummary"), 'format' => "weeks", 'password' => "", 'guest' => 0, 'numsections' => 10, 'idnumber' => '', 'cost' => '', 'newsitems' => 5, 'showgrades' => 1, 'groupmode' => 0, 'groupmodeforce' => 0, 'student' => $site->student, 'students' => $site->students, 'teacher' => $site->teacher, 'teachers' => $site->teachers, ); } // overlay template foreach (array_keys($template) AS $key) { if (empty($course->$key)) { $course->$key = $template[$key]; } } $course->category = 1; // the misc 'catch-all' category if (!empty($CFG->enrol_db_category)){ //category = 0 or undef will break moodle $course->category = $CFG->enrol_db_category; } // define the sortorder $sort = get_field_sql('SELECT COALESCE(MAX(sortorder)+1, 100) AS max ' . ' FROM ' . $CFG->prefix . 'course ' . ' WHERE category=' . $course->category); $course->sortorder = $sort; // override with local data $course->startdate = time() + 3600 * 24; $course->timecreated = time(); $course->visible = 1; // clear out id just in case unset($course->id); // truncate a few key fields $course->idnumber = substr($course->idnumber, 0, 100); $course->shortname = substr($course->shortname, 0, 15); // store it and log if ($newcourseid = insert_record("course", $course)) { // Set up new course $section = NULL; $section->course = $newcourseid; // Create a default section. $section->section = 0; $section->id = insert_record("course_sections", $section); $page = page_create_object(PAGE_COURSE_VIEW, $newcourseid); blocks_repopulate_page($page); // Return value no if(!$skip_fix_course_sortorder){ fix_course_sortorder(); } add_to_log($newcourseid, "course", "new", "view.php?id=$newcourseid", "enrol/database auto-creation"); } else { trigger_error("Could not create new course $extcourse from from database"); notify("Serious Error! Could not create the new course!"); return false; } return $newcourseid; } /// DB Connect /// NOTE: You MUST remember to disconnect /// when you stop using it -- as this call will /// sometimes modify $CFG->prefix for the whole of Moodle! function enrol_connect() { global $CFG; // This is a hack to workaround what seems to be a bug in ADOdb with accessing // two MySQL databases ... it seems to get confused when trying to access // the first database again, after having accessed the second. // The following hack will make the database explicit which keeps it happy if ($CFG->dbtype === 'mysql' && $CFG->enrol_dbtype === 'mysql') { if (strpos($CFG->prefix, $CFG->dbname) === false) { $CFG->prefix_old = $CFG->prefix; $CFG->prefix = "`$CFG->dbname`.$CFG->prefix"; } } // Try to connect to the external database $enroldb = &ADONewConnection($CFG->enrol_dbtype); if ($enroldb->PConnect($CFG->enrol_dbhost,$CFG->enrol_dbuser,$CFG->enrol_dbpass,$CFG->enrol_dbname)) { $enroldb->SetFetchMode(ADODB_FETCH_ASSOC); ///Set Assoc mode always after DB connection return $enroldb; } else { // do a bit of cleanup, and lot the problem if (!empty($CFG->prefix_old)) { $CFG->prefix =$CFG->prefix_old; // Restore it just in case unset($CFG->prefix_old); } trigger_error("Error connecting to enrolment DB backend with: " . "$CFG->enrol_dbhost,$CFG->enrol_dbuser,$CFG->enrol_dbpass,$CFG->enrol_dbname"); return false; } } /// DB Disconnect function enrol_disconnect($enroldb) { global $CFG; $enroldb->Close(); // Cleanup the mysql // hack if (!empty($CFG->prefix_old)) { $CFG->prefix =$CFG->prefix_old; // Restore it just in case unset($CFG->prefix_old); } } /** * This function returns the name and value of the role field to query the db * for, or null if there isn't one. * * @param object The ADOdb connection * @param object The role * @return array (boolean, string, db quoted string) */ function role_fields($enroldb, $role) { global $CFG; if ($have_role = !empty($role) && !empty($CFG->enrol_db_remoterolefield) && !empty($CFG->enrol_db_localrolefield) && !empty($role->{$CFG->enrol_db_localrolefield})) { $remote_role_name = $CFG->enrol_db_remoterolefield; $remote_role_value = $enroldb->quote($role->{$CFG->enrol_db_localrolefield}); } else { $remote_role_name = $remote_role_value = null; } return array($have_role, $remote_role_name, $remote_role_value); } } // end of class ?>