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 * IMS Enterprise file enrolment plugin.
20 * This plugin lets the user specify an IMS Enterprise file to be processed.
21 * The IMS Enterprise file is mainly parsed on a regular cron,
22 * but can also be imported via the UI (Admin Settings).
23 * @package enrol_imsenterprise
24 * @copyright 2010 Eugene Venter
25 * @author Eugene Venter - based on code by Dan Stowell
26 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
29 defined('MOODLE_INTERNAL') ||
die();
31 require_once($CFG->dirroot
.'/group/lib.php');
32 require_once($CFG->dirroot
.'/lib/coursecatlib.php');
35 * IMS Enterprise file enrolment plugin.
37 * @copyright 2010 Eugene Venter
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class enrol_imsenterprise_plugin
extends enrol_plugin
{
43 * @var IMSENTERPRISE_ADD imsenterprise add action.
45 const IMSENTERPRISE_ADD
= 1;
48 * @var IMSENTERPRISE_UPDATE imsenterprise update action.
50 const IMSENTERPRISE_UPDATE
= 2;
53 * @var IMSENTERPRISE_DELETE imsenterprise delete action.
55 const IMSENTERPRISE_DELETE
= 3;
58 * @var $logfp resource file pointer for writing log data to.
63 * @var $continueprocessing bool flag to determine if processing should continue.
65 protected $continueprocessing;
68 * @var $xmlcache string cache of xml lines.
73 * @var $coursemappings array of mappings between IMS data fields and moodle course fields.
75 protected $coursemappings;
78 * @var $rolemappings array of mappings between IMS roles and moodle roles.
80 protected $rolemappings;
83 * @var $defaultcategoryid id of default category.
85 protected $defaultcategoryid;
88 * Read in an IMS Enterprise file.
89 * Originally designed to handle v1.1 files but should be able to handle
90 * earlier types as well, I believe.
91 * This cron feature has been converted to a scheduled task and it can now be scheduled
94 public function cron() {
98 $imsfilelocation = $this->get_config('imsfilelocation');
99 $logtolocation = $this->get_config('logtolocation');
100 $mailadmins = $this->get_config('mailadmins');
101 $prevtime = $this->get_config('prev_time');
102 $prevmd5 = $this->get_config('prev_md5');
103 $prevpath = $this->get_config('prev_path');
105 if (empty($imsfilelocation)) {
106 $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location.
108 $filename = $imsfilelocation;
111 $this->logfp
= false;
112 if (!empty($logtolocation)) {
113 $this->logfp
= fopen($logtolocation, 'a');
116 $this->defaultcategoryid
= null;
119 if ( file_exists($filename) ) {
120 core_php_time_limit
::raise();
123 $this->log_line('----------------------------------------------------------------------');
124 $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
125 $this->log_line('Found file '.$filename);
126 $this->xmlcache
= '';
128 $categoryseparator = trim($this->get_config('categoryseparator'));
129 $categoryidnumber = $this->get_config('categoryidnumber');
131 // Make sure we understand how to map the IMS-E roles to Moodle roles.
132 $this->load_role_mappings();
133 // Make sure we understand how to map the IMS-E course names to Moodle course names.
134 $this->load_course_mappings();
136 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron.
137 $filemtime = filemtime($filename);
139 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
140 // This is so we avoid wasting the server's efforts processing a file unnecessarily.
141 if ($categoryidnumber && empty($categoryseparator)) {
142 $this->log_line('Category idnumber is enabled but category separator is not defined - skipping processing.');
143 } else if (empty($prevpath) ||
($filename != $prevpath)) {
145 } else if (isset($prevtime) && ($filemtime <= $prevtime)) {
146 $this->log_line('File modification time is not more recent than last update - skipping processing.');
147 } else if (isset($prevmd5) && ($md5 == $prevmd5)) {
148 $this->log_line('File MD5 hash is same as on last update - skipping processing.');
150 $fileisnew = true; // Let's process it!
155 // The <properties> tag is allowed to halt processing if we're demanding a matching target.
156 $this->continueprocessing
= true;
158 // Run through the file and process the group/person entries.
159 if (($fh = fopen($filename, "r")) != false) {
162 while ((!feof($fh)) && $this->continueprocessing
) {
165 $curline = fgets($fh);
166 $this->xmlcache
.= $curline; // Add a line onto the XML cache.
169 // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
170 // Must always make sure to remove tags from cache so they don't clog up our memory.
171 if ($tagcontents = $this->full_tag_found_in_cache('group', $curline)) {
172 $this->process_group_tag($tagcontents);
173 $this->remove_tag_from_cache('group');
174 } else if ($tagcontents = $this->full_tag_found_in_cache('person', $curline)) {
175 $this->process_person_tag($tagcontents);
176 $this->remove_tag_from_cache('person');
177 } else if ($tagcontents = $this->full_tag_found_in_cache('membership', $curline)) {
178 $this->process_membership_tag($tagcontents);
179 $this->remove_tag_from_cache('membership');
180 } else if ($tagcontents = $this->full_tag_found_in_cache('comments', $curline)) {
181 $this->remove_tag_from_cache('comments');
182 } else if ($tagcontents = $this->full_tag_found_in_cache('properties', $curline)) {
183 $this->process_properties_tag($tagcontents);
184 $this->remove_tag_from_cache('properties');
191 fix_course_sortorder();
194 $timeelapsed = time() - $starttime;
195 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
199 // These variables are stored so we can compare them against the IMS file, next time round.
200 $this->set_config('prev_time', $filemtime);
201 $this->set_config('prev_md5', $md5);
202 $this->set_config('prev_path', $filename);
205 $this->log_line('File not found: '.$filename);
208 if (!empty($mailadmins) && $fileisnew) {
209 $timeelapsed = isset($timeelapsed) ?
$timeelapsed : 0;
210 $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
211 if (!empty($logtolocation)) {
213 $msg .= "Log data has been written to:\n";
214 $msg .= "$logtolocation\n";
215 $msg .= "(Log file size: ".ceil(filesize($logtolocation) / 1024)."Kb)\n\n";
217 $msg .= "The log file appears not to have been successfully written.\n";
218 $msg .= "Check that the file is writeable by the server:\n";
219 $msg .= "$logtolocation\n\n";
222 $msg .= "Logging is currently not active.";
225 $eventdata = new \core\message\
message();
226 $eventdata->courseid
= SITEID
;
227 $eventdata->modulename
= 'moodle';
228 $eventdata->component
= 'enrol_imsenterprise';
229 $eventdata->name
= 'imsenterprise_enrolment';
230 $eventdata->userfrom
= get_admin();
231 $eventdata->userto
= get_admin();
232 $eventdata->subject
= "Moodle IMS Enterprise enrolment notification";
233 $eventdata->fullmessage
= $msg;
234 $eventdata->fullmessageformat
= FORMAT_PLAIN
;
235 $eventdata->fullmessagehtml
= '';
236 $eventdata->smallmessage
= '';
237 message_send($eventdata);
239 $this->log_line('Notification email sent to administrator.');
244 fclose($this->logfp
);
250 * Check if a complete tag is found in the cached data, which usually happens
251 * when the end of the tag has only just been loaded into the cache.
253 * @param string $tagname Name of tag to look for
254 * @param string $latestline The very last line in the cache (used for speeding up the match)
255 * @return bool|string false, or the contents of the tag (including start and end).
257 protected function full_tag_found_in_cache($tagname, $latestline) {
258 // Return entire element if found. Otherwise return false.
259 if (strpos(strtolower($latestline), '</'.strtolower($tagname).'>') === false) {
261 } else if (preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache
, $matches)) {
269 * Remove complete tag from the cached data (including all its contents) - so
270 * that the cache doesn't grow to unmanageable size
272 * @param string $tagname Name of tag to look for
274 protected function remove_tag_from_cache($tagname) {
275 // Trim the cache so we're not in danger of running out of memory.
276 // "1" so that we replace only the FIRST instance.
277 $this->xmlcache
= trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache
, 1));
281 * Very simple convenience function to return the "recstatus" found in person/group/role tags.
282 * 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
284 * @param string $tagdata the tag XML data
285 * @param string $tagname the name of the tag we're interested in
286 * @return int recstatus value
288 protected static function get_recstatus($tagdata, $tagname) {
289 if (preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)) {
290 return intval($matches[1]);
292 return 0; // Unspecified.
297 * Process the group tag. This defines a Moodle course.
299 * @param string $tagcontents The raw contents of the XML element
301 protected function process_group_tag($tagcontents) {
305 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
306 $createnewcourses = $this->get_config('createnewcourses');
307 $updatecourses = $this->get_config('updatecourses');
309 if ($createnewcourses) {
310 require_once("$CFG->dirroot/course/lib.php");
313 // Process tag contents.
314 $group = new stdClass();
315 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
316 $group->coursecode
= trim($matches[1]);
320 if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
321 $group->long
= trim($matches[1]);
325 if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
326 $group->short
= trim($matches[1]);
330 if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
331 $group->full
= trim($matches[1]);
334 if (preg_match('{<org>(.*?)</org>}is', $tagcontents, $matchesorg)) {
335 if (preg_match_all('{<orgunit>(.*?)</orgunit>}is', $matchesorg[1], $matchesorgunit)) {
336 $group->categories
= array_map('trim', $matchesorgunit[1]);
340 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
342 if (empty($group->coursecode
)) {
343 $this->log_line('Error: Unable to find course code in \'group\' element.');
345 // First, truncate the course code if desired.
346 if (intval($truncatecoursecodes) > 0) {
347 $group->coursecode
= ($truncatecoursecodes > 0)
348 ?
substr($group->coursecode
, 0, intval($truncatecoursecodes))
349 : $group->coursecode
;
352 // For compatibility with the (currently inactive) course aliasing, we need this to be an array.
353 $group->coursecode
= array($group->coursecode
);
355 // Third, check if the course(s) exist.
356 foreach ($group->coursecode
as $coursecode) {
357 $coursecode = trim($coursecode);
358 $dbcourse = $DB->get_record('course', array('idnumber' => $coursecode));
360 if (!$createnewcourses) {
361 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
364 // Create the (hidden) course(s) if not found.
365 $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.
368 $course = new stdClass();
369 foreach ($this->coursemappings
as $courseattr => $imsname) {
371 if ($imsname == 'ignore') {
375 // Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
376 if ($imsname == 'coursecode') {
377 $course->{$courseattr} = $coursecode;
378 } else if (!empty($group->{$imsname})) {
379 $course->{$courseattr} = $group->{$imsname};
381 $this->log_line('No ' . $imsname . ' description tag found for '
382 .$coursecode . ' coursecode, using ' . $coursecode . ' instead');
383 $course->{$courseattr} = $coursecode;
387 $course->idnumber
= $coursecode;
388 $course->format
= $courseconfig->format
;
389 $course->visible
= $courseconfig->visible
;
390 $course->newsitems
= $courseconfig->newsitems
;
391 $course->showgrades
= $courseconfig->showgrades
;
392 $course->showreports
= $courseconfig->showreports
;
393 $course->maxbytes
= $courseconfig->maxbytes
;
394 $course->groupmode
= $courseconfig->groupmode
;
395 $course->groupmodeforce
= $courseconfig->groupmodeforce
;
396 $course->enablecompletion
= $courseconfig->enablecompletion
;
397 // Insert default names for teachers/students, from the current language.
399 // Handle course categorisation (taken from the group.org.orgunit or group.org.id fields if present).
400 $course->category
= $this->get_category_from_group($group->categories
);
402 $course->startdate
= time();
403 // Choose a sort order that puts us at the start of the list!
404 $course->sortorder
= 0;
406 $course = create_course($course);
408 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
410 } else if (($recstatus == self
::IMSENTERPRISE_UPDATE
) && $dbcourse) {
411 if ($updatecourses) {
412 // Update course. Allowed fields to be updated are:
413 // Short Name, and Full Name.
415 if (!empty($group->short
)) {
416 if ($group->short
!= $dbcourse->shortname
) {
417 $dbcourse->shortname
= $group->short
;
421 if (!empty($group->full
)) {
422 if ($group->full
!= $dbcourse->fullname
) {
423 $dbcourse->fullname
= $group->full
;
428 update_course($dbcourse);
429 $courseid = $dbcourse->id
;
430 $this->log_line("Updated course $coursecode in Moodle (Moodle ID is $courseid)");
433 // Update courses option is not enabled. Ignore.
434 $this->log_line("Ignoring update to course $coursecode");
436 } else if (($recstatus == self
::IMSENTERPRISE_DELETE
) && $dbcourse) {
437 // If course does exist, but recstatus==3 (delete), then set the course as hidden.
438 $courseid = $dbcourse->id
;
440 course_change_visibility($courseid, $show);
441 $this->log_line("Updated (set to hidden) course $coursecode in Moodle (Moodle ID is $courseid)");
448 * Process the person tag. This defines a Moodle user.
450 * @param string $tagcontents The raw contents of the XML element
452 protected function process_person_tag($tagcontents) {
455 // Get plugin configs.
456 $imssourcedidfallback = $this->get_config('imssourcedidfallback');
457 $fixcaseusernames = $this->get_config('fixcaseusernames');
458 $fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
459 $imsdeleteusers = $this->get_config('imsdeleteusers');
460 $createnewusers = $this->get_config('createnewusers');
461 $imsupdateusers = $this->get_config('imsupdateusers');
463 $person = new stdClass();
464 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
465 $person->idnumber
= trim($matches[1]);
469 if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
470 $person->firstname
= trim($matches[1]);
474 if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
475 $person->lastname
= trim($matches[1]);
479 if (preg_match('{<userid.*?>(.*?)</userid>}is', $tagcontents, $matches)) {
480 $person->username
= trim($matches[1]);
484 if (preg_match('{<userid\s+authenticationtype\s*=\s*"*(.+?)"*>.*?</userid>}is', $tagcontents, $matches)) {
485 $person->auth
= trim($matches[1]);
488 if ($imssourcedidfallback && trim($person->username
) == '') {
489 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied.
490 // NB We don't use an "elseif" because the tag may be supplied-but-empty.
491 $person->username
= $person->idnumber
;
495 if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {
496 $person->email
= trim($matches[1]);
500 if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {
501 $person->url
= trim($matches[1]);
505 if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {
506 $person->city
= trim($matches[1]);
510 if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {
511 $person->country
= trim($matches[1]);
514 // Fix case of some of the fields if required.
515 if ($fixcaseusernames && isset($person->username
)) {
516 $person->username
= strtolower($person->username
);
518 if ($fixcasepersonalnames) {
519 if (isset($person->firstname
)) {
520 $person->firstname
= ucwords(strtolower($person->firstname
));
522 if (isset($person->lastname
)) {
523 $person->lastname
= ucwords(strtolower($person->lastname
));
527 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
529 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on.
530 if ($recstatus == self
::IMSENTERPRISE_DELETE
) {
532 if ($imsdeleteusers) { // If we're allowed to delete user records.
533 // Do not dare to hack the user.deleted field directly in database!!!
534 $params = array('username' => $person->username
, 'mnethostid' => $CFG->mnet_localhost_id
, 'deleted' => 0);
535 if ($user = $DB->get_record('user', $params)) {
536 if (delete_user($user)) {
537 $this->log_line("Deleted user '$person->username' (ID number $person->idnumber).");
539 $this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");
542 $this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");
545 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
547 } else if ($recstatus == self
::IMSENTERPRISE_UPDATE
) { // Update user.
548 if ($imsupdateusers) {
549 if ($id = $DB->get_field('user', 'id', array('idnumber' => $person->idnumber
))) {
551 $DB->update_record('user', $person);
552 $this->log_line("Updated user $person->username");
554 $this->log_line("Ignoring update request for non-existent user $person->username");
557 $this->log_line("Ignoring update request for user $person->username");
560 } else { // Add or update record.
562 // If the user exists (matching sourcedid) then we don't need to do anything.
563 if (!$DB->get_field('user', 'id', array('idnumber' => $person->idnumber
)) && $createnewusers) {
564 // If they don't exist and haven't a defined username, we log this as a potential problem.
565 if ((!isset($person->username
)) ||
(strlen($person->username
) == 0)) {
566 $this->log_line("Cannot create new user for ID # $person->idnumber".
567 "- no username listed in IMS data for this person.");
568 } else if ($DB->get_field('user', 'id', array('username' => $person->username
))) {
569 // If their idnumber is not registered but their user ID is, then add their idnumber to their record.
570 $DB->set_field('user', 'idnumber', $person->idnumber
, array('username' => $person->username
));
573 // If they don't exist and they have a defined username, and $createnewusers == true, we create them.
574 $person->lang
= $CFG->lang
;
575 // TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.
576 if (empty($person->auth
)) {
577 $auth = explode(',', $CFG->auth
);
578 $auth = reset($auth);
579 $person->auth
= $auth;
581 $person->confirmed
= 1;
582 $person->timemodified
= time();
583 $person->mnethostid
= $CFG->mnet_localhost_id
;
584 $id = $DB->insert_record('user', $person);
585 $this->log_line("Created user record ('.$id.') for user '$person->username' (ID number $person->idnumber).");
587 } else if ($createnewusers) {
588 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
590 // It is totally wrong to mess with deleted users flag directly in database!!!
591 // There is no official way to undelete user, sorry..
593 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
601 * Process the membership tag. This defines whether the specified Moodle users
602 * should be added/removed as teachers/students.
604 * @param string $tagcontents The raw contents of the XML element
606 protected function process_membership_tag($tagcontents) {
609 // Get plugin configs.
610 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
611 $imscapitafix = $this->get_config('imscapitafix');
616 // In order to reduce the number of db queries required, group name/id associations are cached in this array.
619 $ship = new stdClass();
621 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
622 $ship->coursecode
= ($truncatecoursecodes > 0)
623 ?
substr(trim($matches[1]), 0, intval($truncatecoursecodes))
625 $ship->courseid
= $DB->get_field('course', 'id', array('idnumber' => $ship->coursecode
));
627 if ($ship->courseid
&& preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER
)) {
628 $courseobj = new stdClass();
629 $courseobj->id
= $ship->courseid
;
631 foreach ($membermatches as $mmatch) {
632 $member = new stdClass();
633 $memberstoreobj = new stdClass();
635 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)) {
636 $member->idnumber
= trim($matches[1]);
640 if (preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)) {
641 // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides.
642 $member->roletype
= trim($matches[1]);
643 } else if ($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)) {
644 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of
645 // the IMS specification! 01 means Student, 02 means Instructor, 3 means ContentDeveloper,
646 // and there are more besides.
647 $member->roletype
= trim($matches[1]);
651 if (preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)) {
652 // 1 means active, 0 means inactive - treat this as enrol vs unenrol.
653 $member->status
= trim($matches[1]);
656 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
657 if ($recstatus == self
::IMSENTERPRISE_DELETE
) {
658 // See above - recstatus of 3 (==delete) is treated the same as status of 0.
662 $timeframe = new stdClass();
663 $timeframe->begin
= 0;
666 if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {
667 $timeframe = $this->decode_timeframe($matches[1]);
671 if (preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is',
672 $mmatch[1], $matches)) {
673 $member->groupname
= trim($matches[1]);
674 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause.
677 // Add or remove this student or teacher to the course...
678 $memberstoreobj->userid
= $DB->get_field('user', 'id', array('idnumber' => $member->idnumber
));
679 $memberstoreobj->enrol
= 'imsenterprise';
680 $memberstoreobj->course
= $ship->courseid
;
681 $memberstoreobj->time
= time();
682 $memberstoreobj->timemodified
= time();
683 if ($memberstoreobj->userid
) {
685 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
686 // Zero means this roletype is supposed to be skipped.
687 $moodleroleid = $this->rolemappings
[$member->roletype
];
688 if (!$moodleroleid) {
689 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid "
690 ."($member->idnumber) in course $memberstoreobj->course");
694 if (intval($member->status
) == 1) {
697 $einstance = $DB->get_record('enrol',
698 array('courseid' => $courseobj->id
, 'enrol' => $memberstoreobj->enrol
));
699 if (empty($einstance)) {
700 // Only add an enrol instance to the course if non-existent.
701 $enrolid = $this->add_instance($courseobj);
702 $einstance = $DB->get_record('enrol', array('id' => $enrolid));
705 $this->enrol_user($einstance, $memberstoreobj->userid
, $moodleroleid, $timeframe->begin
, $timeframe->end
);
707 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) "
708 ."to role $member->roletype in course $memberstoreobj->course");
711 // At this point we can also ensure the group membership is recorded if present.
712 if (isset($member->groupname
)) {
713 // Create the group if it doesn't exist - either way, make sure we know the group ID.
714 if (isset($groupids[$member->groupname
])) {
715 $member->groupid
= $groupids[$member->groupname
]; // Recall the group ID from cache if available.
717 $params = array('courseid' => $ship->courseid
, 'name' => $member->groupname
);
718 if ($groupid = $DB->get_field('groups', 'id', $params)) {
719 $member->groupid
= $groupid;
720 $groupids[$member->groupname
] = $groupid; // Store ID in cache.
722 // Attempt to create the group.
723 $group = new stdClass();
724 $group->name
= $member->groupname
;
725 $group->courseid
= $ship->courseid
;
726 $group->timecreated
= time();
727 $group->timemodified
= time();
728 $groupid = $DB->insert_record('groups', $group);
729 $this->log_line('Added a new group for this course: '.$group->name
);
730 $groupids[$member->groupname
] = $groupid; // Store ID in cache.
731 $member->groupid
= $groupid;
732 // Invalidate the course group data cache just in case.
733 cache_helper
::invalidate_by_definition('core', 'groupdata', array(), array($ship->courseid
));
736 // Add the user-to-group association if it doesn't already exist.
737 if ($member->groupid
) {
738 groups_add_member($member->groupid
, $memberstoreobj->userid
,
739 'enrol_imsenterprise', $einstance->id
);
743 } else if ($this->get_config('imsunenrol')) {
746 $einstances = $DB->get_records('enrol',
747 array('enrol' => $memberstoreobj->enrol
, 'courseid' => $courseobj->id
));
748 foreach ($einstances as $einstance) {
749 // Unenrol the user from all imsenterprise enrolment instances.
750 $this->unenrol_user($einstance, $memberstoreobj->userid
);
754 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
759 $this->log_line("Added $memberstally users to course $ship->coursecode");
760 if ($membersuntally > 0) {
761 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
764 } // End process_membership_tag().
767 * Process the properties tag. The only data from this element
768 * that is relevant is whether a <target> is specified.
770 * @param string $tagcontents The raw contents of the XML element
772 protected function process_properties_tag($tagcontents) {
773 $imsrestricttarget = $this->get_config('imsrestricttarget');
775 if ($imsrestricttarget) {
776 if (!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))) {
777 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
778 $this->continueprocessing
= false;
784 * Store logging information. This does two things: uses the {@link mtrace()}
785 * function to print info to screen/STDOUT, and also writes log to a text file
786 * if a path has been specified.
787 * @param string $string Text to write (newline will be added automatically)
789 protected function log_line($string) {
795 fwrite($this->logfp
, $string . "\n");
800 * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
802 * @param string $string tag to decode.
803 * @return stdClass beginning and/or ending is returned, in unix time, zero indicating not specified.
805 protected static function decode_timeframe($string) {
806 $ret = new stdClass();
807 $ret->begin
= $ret->end
= 0;
808 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
809 // because otherwise the time markers should be ignored (participation should be
810 // allowed outside the period).
811 if (preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)) {
812 $ret->begin
= mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
816 if (preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)) {
817 $ret->end
= mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
823 * Load the role mappings (from the config), so we can easily refer to
824 * how an IMS-E role corresponds to a Moodle role
826 protected function load_role_mappings() {
827 require_once('locallib.php');
829 $imsroles = new imsenterprise_roles();
830 $imsroles = $imsroles->get_imsroles();
832 $this->rolemappings
= array();
833 foreach ($imsroles as $imsrolenum => $imsrolename) {
834 $this->rolemappings
[$imsrolenum] = $this->rolemappings
[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
839 * Load the name mappings (from the config), so we can easily refer to
840 * how an IMS-E course properties corresponds to a Moodle course properties
842 protected function load_course_mappings() {
843 require_once('locallib.php');
845 $imsnames = new imsenterprise_courses();
846 $courseattrs = $imsnames->get_courseattrs();
848 $this->coursemappings
= array();
849 foreach ($courseattrs as $courseattr) {
850 $this->coursemappings
[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
855 * Called whenever anybody tries (from the normal interface) to remove a group
856 * member which is registered as being created by this component. (Not called
857 * when deleting an entire group or course at once.)
858 * @param int $itemid Item ID that was stored in the group_members entry
859 * @param int $groupid Group ID
860 * @param int $userid User ID being removed from group
861 * @return bool True if the remove is permitted, false to give an error
863 public function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {
869 * Get the default category id (often known as 'Miscellaneous'),
870 * statically cached to avoid multiple DB lookups on big imports.
872 * @return int id of default category.
874 private function get_default_category_id() {
876 require_once($CFG->libdir
.'/coursecatlib.php');
878 if ($this->defaultcategoryid
=== null) {
879 $category = coursecat
::get_default();
880 $this->defaultcategoryid
= $category->id
;
883 return $this->defaultcategoryid
;
887 * Find the category using idnumber or name.
889 * @param array $categories List of categories
891 * @return int id of category found.
893 private function get_category_from_group($categories) {
896 if (empty($categories)) {
897 $catid = $this->get_default_category_id();
899 $createnewcategories = $this->get_config('createnewcategories');
900 $categoryseparator = trim($this->get_config('categoryseparator'));
901 $nestedcategories = trim($this->get_config('nestedcategories'));
902 $searchbyidnumber = trim($this->get_config('categoryidnumber'));
904 if (!empty($categoryseparator)) {
905 $sep = '{\\'.$categoryseparator.'}';
909 $fullnestedcatname = '';
911 foreach ($categories as $categoryinfo) {
912 if ($searchbyidnumber) {
913 $values = preg_split($sep, $categoryinfo, -1, PREG_SPLIT_NO_EMPTY
);
914 if (count($values) < 2) {
915 $this->log_line('Category ' . $categoryinfo . ' missing name or idnumber. Using default category instead.');
916 $catid = $this->get_default_category_id();
919 $categoryname = $values[0];
920 $categoryidnumber = $values[1];
922 $categoryname = $categoryinfo;
923 $categoryidnumber = null;
924 if (empty($categoryname)) {
925 $this->log_line('Category ' . $categoryinfo . ' missing name. Using default category instead.');
926 $catid = $this->get_default_category_id();
931 if (!empty($fullnestedcatname)) {
932 $fullnestedcatname .= ' / ';
935 $fullnestedcatname .= $categoryname;
938 // Check if category exist.
940 if ($searchbyidnumber) {
941 $params['idnumber'] = $categoryidnumber;
943 $params['name'] = $categoryname;
945 if ($nestedcategories) {
946 $params['parent'] = $parentid;
949 if ($catid = $DB->get_field('course_categories', 'id', $params)) {
950 continue; // This category already exists.
953 // If we're allowed to create new categories, let's create this one.
954 if ($createnewcategories) {
955 $newcat = new stdClass();
956 $newcat->name
= $categoryname;
957 $newcat->visible
= 0;
958 $newcat->parent
= $parentid;
959 $newcat->idnumber
= $categoryidnumber;
960 $newcat = coursecat
::create($newcat);
961 $catid = $newcat->id
;
962 $this->log_line("Created new (hidden) category '$fullnestedcatname'");
964 // If not found and not allowed to create, stick with default.
965 $this->log_line('Category ' . $categoryinfo . ' not found in Moodle database. Using default category instead.');
966 $catid = $this->get_default_category_id();
976 * Is it possible to delete enrol instance via standard UI?
978 * @param object $instance
981 public function can_delete_instance($instance) {
982 $context = context_course
::instance($instance->courseid
);
983 return has_capability('enrol/imsenterprise:config', $context);
987 * Is it possible to hide/show enrol instance via standard UI?
989 * @param stdClass $instance
992 public function can_hide_show_instance($instance) {
993 $context = context_course
::instance($instance->courseid
);
994 return has_capability('enrol/imsenterprise:config', $context);