/** * Sync all meta course links. * * @param progress_trace $trace * @param int $courseid one course, empty mean all * @return int 0 means ok, 1 means error, 2 means plugin disabled */ public function sync(progress_trace $trace, $courseid = null) { global $DB; if (!enrol_is_enabled('manual')) { $trace->finished(); return 2; } // Unfortunately this may take a long time, execution can be interrupted safely here. @set_time_limit(0); raise_memory_limit(MEMORY_HUGE); $trace->output('Verifying manual enrolment expiration...'); $params = array('now' => time(), 'useractive' => ENROL_USER_ACTIVE, 'courselevel' => CONTEXT_COURSE); $coursesql = ""; if ($courseid) { $coursesql = "AND e.courseid = :courseid"; $params['courseid'] = $courseid; } // Deal with expired accounts. $action = $this->get_config('expiredaction', ENROL_EXT_REMOVED_KEEP); if ($action == ENROL_EXT_REMOVED_UNENROL) { $instances = array(); $sql = "SELECT ue.*, e.courseid, c.id AS contextid\n FROM {user_enrolments} ue\n JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'manual')\n JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)\n WHERE ue.timeend > 0 AND ue.timeend < :now\n {$coursesql}"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ue) { if (empty($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id' => $ue->enrolid)); } $instance = $instances[$ue->enrolid]; // Always remove all manually assigned roles here, this may break enrol_self roles but we do not want hardcoded hacks here. role_unassign_all(array('userid' => $ue->userid, 'contextid' => $ue->contextid, 'component' => '', 'itemid' => 0), true); $this->unenrol_user($instance, $ue->userid); $trace->output("unenrolling expired user {$ue->userid} from course {$instance->courseid}", 1); } $rs->close(); unset($instances); } else { if ($action == ENROL_EXT_REMOVED_SUSPENDNOROLES) { $instances = array(); $sql = "SELECT ue.*, e.courseid, c.id AS contextid\n FROM {user_enrolments} ue\n JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'manual')\n JOIN {context} c ON (c.instanceid = e.courseid AND c.contextlevel = :courselevel)\n WHERE ue.timeend > 0 AND ue.timeend < :now\n AND ue.status = :useractive\n {$coursesql}"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $ue) { if (empty($instances[$ue->enrolid])) { $instances[$ue->enrolid] = $DB->get_record('enrol', array('id' => $ue->enrolid)); } $instance = $instances[$ue->enrolid]; // Always remove all manually assigned roles here, this may break enrol_self roles but we do not want hardcoded hacks here. role_unassign_all(array('userid' => $ue->userid, 'contextid' => $ue->contextid, 'component' => '', 'itemid' => 0), true); $this->update_user_enrol($instance, $ue->userid, ENROL_USER_SUSPENDED); $trace->output("suspending expired user {$ue->userid} in course {$instance->courseid}", 1); } $rs->close(); unset($instances); } else { // ENROL_EXT_REMOVED_KEEP means no changes. } } $trace->output('...manual enrolment updates finished.'); $trace->finished(); return 0; }
/** * Sync all meta course links. * * @param progress_trace $trace * @param int $courseid one course, empty mean all * @return int 0 means ok, 1 means error, 2 means plugin disabled */ public function sync(progress_trace $trace, $courseid = null) { global $DB; if (!enrol_is_enabled('self')) { $trace->finished(); return 2; } // Unfortunately this may take a long time, execution can be interrupted safely here. core_php_time_limit::raise(); raise_memory_limit(MEMORY_HUGE); $trace->output('Verifying self-enrolments...'); $params = array('now' => time(), 'useractive' => ENROL_USER_ACTIVE, 'courselevel' => CONTEXT_COURSE); $coursesql = ""; if ($courseid) { $coursesql = "AND e.courseid = :courseid"; $params['courseid'] = $courseid; } // Note: the logic of self enrolment guarantees that user logged in at least once (=== u.lastaccess set) // and that user accessed course at least once too (=== user_lastaccess record exists). // First deal with users that did not log in for a really long time - they do not have user_lastaccess records. $sql = "SELECT e.*, ue.userid\n FROM {user_enrolments} ue\n JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self' AND e.customint2 > 0)\n JOIN {user} u ON u.id = ue.userid\n WHERE :now - u.lastaccess > e.customint2\n {$coursesql}"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $instance) { $userid = $instance->userid; unset($instance->userid); $this->unenrol_user($instance, $userid); $days = $instance->customint2 / 60 * 60 * 24; $trace->output("unenrolling user {$userid} from course {$instance->courseid} as they have did not log in for at least {$days} days", 1); } $rs->close(); // Now unenrol from course user did not visit for a long time. $sql = "SELECT e.*, ue.userid\n FROM {user_enrolments} ue\n JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = 'self' AND e.customint2 > 0)\n JOIN {user_lastaccess} ul ON (ul.userid = ue.userid AND ul.courseid = e.courseid)\n WHERE :now - ul.timeaccess > e.customint2\n {$coursesql}"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $instance) { $userid = $instance->userid; unset($instance->userid); $this->unenrol_user($instance, $userid); $days = $instance->customint2 / 60 * 60 * 24; $trace->output("unenrolling user {$userid} from course {$instance->courseid} as they have did not access course for at least {$days} days", 1); } $rs->close(); $trace->output('...user self-enrolment updates finished.'); $trace->finished(); $this->process_expirations($trace, $courseid); return 0; }
/** * Synchronizes user from external db to moodle user table. * * Sync should be done by using idnumber attribute, not username. * You need to pass firstsync parameter to function to fill in * idnumbers if they don't exists in moodle user table. * * Syncing users removes (disables) users that don't exists anymore in external db. * Creates new users and updates coursecreator status of users. * * This implementation is simpler but less scalable than the one found in the LDAP module. * * @param progress_trace $trace * @param bool $do_updates Optional: set to true to force an update of existing accounts * @return int 0 means success, 1 means failure */ function sync_users(progress_trace $trace, $do_updates = false) { global $CFG, $DB; require_once $CFG->dirroot . '/user/lib.php'; // List external users. $userlist = $this->get_userlist(); // Delete obsolete internal users. if (!empty($this->config->removeuser)) { $suspendselect = ""; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $suspendselect = "AND u.suspended = 0"; } // Find obsolete users. if (count($userlist)) { list($notin_sql, $params) = $DB->get_in_or_equal($userlist, SQL_PARAMS_NAMED, 'u', false); $params['authtype'] = $this->authtype; $sql = "SELECT u.*\n FROM {user} u\n WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid {$suspendselect} AND u.username {$notin_sql}"; } else { $sql = "SELECT u.*\n FROM {user} u\n WHERE u.auth=:authtype AND u.deleted=0 AND u.mnethostid=:mnethostid {$suspendselect}"; $params = array(); $params['authtype'] = $this->authtype; } $params['mnethostid'] = $CFG->mnet_localhost_id; $remove_users = $DB->get_records_sql($sql, $params); if (!empty($remove_users)) { $trace->output(get_string('auth_dbuserstoremove', 'auth_db', count($remove_users))); foreach ($remove_users as $user) { if ($this->config->removeuser == AUTH_REMOVEUSER_FULLDELETE) { delete_user($user); $trace->output(get_string('auth_dbdeleteuser', 'auth_db', array('name' => $user->username, 'id' => $user->id)), 1); } else { if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $updateuser = new stdClass(); $updateuser->id = $user->id; $updateuser->suspended = 1; user_update_user($updateuser, false); $trace->output(get_string('auth_dbsuspenduser', 'auth_db', array('name' => $user->username, 'id' => $user->id)), 1); } } } } unset($remove_users); } if (!count($userlist)) { // Exit right here, nothing else to do. $trace->finished(); return 0; } // Update existing accounts. if ($do_updates) { // Narrow down what fields we need to update. $all_keys = array_keys(get_object_vars($this->config)); $updatekeys = array(); foreach ($all_keys as $key) { if (preg_match('/^field_updatelocal_(.+)$/', $key, $match)) { if ($this->config->{$key} === 'onlogin') { array_push($updatekeys, $match[1]); // The actual key name. } } } unset($all_keys); unset($key); // Only go ahead if we actually have fields to update locally. if (!empty($updatekeys)) { list($in_sql, $params) = $DB->get_in_or_equal($userlist, SQL_PARAMS_NAMED, 'u', true); $params['authtype'] = $this->authtype; $sql = "SELECT u.id, u.username\n FROM {user} u\n WHERE u.auth=:authtype AND u.deleted=0 AND u.username {$in_sql}"; if ($update_users = $DB->get_records_sql($sql, $params)) { $trace->output("User entries to update: " . count($update_users)); foreach ($update_users as $user) { if ($this->update_user_record($user->username, $updatekeys)) { $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name' => $user->username, 'id' => $user->id)), 1); } else { $trace->output(get_string('auth_dbupdatinguser', 'auth_db', array('name' => $user->username, 'id' => $user->id)) . " - " . get_string('skipped'), 1); } } unset($update_users); } } } // Create missing accounts. // NOTE: this is very memory intensive and generally inefficient. $suspendselect = ""; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { $suspendselect = "AND u.suspended = 0"; } $sql = "SELECT u.id, u.username\n FROM {user} u\n WHERE u.auth=:authtype AND u.deleted='0' AND mnethostid=:mnethostid {$suspendselect}"; $users = $DB->get_records_sql($sql, array('authtype' => $this->authtype, 'mnethostid' => $CFG->mnet_localhost_id)); // Simplify down to usernames. $usernames = array(); if (!empty($users)) { foreach ($users as $user) { array_push($usernames, $user->username); } unset($users); } $add_users = array_diff($userlist, $usernames); unset($usernames); if (!empty($add_users)) { $trace->output(get_string('auth_dbuserstoadd', 'auth_db', count($add_users))); // Do not use transactions around this foreach, we want to skip problematic users, not revert everything. foreach ($add_users as $user) { $username = $user; if ($this->config->removeuser == AUTH_REMOVEUSER_SUSPEND) { if ($olduser = $DB->get_record('user', array('username' => $username, 'deleted' => 0, 'suspended' => 1, 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => $this->authtype))) { $updateuser = new stdClass(); $updateuser->id = $olduser->id; $updateuser->suspended = 0; user_update_user($updateuser); $trace->output(get_string('auth_dbreviveduser', 'auth_db', array('name' => $username, 'id' => $olduser->id)), 1); continue; } } // Do not try to undelete users here, instead select suspending if you ever expect users will reappear. // Prep a few params. $user = $this->get_userinfo_asobj($user); $user->username = $username; $user->confirmed = 1; $user->auth = $this->authtype; $user->mnethostid = $CFG->mnet_localhost_id; if (empty($user->lang)) { $user->lang = $CFG->lang; } if ($collision = $DB->get_record_select('user', "username = :username AND mnethostid = :mnethostid AND auth <> :auth", array('username' => $user->username, 'mnethostid' => $CFG->mnet_localhost_id, 'auth' => $this->authtype), 'id,username,auth')) { $trace->output(get_string('auth_dbinsertuserduplicate', 'auth_db', array('username' => $user->username, 'auth' => $collision->auth)), 1); continue; } try { $id = user_create_user($user, false); // It is truly a new user. $trace->output(get_string('auth_dbinsertuser', 'auth_db', array('name' => $user->username, 'id' => $id)), 1); } catch (moodle_exception $e) { $trace->output(get_string('auth_dbinsertusererror', 'auth_db', $user->username), 1); continue; } // If relevant, tag for password generation. if ($this->is_internal()) { set_user_preference('auth_forcepasswordchange', 1, $id); set_user_preference('create_password', 1, $id); } // Make sure user context is present. context_user::instance($id); } unset($add_users); } $trace->finished(); return 0; }
/** * Process any future enrollments stored in the buffer. * @param progress_trace $trace * @return bool true if any data processed, false if not */ protected function process_buffer(progress_trace $trace) { global $DB; if (!($future_enrols = $DB->get_records_select('enrol_flatfile', "timestart < ?", array(time())))) { $trace->output("No enrolments to be processed in flatfile buffer"); $trace->finished(); return false; } $trace->output("Starting processing of flatfile buffer"); foreach ($future_enrols as $en) { $user = $DB->get_record('user', array('id' => $en->userid)); $course = $DB->get_record('course', array('id' => $en->courseid)); if ($user and $course) { $trace->output("buffer: {$en->action} {$en->roleid} {$user->id} {$course->id} {$en->timestart} {$en->timeend}", 1); $this->process_records($trace, $en->action, $en->roleid, $user, $course, $en->timestart, $en->timeend, false); } $DB->delete_records('enrol_flatfile', array('id' => $en->id)); } $trace->output("Finished processing of flatfile buffer"); $trace->finished(); return true; }
/** * Synchronise courses in all categories. * * It gets out-of-sync if: * - you move course to different category * - reorder categories * - disable enrol_category and enable it again * * @param progress_trace $trace * @return int exit code - 0 is ok, 1 means error, 2 if plugin disabled */ function enrol_category_sync_full(progress_trace $trace) { global $DB; if (!enrol_is_enabled('category')) { $trace->finished(); return 2; } // We may need a lot of time here. core_php_time_limit::raise(); $plugin = enrol_get_plugin('category'); $syscontext = context_system::instance(); // Any interesting roles worth synchronising? if (!($roles = get_roles_with_capability('enrol/category:synchronised', CAP_ALLOW, $syscontext))) { // yay, nothing to do, so let's remove all leftovers $trace->output("No roles with 'enrol/category:synchronised' capability found."); if ($instances = $DB->get_records('enrol', array('enrol' => 'category'))) { $trace->output("Deleting all category enrol instances..."); foreach ($instances as $instance) { $trace->output("deleting category enrol instance from course {$instance->courseid}", 1); $plugin->delete_instance($instance); } $trace->output("...all instances deleted."); } $trace->finished(); return 0; } $rolenames = role_fix_names($roles, null, ROLENAME_SHORT, true); $trace->output('Synchronising category enrolments for roles: ' . implode(', ', $rolenames) . '...'); list($roleids, $params) = $DB->get_in_or_equal(array_keys($roles), SQL_PARAMS_NAMED, 'r'); $params['courselevel'] = CONTEXT_COURSE; $params['catlevel'] = CONTEXT_COURSECAT; // First of all add necessary enrol instances to all courses. $parentcat = $DB->sql_concat("cat.path", "'/%'"); $parentcctx = $DB->sql_concat("cctx.path", "'/%'"); // Need whole course records to be used by add_instance(), use inner view (ci) to // get distinct records only. // TODO: Moodle 2.1. Improve enrol API to accept courseid / courserec $sql = "SELECT c.*\n FROM {course} c\n JOIN (\n SELECT DISTINCT c.id\n FROM {course} c\n JOIN {context} ctx ON (ctx.instanceid = c.id AND ctx.contextlevel = :courselevel)\n JOIN (SELECT DISTINCT cctx.path\n FROM {course_categories} cc\n JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)\n JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid {$roleids})\n ) cat ON (ctx.path LIKE {$parentcat})\n LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'category')\n WHERE e.id IS NULL) ci ON (c.id = ci.id)"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $course) { $plugin->add_instance($course); } $rs->close(); // Now look for courses that do not have any interesting roles in parent contexts, // but still have the instance and delete them. $sql = "SELECT e.*\n FROM {enrol} e\n JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)\n LEFT JOIN ({course_categories} cc\n JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)\n JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid {$roleids})\n ) ON (ctx.path LIKE {$parentcctx})\n WHERE e.enrol = 'category' AND cc.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $instance) { $plugin->delete_instance($instance); } $rs->close(); // Add missing enrolments. $sql = "SELECT e.*, cat.userid, cat.estart\n FROM {enrol} e\n JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)\n JOIN (SELECT cctx.path, ra.userid, MIN(ra.timemodified) AS estart\n FROM {course_categories} cc\n JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)\n JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid {$roleids})\n GROUP BY cctx.path, ra.userid\n ) cat ON (ctx.path LIKE {$parentcat})\n LEFT JOIN {user_enrolments} ue ON (ue.enrolid = e.id AND ue.userid = cat.userid)\n WHERE e.enrol = 'category' AND ue.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $instance) { $userid = $instance->userid; $estart = $instance->estart; unset($instance->userid); unset($instance->estart); $plugin->enrol_user($instance, $userid, null, $estart); $trace->output("enrolling: user {$userid} ==> course {$instance->courseid}", 1); } $rs->close(); // Remove stale enrolments. $sql = "SELECT e.*, ue.userid\n FROM {enrol} e\n JOIN {context} ctx ON (ctx.instanceid = e.courseid AND ctx.contextlevel = :courselevel)\n JOIN {user_enrolments} ue ON (ue.enrolid = e.id)\n LEFT JOIN ({course_categories} cc\n JOIN {context} cctx ON (cctx.instanceid = cc.id AND cctx.contextlevel = :catlevel)\n JOIN {role_assignments} ra ON (ra.contextid = cctx.id AND ra.roleid {$roleids})\n ) ON (ctx.path LIKE {$parentcctx} AND ra.userid = ue.userid)\n WHERE e.enrol = 'category' AND cc.id IS NULL"; $rs = $DB->get_recordset_sql($sql, $params); foreach ($rs as $instance) { $userid = $instance->userid; unset($instance->userid); $plugin->unenrol_user($instance, $userid); $trace->output("unenrolling: user {$userid} ==> course {$instance->courseid}", 1); } $rs->close(); $trace->output('...user enrolment synchronisation finished.'); $trace->finished(); return 0; }
/** * Send expiry notifications. * * Plugin that wants to have expiry notification MUST implement following: * - expirynotifyhour plugin setting, * - configuration options in instance edit form (expirynotify, notifyall and expirythreshold), * - notification strings (expirymessageenrollersubject, expirymessageenrollerbody, * expirymessageenrolledsubject and expirymessageenrolledbody), * - expiry_notification provider in db/messages.php, * - upgrade code that sets default thresholds for existing courses (should be 1 day), * - something that calls this method, such as cron. * * @param progress_trace $trace (accepts bool for backwards compatibility only) */ public function send_expiry_notifications($trace) { global $DB, $CFG; $name = $this->get_name(); if (!enrol_is_enabled($name)) { $trace->finished(); return; } // Unfortunately this may take a long time, it should not be interrupted, // otherwise users get duplicate notification. @set_time_limit(0); raise_memory_limit(MEMORY_HUGE); $expirynotifylast = $this->get_config('expirynotifylast', 0); $expirynotifyhour = $this->get_config('expirynotifyhour'); if (is_null($expirynotifyhour)) { debugging("send_expiry_notifications() in {$name} enrolment plugin needs expirynotifyhour setting"); $trace->finished(); return; } if (!$trace instanceof progress_trace) { $trace = $trace ? new text_progress_trace() : new null_progress_trace(); debugging('enrol_plugin::send_expiry_notifications() now expects progress_trace instance as parameter!', DEBUG_DEVELOPER); } $timenow = time(); $notifytime = usergetmidnight($timenow, $CFG->timezone) + $expirynotifyhour * 3600; if ($expirynotifylast > $notifytime) { $trace->output($name . ' enrolment expiry notifications were already sent today at ' . userdate($expirynotifylast, '', $CFG->timezone) . '.'); $trace->finished(); return; } else { if ($timenow < $notifytime) { $trace->output($name . ' enrolment expiry notifications will be sent at ' . userdate($notifytime, '', $CFG->timezone) . '.'); $trace->finished(); return; } } $trace->output('Processing ' . $name . ' enrolment expiration notifications...'); // Notify users responsible for enrolment once every day. $sql = "SELECT ue.*, e.expirynotify, e.notifyall, e.expirythreshold, e.courseid, c.fullname\n FROM {user_enrolments} ue\n JOIN {enrol} e ON (e.id = ue.enrolid AND e.enrol = :name AND e.expirynotify > 0 AND e.status = :enabled)\n JOIN {course} c ON (c.id = e.courseid)\n JOIN {user} u ON (u.id = ue.userid AND u.deleted = 0 AND u.suspended = 0)\n WHERE ue.status = :active AND ue.timeend > 0 AND ue.timeend > :now1 AND ue.timeend < (e.expirythreshold + :now2)\n ORDER BY ue.enrolid ASC, u.lastname ASC, u.firstname ASC, u.id ASC"; $params = array('enabled' => ENROL_INSTANCE_ENABLED, 'active' => ENROL_USER_ACTIVE, 'now1' => $timenow, 'now2' => $timenow, 'name' => $name); $rs = $DB->get_recordset_sql($sql, $params); $lastenrollid = 0; $users = array(); foreach ($rs as $ue) { if ($lastenrollid and $lastenrollid != $ue->enrolid) { $this->notify_expiry_enroller($lastenrollid, $users, $trace); $users = array(); } $lastenrollid = $ue->enrolid; $enroller = $this->get_enroller($ue->enrolid); $context = context_course::instance($ue->courseid); $user = $DB->get_record('user', array('id' => $ue->userid)); $users[] = array('fullname' => fullname($user, has_capability('moodle/site:viewfullnames', $context, $enroller)), 'timeend' => $ue->timeend); if (!$ue->notifyall) { continue; } if ($ue->timeend - $ue->expirythreshold + 86400 < $timenow) { // Notify enrolled users only once at the start of the threshold. $trace->output("user {$ue->userid} was already notified that enrolment in course {$ue->courseid} expires on " . userdate($ue->timeend, '', $CFG->timezone), 1); continue; } $this->notify_expiry_enrolled($user, $ue, $trace); } $rs->close(); if ($lastenrollid and $users) { $this->notify_expiry_enroller($lastenrollid, $users, $trace); } $trace->output('...notification processing finished.'); $trace->finished(); $this->set_config('expirynotifylast', $timenow); }
/** * 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()); 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; }