2 // This file is part of Moodle - http://moodle.org/
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
18 * Database enrolment plugin.
20 * This plugin synchronises enrolment and roles with external database table.
22 * @package enrol_database
23 * @copyright 2010 Petr Skoda {@link http://skodak.org}
24 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
27 defined('MOODLE_INTERNAL') ||
die();
30 * Database enrolment plugin implementation.
31 * @author Petr Skoda - based on code by Martin Dougiamas, Martin Langhoff and others
32 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
34 class enrol_database_plugin
extends enrol_plugin
{
36 * Is it possible to delete enrol instance via standard UI?
38 * @param stdClass $instance
41 public function can_delete_instance($instance) {
42 $context = context_course
::instance($instance->courseid
);
43 if (!has_capability('enrol/database:config', $context)) {
46 if (!enrol_is_enabled('database')) {
49 if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
53 //TODO: connect to external system and make sure no users are to be enrolled in this course
58 * Is it possible to hide/show enrol instance via standard UI?
60 * @param stdClass $instance
63 public function can_hide_show_instance($instance) {
64 $context = context_course
::instance($instance->courseid
);
65 return has_capability('enrol/database:config', $context);
69 * Does this plugin allow manual unenrolment of a specific user?
70 * Yes, but only if user suspended...
72 * @param stdClass $instance course enrol instance
73 * @param stdClass $ue record from user_enrolments table
75 * @return bool - true means user with 'enrol/xxx:unenrol' may unenrol this user, false means nobody may touch this user enrolment
77 public function allow_unenrol_user(stdClass
$instance, stdClass
$ue) {
78 if ($ue->status
== ENROL_USER_SUSPENDED
) {
86 * Forces synchronisation of user enrolments with external database,
87 * does not create new courses.
89 * @param stdClass $user user record
92 public function sync_user_enrolments($user) {
95 // We do not create courses here intentionally because it requires full sync and is slow.
96 if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
100 $table = $this->get_config('remoteenroltable');
101 $coursefield = trim($this->get_config('remotecoursefield'));
102 $userfield = trim($this->get_config('remoteuserfield'));
103 $rolefield = trim($this->get_config('remoterolefield'));
104 $otheruserfield = trim($this->get_config('remoteotheruserfield'));
106 // Lowercased versions - necessary because we normalise the resultset with array_change_key_case().
107 $coursefield_l = strtolower($coursefield);
108 $userfield_l = strtolower($userfield);
109 $rolefield_l = strtolower($rolefield);
110 $otheruserfieldlower = strtolower($otheruserfield);
112 $localrolefield = $this->get_config('localrolefield');
113 $localuserfield = $this->get_config('localuserfield');
114 $localcoursefield = $this->get_config('localcoursefield');
116 $unenrolaction = $this->get_config('unenrolaction');
117 $defaultrole = $this->get_config('defaultrole');
119 $ignorehidden = $this->get_config('ignorehiddencourses');
121 if (!is_object($user) or !property_exists($user, 'id')) {
122 throw new coding_exception('Invalid $user parameter in sync_user_enrolments()');
125 if (!property_exists($user, $localuserfield)) {
126 debugging('Invalid $user parameter in sync_user_enrolments(), missing '.$localuserfield);
127 $user = $DB->get_record('user', array('id'=>$user->id
));
130 // Create roles mapping.
131 $allroles = get_all_roles();
132 if (!isset($allroles[$defaultrole])) {
136 foreach ($allroles as $role) {
137 $roles[$role->$localrolefield] = $role->id
;
140 $roleassigns = array();
142 $instances = array();
144 if (!$extdb = $this->db_init()) {
145 // Can not connect to database, sorry.
149 // Read remote enrols and create instances.
150 $sql = $this->db_get_sql($table, array($userfield=>$user->$localuserfield), array(), false);
152 if ($rs = $extdb->Execute($sql)) {
154 while ($fields = $rs->FetchRow()) {
155 $fields = array_change_key_case($fields, CASE_LOWER
);
156 $fields = $this->db_decode($fields);
158 if (empty($fields[$coursefield_l])) {
159 // Missing course info.
162 if (!$course = $DB->get_record('course', array($localcoursefield=>$fields[$coursefield_l]), 'id,visible')) {
165 if (!$course->visible
and $ignorehidden) {
169 if (empty($fields[$rolefield_l]) or !isset($roles[$fields[$rolefield_l]])) {
171 // Role is mandatory.
174 $roleid = $defaultrole;
176 $roleid = $roles[$fields[$rolefield_l]];
179 $roleassigns[$course->id
][$roleid] = $roleid;
180 if (empty($fields[$otheruserfieldlower])) {
181 $enrols[$course->id
][$roleid] = $roleid;
184 if ($instance = $DB->get_record('enrol', array('courseid'=>$course->id
, 'enrol'=>'database'), '*', IGNORE_MULTIPLE
)) {
185 $instances[$course->id
] = $instance;
189 $enrolid = $this->add_instance($course);
190 $instances[$course->id
] = $DB->get_record('enrol', array('id'=>$enrolid));
196 // Bad luck, something is wrong with the db connection.
201 // Enrol user into courses and sync roles.
202 foreach ($roleassigns as $courseid => $roles) {
203 if (!isset($instances[$courseid])) {
207 $instance = $instances[$courseid];
209 if (isset($enrols[$courseid])) {
210 if ($e = $DB->get_record('user_enrolments', array('userid' => $user->id
, 'enrolid' => $instance->id
))) {
211 // Reenable enrolment when previously disable enrolment refreshed.
212 if ($e->status
== ENROL_USER_SUSPENDED
) {
213 $this->update_user_enrol($instance, $user->id
, ENROL_USER_ACTIVE
);
216 $roleid = reset($enrols[$courseid]);
217 $this->enrol_user($instance, $user->id
, $roleid, 0, 0, ENROL_USER_ACTIVE
);
221 if (!$context = context_course
::instance($instance->courseid
, IGNORE_MISSING
)) {
225 $current = $DB->get_records('role_assignments', array('contextid'=>$context->id
, 'userid'=>$user->id
, 'component'=>'enrol_database', 'itemid'=>$instance->id
), '', 'id, roleid');
228 foreach ($current as $r) {
229 if (isset($roles[$r->roleid
])) {
230 $existing[$r->roleid
] = $r->roleid
;
232 role_unassign($r->roleid
, $user->id
, $context->id
, 'enrol_database', $instance->id
);
235 foreach ($roles as $rid) {
236 if (!isset($existing[$rid])) {
237 role_assign($rid, $user->id
, $context->id
, 'enrol_database', $instance->id
);
242 // Unenrol as necessary.
243 $sql = "SELECT e.*, c.visible AS cvisible, ue.status AS ustatus
245 JOIN {course} c ON c.id = e.courseid
246 JOIN {role_assignments} ra ON ra.itemid = e.id
247 LEFT JOIN {user_enrolments} ue ON ue.enrolid = e.id AND ue.userid = ra.userid
248 WHERE ra.userid = :userid AND e.enrol = 'database'";
249 $rs = $DB->get_recordset_sql($sql, array('userid' => $user->id
));
250 foreach ($rs as $instance) {
251 if (!$instance->cvisible
and $ignorehidden) {
255 if (!$context = context_course
::instance($instance->courseid
, IGNORE_MISSING
)) {
260 if (!empty($enrols[$instance->courseid
])) {
261 // We want this user enrolled.
265 // Deal with enrolments removed from external table
266 if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL
) {
267 $this->unenrol_user($instance, $user->id
);
269 } else if ($unenrolaction == ENROL_EXT_REMOVED_KEEP
) {
270 // Keep - only adding enrolments.
272 } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND
or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
274 if ($instance->ustatus
!= ENROL_USER_SUSPENDED
) {
275 $this->update_user_enrol($instance, $user->id
, ENROL_USER_SUSPENDED
);
277 if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
278 if (!empty($roleassigns[$instance->courseid
])) {
279 // We want this "other user" to keep their roles.
282 role_unassign_all(array('contextid'=>$context->id
, 'userid'=>$user->id
, 'component'=>'enrol_database', 'itemid'=>$instance->id
));
290 * Forces synchronisation of all enrolments with external database.
292 * @param progress_trace $trace
293 * @param null|int $onecourse limit sync to one course only (used primarily in restore)
294 * @return int 0 means success, 1 db connect failure, 2 db read failure
296 public function sync_enrolments(progress_trace
$trace, $onecourse = null) {
299 // We do not create courses here intentionally because it requires full sync and is slow.
300 if (!$this->get_config('dbtype') or !$this->get_config('remoteenroltable') or !$this->get_config('remotecoursefield') or !$this->get_config('remoteuserfield')) {
301 $trace->output('User enrolment synchronisation skipped.');
306 $trace->output('Starting user enrolment synchronisation...');
308 if (!$extdb = $this->db_init()) {
309 $trace->output('Error while communicating with external enrolment database');
314 // We may need a lot of memory here.
315 core_php_time_limit
::raise();
316 raise_memory_limit(MEMORY_HUGE
);
318 $table = $this->get_config('remoteenroltable');
319 $coursefield = trim($this->get_config('remotecoursefield'));
320 $userfield = trim($this->get_config('remoteuserfield'));
321 $rolefield = trim($this->get_config('remoterolefield'));
322 $otheruserfield = trim($this->get_config('remoteotheruserfield'));
324 // Lowercased versions - necessary because we normalise the resultset with array_change_key_case().
325 $coursefield_l = strtolower($coursefield);
326 $userfield_l = strtolower($userfield);
327 $rolefield_l = strtolower($rolefield);
328 $otheruserfieldlower = strtolower($otheruserfield);
330 $localrolefield = $this->get_config('localrolefield');
331 $localuserfield = $this->get_config('localuserfield');
332 $localcoursefield = $this->get_config('localcoursefield');
334 $unenrolaction = $this->get_config('unenrolaction');
335 $defaultrole = $this->get_config('defaultrole');
337 // Create roles mapping.
338 $allroles = get_all_roles();
339 if (!isset($allroles[$defaultrole])) {
343 foreach ($allroles as $role) {
344 $roles[$role->$localrolefield] = $role->id
;
348 $sql = "SELECT c.id, c.visible, c.$localcoursefield AS mapping, c.shortname, e.id AS enrolid
350 LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'database')
352 if (!$course = $DB->get_record_sql($sql, array('id'=>$onecourse))) {
353 // Course does not exist, nothing to sync.
356 if (empty($course->mapping
)) {
357 // We can not map to this course, sorry.
360 if (empty($course->enrolid
)) {
361 $course->enrolid
= $this->add_instance($course);
363 $existing = array($course->mapping
=>$course);
365 // Feel free to unenrol everybody, no safety tricks here.
366 $preventfullunenrol = false;
367 // Course being restored are always hidden, we have to ignore the setting here.
368 $ignorehidden = false;
371 // Get a list of courses to be synced that are in external table.
372 $externalcourses = array();
373 $sql = $this->db_get_sql($table, array(), array($coursefield), true);
374 if ($rs = $extdb->Execute($sql)) {
376 while ($mapping = $rs->FetchRow()) {
377 $mapping = reset($mapping);
378 $mapping = $this->db_decode($mapping);
379 if (empty($mapping)) {
383 $externalcourses[$mapping] = true;
388 $trace->output('Error reading data from the external enrolment table');
392 $preventfullunenrol = empty($externalcourses);
393 if ($preventfullunenrol and $unenrolaction == ENROL_EXT_REMOVED_UNENROL
) {
394 $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);
397 // First find all existing courses with enrol instance.
399 $sql = "SELECT c.id, c.visible, c.$localcoursefield AS mapping, e.id AS enrolid, c.shortname
401 JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'database')";
402 $rs = $DB->get_recordset_sql($sql); // Watch out for idnumber duplicates.
403 foreach ($rs as $course) {
404 if (empty($course->mapping
)) {
407 $existing[$course->mapping
] = $course;
408 unset($externalcourses[$course->mapping
]);
412 // Add necessary enrol instances that are not present yet.
415 if ($localcoursefield !== 'id') {
416 $localnotempty = "AND c.$localcoursefield <> :lcfe";
417 $params['lcfe'] = '';
419 $sql = "SELECT c.id, c.visible, c.$localcoursefield AS mapping, c.shortname
421 LEFT JOIN {enrol} e ON (e.courseid = c.id AND e.enrol = 'database')
422 WHERE e.id IS NULL $localnotempty";
423 $rs = $DB->get_recordset_sql($sql, $params);
424 foreach ($rs as $course) {
425 if (empty($course->mapping
)) {
428 if (!isset($externalcourses[$course->mapping
])) {
429 // Course not synced or duplicate.
432 $course->enrolid
= $this->add_instance($course);
433 $existing[$course->mapping
] = $course;
434 unset($externalcourses[$course->mapping
]);
438 // Print list of missing courses.
439 if ($externalcourses) {
440 $list = implode(', ', array_keys($externalcourses));
441 $trace->output("error: following courses do not exist - $list", 1);
446 unset($externalcourses);
448 $ignorehidden = $this->get_config('ignorehiddencourses');
451 // Sync user enrolments.
452 $sqlfields = array($userfield);
454 $sqlfields[] = $rolefield;
456 if ($otheruserfield) {
457 $sqlfields[] = $otheruserfield;
459 foreach ($existing as $course) {
460 if ($ignorehidden and !$course->visible
) {
463 if (!$instance = $DB->get_record('enrol', array('id'=>$course->enrolid
))) {
466 $context = context_course
::instance($course->id
);
468 // Get current list of enrolled users with their roles.
469 $currentroles = array();
470 $currentenrols = array();
471 $currentstatus = array();
472 $usermapping = array();
473 $sql = "SELECT u.$localuserfield AS mapping, u.id AS userid, ue.status, ra.roleid
475 JOIN {role_assignments} ra ON (ra.userid = u.id AND ra.component = 'enrol_database' AND ra.itemid = :enrolid)
476 LEFT JOIN {user_enrolments} ue ON (ue.userid = u.id AND ue.enrolid = ra.itemid)
477 WHERE u.deleted = 0";
478 $params = array('enrolid'=>$instance->id
);
479 if ($localuserfield === 'username') {
480 $sql .= " AND u.mnethostid = :mnethostid";
481 $params['mnethostid'] = $CFG->mnet_localhost_id
;
483 $rs = $DB->get_recordset_sql($sql, $params);
484 foreach ($rs as $ue) {
485 $currentroles[$ue->userid
][$ue->roleid
] = $ue->roleid
;
486 $usermapping[$ue->mapping
] = $ue->userid
;
488 if (isset($ue->status
)) {
489 $currentenrols[$ue->userid
][$ue->roleid
] = $ue->roleid
;
490 $currentstatus[$ue->userid
] = $ue->status
;
495 // Get list of users that need to be enrolled and their roles.
496 $requestedroles = array();
497 $requestedenrols = array();
498 $sql = $this->db_get_sql($table, array($coursefield=>$course->mapping
), $sqlfields);
499 if ($rs = $extdb->Execute($sql)) {
501 $usersearch = array('deleted' => 0);
502 if ($localuserfield === 'username') {
503 $usersearch['mnethostid'] = $CFG->mnet_localhost_id
;
505 while ($fields = $rs->FetchRow()) {
506 $fields = array_change_key_case($fields, CASE_LOWER
);
507 if (empty($fields[$userfield_l])) {
508 $trace->output("error: skipping user without mandatory $localuserfield in course '$course->mapping'", 1);
511 $mapping = $fields[$userfield_l];
512 if (!isset($usermapping[$mapping])) {
513 $usersearch[$localuserfield] = $mapping;
514 if (!$user = $DB->get_record('user', $usersearch, 'id', IGNORE_MULTIPLE
)) {
515 $trace->output("error: skipping unknown user $localuserfield '$mapping' in course '$course->mapping'", 1);
518 $usermapping[$mapping] = $user->id
;
521 $userid = $usermapping[$mapping];
523 if (empty($fields[$rolefield_l]) or !isset($roles[$fields[$rolefield_l]])) {
525 $trace->output("error: skipping user '$userid' in course '$course->mapping' - missing course and default role", 1);
528 $roleid = $defaultrole;
530 $roleid = $roles[$fields[$rolefield_l]];
533 $requestedroles[$userid][$roleid] = $roleid;
534 if (empty($fields[$otheruserfieldlower])) {
535 $requestedenrols[$userid][$roleid] = $roleid;
541 $trace->output("error: skipping course '$course->mapping' - could not match with external database", 1);
546 // Enrol all users and sync roles.
547 foreach ($requestedenrols as $userid => $userroles) {
548 foreach ($userroles as $roleid) {
549 if (empty($currentenrols[$userid])) {
550 $this->enrol_user($instance, $userid, $roleid, 0, 0, ENROL_USER_ACTIVE
);
551 $currentroles[$userid][$roleid] = $roleid;
552 $currentenrols[$userid][$roleid] = $roleid;
553 $currentstatus[$userid] = ENROL_USER_ACTIVE
;
554 $trace->output("enrolling: $userid ==> $course->shortname as ".$allroles[$roleid]->shortname
, 1);
558 // Reenable enrolment when previously disable enrolment refreshed.
559 if ($currentstatus[$userid] == ENROL_USER_SUSPENDED
) {
560 $this->update_user_enrol($instance, $userid, ENROL_USER_ACTIVE
);
561 $trace->output("unsuspending: $userid ==> $course->shortname", 1);
565 foreach ($requestedroles as $userid => $userroles) {
566 // Assign extra roles.
567 foreach ($userroles as $roleid) {
568 if (empty($currentroles[$userid][$roleid])) {
569 role_assign($roleid, $userid, $context->id
, 'enrol_database', $instance->id
);
570 $currentroles[$userid][$roleid] = $roleid;
571 $trace->output("assigning roles: $userid ==> $course->shortname as ".$allroles[$roleid]->shortname
, 1);
575 // Unassign removed roles.
576 foreach ($currentroles[$userid] as $cr) {
577 if (empty($userroles[$cr])) {
578 role_unassign($cr, $userid, $context->id
, 'enrol_database', $instance->id
);
579 unset($currentroles[$userid][$cr]);
580 $trace->output("unsassigning roles: $userid ==> $course->shortname", 1);
584 unset($currentroles[$userid]);
587 foreach ($currentroles as $userid => $userroles) {
588 // These are roles that exist only in Moodle, not the external database
589 // so make sure the unenrol actions will handle them by setting status.
590 $currentstatus +
= array($userid => ENROL_USER_ACTIVE
);
593 // Deal with enrolments removed from external table.
594 if ($unenrolaction == ENROL_EXT_REMOVED_UNENROL
) {
595 if (!$preventfullunenrol) {
597 foreach ($currentstatus as $userid => $status) {
598 if (isset($requestedenrols[$userid])) {
601 $this->unenrol_user($instance, $userid);
602 $trace->output("unenrolling: $userid ==> $course->shortname", 1);
606 } else if ($unenrolaction == ENROL_EXT_REMOVED_KEEP
) {
607 // Keep - only adding enrolments.
609 } else if ($unenrolaction == ENROL_EXT_REMOVED_SUSPEND
or $unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
610 // Suspend enrolments.
611 foreach ($currentstatus as $userid => $status) {
612 if (isset($requestedenrols[$userid])) {
615 if ($status != ENROL_USER_SUSPENDED
) {
616 $this->update_user_enrol($instance, $userid, ENROL_USER_SUSPENDED
);
617 $trace->output("suspending: $userid ==> $course->shortname", 1);
619 if ($unenrolaction == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
620 if (isset($requestedroles[$userid])) {
621 // We want this "other user" to keep their roles.
624 role_unassign_all(array('contextid'=>$context->id
, 'userid'=>$userid, 'component'=>'enrol_database', 'itemid'=>$instance->id
));
626 $trace->output("unsassigning all roles: $userid ==> $course->shortname", 1);
632 // Close db connection.
635 $trace->output('...user enrolment synchronisation finished.');
642 * Performs a full sync with external database.
644 * First it creates new courses if necessary, then
645 * enrols and unenrols users.
647 * @param progress_trace $trace
648 * @return int 0 means success, 1 db connect failure, 4 db read failure
650 public function sync_courses(progress_trace
$trace) {
653 // Make sure we sync either enrolments or courses.
654 if (!$this->get_config('dbtype') or !$this->get_config('newcoursetable') or !$this->get_config('newcoursefullname') or !$this->get_config('newcourseshortname')) {
655 $trace->output('Course synchronisation skipped.');
660 $trace->output('Starting course synchronisation...');
662 // We may need a lot of memory here.
663 core_php_time_limit
::raise();
664 raise_memory_limit(MEMORY_HUGE
);
666 if (!$extdb = $this->db_init()) {
667 $trace->output('Error while communicating with external enrolment database');
672 $table = $this->get_config('newcoursetable');
673 $fullname = trim($this->get_config('newcoursefullname'));
674 $shortname = trim($this->get_config('newcourseshortname'));
675 $idnumber = trim($this->get_config('newcourseidnumber'));
676 $category = trim($this->get_config('newcoursecategory'));
678 // Lowercased versions - necessary because we normalise the resultset with array_change_key_case().
679 $fullname_l = strtolower($fullname);
680 $shortname_l = strtolower($shortname);
681 $idnumber_l = strtolower($idnumber);
682 $category_l = strtolower($category);
684 $localcategoryfield = $this->get_config('localcategoryfield', 'id');
685 $defaultcategory = $this->get_config('defaultcategory');
687 if (!$DB->record_exists('course_categories', array('id'=>$defaultcategory))) {
688 $trace->output("default course category does not exist!", 1);
689 $categories = $DB->get_records('course_categories', array(), 'sortorder', 'id', 0, 1);
690 $first = reset($categories);
691 $defaultcategory = $first->id
;
694 $sqlfields = array($fullname, $shortname);
696 $sqlfields[] = $category;
699 $sqlfields[] = $idnumber;
701 $sql = $this->db_get_sql($table, array(), $sqlfields, true);
702 $createcourses = array();
703 if ($rs = $extdb->Execute($sql)) {
705 while ($fields = $rs->FetchRow()) {
706 $fields = array_change_key_case($fields, CASE_LOWER
);
707 $fields = $this->db_decode($fields);
708 if (empty($fields[$shortname_l]) or empty($fields[$fullname_l])) {
709 $trace->output('error: invalid external course record, shortname and fullname are mandatory: ' . json_encode($fields), 1); // Hopefully every geek can read JS, right?
712 if ($DB->record_exists('course', array('shortname'=>$fields[$shortname_l]))) {
713 // Already exists, skip.
716 // Allow empty idnumber but not duplicates.
717 if ($idnumber and $fields[$idnumber_l] !== '' and $fields[$idnumber_l] !== null and $DB->record_exists('course', array('idnumber'=>$fields[$idnumber_l]))) {
718 $trace->output('error: duplicate idnumber, can not create course: '.$fields[$shortname_l].' ['.$fields[$idnumber_l].']', 1);
721 $course = new stdClass();
722 $course->fullname
= $fields[$fullname_l];
723 $course->shortname
= $fields[$shortname_l];
724 $course->idnumber
= $idnumber ?
$fields[$idnumber_l] : '';
726 if (empty($fields[$category_l])) {
727 // Empty category means use default.
728 $course->category
= $defaultcategory;
729 } else if ($coursecategory = $DB->get_record('course_categories', array($localcategoryfield=>$fields[$category_l]), 'id')) {
730 // Yay, correctly specified category!
731 $course->category
= $coursecategory->id
;
732 unset($coursecategory);
734 // Bad luck, better not continue because unwanted ppl might get access to course in different category.
735 $trace->output('error: invalid category '.$localcategoryfield.', can not create course: '.$fields[$shortname_l], 1);
739 $course->category
= $defaultcategory;
741 $createcourses[] = $course;
747 $trace->output('Error reading data from the external course table');
751 if ($createcourses) {
752 require_once("$CFG->dirroot/course/lib.php");
754 $templatecourse = $this->get_config('templatecourse');
757 if ($templatecourse) {
758 if ($template = $DB->get_record('course', array('shortname'=>$templatecourse))) {
759 $template = fullclone(course_get_format($template)->get_course());
760 if (!isset($template->numsections
)) {
761 $template->numsections
= course_get_format($template)->get_last_section_number();
763 unset($template->id
);
764 unset($template->fullname
);
765 unset($template->shortname
);
766 unset($template->idnumber
);
768 $trace->output("can not find template for new course!", 1);
772 $courseconfig = get_config('moodlecourse');
773 $template = new stdClass();
774 $template->summary
= '';
775 $template->summaryformat
= FORMAT_HTML
;
776 $template->format
= $courseconfig->format
;
777 $template->numsections
= $courseconfig->numsections
;
778 $template->newsitems
= $courseconfig->newsitems
;
779 $template->showgrades
= $courseconfig->showgrades
;
780 $template->showreports
= $courseconfig->showreports
;
781 $template->maxbytes
= $courseconfig->maxbytes
;
782 $template->groupmode
= $courseconfig->groupmode
;
783 $template->groupmodeforce
= $courseconfig->groupmodeforce
;
784 $template->visible
= $courseconfig->visible
;
785 $template->lang
= $courseconfig->lang
;
786 $template->enablecompletion
= $courseconfig->enablecompletion
;
787 $template->groupmodeforce
= $courseconfig->groupmodeforce
;
788 $template->startdate
= usergetmidnight(time());
789 if ($courseconfig->courseenddateenabled
) {
790 $template->enddate
= usergetmidnight(time()) +
$courseconfig->courseduration
;
794 foreach ($createcourses as $fields) {
795 $newcourse = clone($template);
796 $newcourse->fullname
= $fields->fullname
;
797 $newcourse->shortname
= $fields->shortname
;
798 $newcourse->idnumber
= $fields->idnumber
;
799 $newcourse->category
= $fields->category
;
801 // Detect duplicate data once again, above we can not find duplicates
802 // in external data using DB collation rules...
803 if ($DB->record_exists('course', array('shortname' => $newcourse->shortname
))) {
804 $trace->output("can not insert new course, duplicate shortname detected: ".$newcourse->shortname
, 1);
806 } else if (!empty($newcourse->idnumber
) and $DB->record_exists('course', array('idnumber' => $newcourse->idnumber
))) {
807 $trace->output("can not insert new course, duplicate idnumber detected: ".$newcourse->idnumber
, 1);
810 $c = create_course($newcourse);
811 $trace->output("creating course: $c->id, $c->fullname, $c->shortname, $c->idnumber, $c->category", 1);
814 unset($createcourses);
818 // Close db connection.
821 $trace->output('...course synchronisation finished.');
827 protected function db_get_sql($table, array $conditions, array $fields, $distinct = false, $sort = "") {
828 $fields = $fields ?
implode(',', $fields) : "*";
831 foreach ($conditions as $key=>$value) {
832 $value = $this->db_encode($this->db_addslashes($value));
834 $where[] = "$key = '$value'";
837 $where = $where ?
"WHERE ".implode(" AND ", $where) : "";
838 $sort = $sort ?
"ORDER BY $sort" : "";
839 $distinct = $distinct ?
"DISTINCT" : "";
840 $sql = "SELECT $distinct $fields
849 * Tries to make connection to the external database.
851 * @return null|ADONewConnection
853 protected function db_init() {
856 require_once($CFG->libdir
.'/adodb/adodb.inc.php');
858 // Connect to the external database (forcing new connection).
859 $extdb = ADONewConnection($this->get_config('dbtype'));
860 if ($this->get_config('debugdb')) {
861 $extdb->debug
= true;
862 ob_start(); // Start output buffer to allow later use of the page headers.
865 // The dbtype my contain the new connection URL, so make sure we are not connected yet.
866 if (!$extdb->IsConnected()) {
867 $result = $extdb->Connect($this->get_config('dbhost'), $this->get_config('dbuser'), $this->get_config('dbpass'), $this->get_config('dbname'), true);
873 $extdb->SetFetchMode(ADODB_FETCH_ASSOC
);
874 if ($this->get_config('dbsetupsql')) {
875 $extdb->Execute($this->get_config('dbsetupsql'));
880 protected function db_addslashes($text) {
881 // Use custom made function for now - it is better to not rely on adodb or php defaults.
882 if ($this->get_config('dbsybasequoting')) {
883 $text = str_replace('\\', '\\\\', $text);
884 $text = str_replace(array('\'', '"', "\0"), array('\\\'', '\\"', '\\0'), $text);
886 $text = str_replace("'", "''", $text);
891 protected function db_encode($text) {
892 $dbenc = $this->get_config('dbencoding');
893 if (empty($dbenc) or $dbenc == 'utf-8') {
896 if (is_array($text)) {
897 foreach($text as $k=>$value) {
898 $text[$k] = $this->db_encode($value);
902 return core_text
::convert($text, 'utf-8', $dbenc);
906 protected function db_decode($text) {
907 $dbenc = $this->get_config('dbencoding');
908 if (empty($dbenc) or $dbenc == 'utf-8') {
911 if (is_array($text)) {
912 foreach($text as $k=>$value) {
913 $text[$k] = $this->db_decode($value);
917 return core_text
::convert($text, $dbenc, 'utf-8');
922 * Automatic enrol sync executed during restore.
923 * @param stdClass $course course record
925 public function restore_sync_course($course) {
926 $trace = new null_progress_trace();
927 $this->sync_enrolments($trace, $course->id
);
931 * Restore instance and map settings.
933 * @param restore_enrolments_structure_step $step
934 * @param stdClass $data
935 * @param stdClass $course
938 public function restore_instance(restore_enrolments_structure_step
$step, stdClass
$data, $course, $oldid) {
941 if ($instance = $DB->get_record('enrol', array('courseid'=>$course->id
, 'enrol'=>$this->get_name()))) {
942 $instanceid = $instance->id
;
944 $instanceid = $this->add_instance($course);
946 $step->set_mapping('enrol', $oldid, $instanceid);
950 * Restore user enrolment.
952 * @param restore_enrolments_structure_step $step
953 * @param stdClass $data
954 * @param stdClass $instance
955 * @param int $oldinstancestatus
958 public function restore_user_enrolment(restore_enrolments_structure_step
$step, $data, $instance, $userid, $oldinstancestatus) {
961 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL
) {
962 // Enrolments were already synchronised in restore_instance(), we do not want any suspended leftovers.
965 if (!$DB->record_exists('user_enrolments', array('enrolid'=>$instance->id
, 'userid'=>$userid))) {
966 $this->enrol_user($instance, $userid, null, 0, 0, ENROL_USER_SUSPENDED
);
971 * Restore role assignment.
973 * @param stdClass $instance
976 * @param int $contextid
978 public function restore_role_assignment($instance, $roleid, $userid, $contextid) {
979 if ($this->get_config('unenrolaction') == ENROL_EXT_REMOVED_UNENROL
or $this->get_config('unenrolaction') == ENROL_EXT_REMOVED_SUSPENDNOROLES
) {
980 // Role assignments were already synchronised in restore_instance(), we do not want any leftovers.
983 role_assign($roleid, $userid, $contextid, 'enrol_'.$this->get_name(), $instance->id
);
987 * Test plugin settings, print info to output.
989 public function test_settings() {
990 global $CFG, $OUTPUT;
992 // NOTE: this is not localised intentionally, admins are supposed to understand English at least a bit...
994 raise_memory_limit(MEMORY_HUGE
);
996 $this->load_config();
998 $enroltable = $this->get_config('remoteenroltable');
999 $coursetable = $this->get_config('newcoursetable');
1001 if (empty($enroltable)) {
1002 echo $OUTPUT->notification('External enrolment table not specified.', 'notifyproblem');
1005 if (empty($coursetable)) {
1006 echo $OUTPUT->notification('External course table not specified.', 'notifyproblem');
1009 if (empty($coursetable) and empty($enroltable)) {
1013 $olddebug = $CFG->debug
;
1014 $olddisplay = ini_get('display_errors');
1015 ini_set('display_errors', '1');
1016 $CFG->debug
= DEBUG_DEVELOPER
;
1017 $olddebugdb = $this->config
->debugdb
;
1018 $this->config
->debugdb
= 1;
1019 error_reporting($CFG->debug
);
1021 $adodb = $this->db_init();
1023 if (!$adodb or !$adodb->IsConnected()) {
1024 $this->config
->debugdb
= $olddebugdb;
1025 $CFG->debug
= $olddebug;
1026 ini_set('display_errors', $olddisplay);
1027 error_reporting($CFG->debug
);
1030 echo $OUTPUT->notification('Cannot connect the database.', 'notifyproblem');
1034 if (!empty($enroltable)) {
1035 $rs = $adodb->Execute("SELECT *
1038 echo $OUTPUT->notification('Can not read external enrol table.', 'notifyproblem');
1040 } else if ($rs->EOF
) {
1041 echo $OUTPUT->notification('External enrol table is empty.', 'notifyproblem');
1045 $fields_obj = $rs->FetchObj();
1046 $columns = array_keys((array)$fields_obj);
1048 echo $OUTPUT->notification('External enrolment table contains following columns:<br />'.implode(', ', $columns), 'notifysuccess');
1053 if (!empty($coursetable)) {
1054 $rs = $adodb->Execute("SELECT *
1055 FROM $coursetable");
1057 echo $OUTPUT->notification('Can not read external course table.', 'notifyproblem');
1059 } else if ($rs->EOF
) {
1060 echo $OUTPUT->notification('External course table is empty.', 'notifyproblem');
1064 $fields_obj = $rs->FetchObj();
1065 $columns = array_keys((array)$fields_obj);
1067 echo $OUTPUT->notification('External course table contains following columns:<br />'.implode(', ', $columns), 'notifysuccess');
1074 $this->config
->debugdb
= $olddebugdb;
1075 $CFG->debug
= $olddebug;
1076 ini_set('display_errors', $olddisplay);
1077 error_reporting($CFG->debug
);