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');
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 $logfp resource file pointer for writing log data to.
48 * @var $continueprocessing bool flag to determine if processing should continue.
50 protected $continueprocessing;
53 * @var $xmlcache string cache of xml lines.
58 * @var $coursemappings array of mappings between IMS data fields and moodle course fields.
60 protected $coursemappings;
63 * @var $rolemappings array of mappings between IMS roles and moodle roles.
65 protected $rolemappings;
68 * Read in an IMS Enterprise file.
69 * Originally designed to handle v1.1 files but should be able to handle
70 * earlier types as well, I believe.
73 public function cron() {
77 $imsfilelocation = $this->get_config('imsfilelocation');
78 $logtolocation = $this->get_config('logtolocation');
79 $mailadmins = $this->get_config('mailadmins');
80 $prevtime = $this->get_config('prev_time');
81 $prevmd5 = $this->get_config('prev_md5');
82 $prevpath = $this->get_config('prev_path');
84 if (empty($imsfilelocation)) {
85 $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location.
87 $filename = $imsfilelocation;
91 if (!empty($logtolocation)) {
92 $this->logfp = fopen($logtolocation, 'a');
96 if ( file_exists($filename) ) {
97 core_php_time_limit::raise();
100 $this->log_line('----------------------------------------------------------------------');
101 $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
102 $this->log_line('Found file '.$filename);
103 $this->xmlcache = '';
105 // Make sure we understand how to map the IMS-E roles to Moodle roles.
106 $this->load_role_mappings();
107 // Make sure we understand how to map the IMS-E course names to Moodle course names.
108 $this->load_course_mappings();
110 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron.
111 $filemtime = filemtime($filename);
113 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
114 // This is so we avoid wasting the server's efforts processing a file unnecessarily.
115 if (empty($prevpath) || ($filename != $prevpath)) {
117 } else if (isset($prevtime) && ($filemtime <= $prevtime)) {
118 $this->log_line('File modification time is not more recent than last update - skipping processing.');
119 } else if (isset($prevmd5) && ($md5 == $prevmd5)) {
120 $this->log_line('File MD5 hash is same as on last update - skipping processing.');
122 $fileisnew = true; // Let's process it!
127 // The <properties> tag is allowed to halt processing if we're demanding a matching target.
128 $this->continueprocessing = true;
130 // Run through the file and process the group/person entries.
131 if (($fh = fopen($filename, "r")) != false) {
134 while ((!feof($fh)) && $this->continueprocessing) {
137 $curline = fgets($fh);
138 $this->xmlcache .= $curline; // Add a line onto the XML cache.
141 // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
142 // Must always make sure to remove tags from cache so they don't clog up our memory.
143 if ($tagcontents = $this->full_tag_found_in_cache('group', $curline)) {
144 $this->process_group_tag($tagcontents);
145 $this->remove_tag_from_cache('group');
146 } else if ($tagcontents = $this->full_tag_found_in_cache('person', $curline)) {
147 $this->process_person_tag($tagcontents);
148 $this->remove_tag_from_cache('person');
149 } else if ($tagcontents = $this->full_tag_found_in_cache('membership', $curline)) {
150 $this->process_membership_tag($tagcontents);
151 $this->remove_tag_from_cache('membership');
152 } else if ($tagcontents = $this->full_tag_found_in_cache('comments', $curline)) {
153 $this->remove_tag_from_cache('comments');
154 } else if ($tagcontents = $this->full_tag_found_in_cache('properties', $curline)) {
155 $this->process_properties_tag($tagcontents);
156 $this->remove_tag_from_cache('properties');
163 fix_course_sortorder();
166 $timeelapsed = time() - $starttime;
167 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
171 // These variables are stored so we can compare them against the IMS file, next time round.
172 $this->set_config('prev_time', $filemtime);
173 $this->set_config('prev_md5', $md5);
174 $this->set_config('prev_path', $filename);
177 $this->log_line('File not found: '.$filename);
180 if (!empty($mailadmins) && $fileisnew) {
181 $timeelapsed = isset($timeelapsed) ? $timeelapsed : 0;
182 $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
183 if (!empty($logtolocation)) {
185 $msg .= "Log data has been written to:\n";
186 $msg .= "$logtolocation\n";
187 $msg .= "(Log file size: ".ceil(filesize($logtolocation) / 1024)."Kb)\n\n";
189 $msg .= "The log file appears not to have been successfully written.\n";
190 $msg .= "Check that the file is writeable by the server:\n";
191 $msg .= "$logtolocation\n\n";
194 $msg .= "Logging is currently not active.";
197 $eventdata = new stdClass();
198 $eventdata->modulename = 'moodle';
199 $eventdata->component = 'enrol_imsenterprise';
200 $eventdata->name = 'imsenterprise_enrolment';
201 $eventdata->userfrom = get_admin();
202 $eventdata->userto = get_admin();
203 $eventdata->subject = "Moodle IMS Enterprise enrolment notification";
204 $eventdata->fullmessage = $msg;
205 $eventdata->fullmessageformat = FORMAT_PLAIN;
206 $eventdata->fullmessagehtml = '';
207 $eventdata->smallmessage = '';
208 message_send($eventdata);
210 $this->log_line('Notification email sent to administrator.');
215 fclose($this->logfp);
221 * Check if a complete tag is found in the cached data, which usually happens
222 * when the end of the tag has only just been loaded into the cache.
224 * @param string $tagname Name of tag to look for
225 * @param string $latestline The very last line in the cache (used for speeding up the match)
226 * @return bool|string false, or the contents of the tag (including start and end).
228 protected function full_tag_found_in_cache($tagname, $latestline) {
229 // Return entire element if found. Otherwise return false.
230 if (strpos(strtolower($latestline), '</'.strtolower($tagname).'>') === false) {
232 } else if (preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)) {
240 * Remove complete tag from the cached data (including all its contents) - so
241 * that the cache doesn't grow to unmanageable size
243 * @param string $tagname Name of tag to look for
245 protected function remove_tag_from_cache($tagname) {
246 // Trim the cache so we're not in danger of running out of memory.
247 // "1" so that we replace only the FIRST instance.
248 $this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1));
252 * Very simple convenience function to return the "recstatus" found in person/group/role tags.
253 * 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
255 * @param string $tagdata the tag XML data
256 * @param string $tagname the name of the tag we're interested in
257 * @return int recstatus value
259 protected static function get_recstatus($tagdata, $tagname) {
260 if (preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)) {
261 return intval($matches[1]);
263 return 0; // Unspecified.
268 * Process the group tag. This defines a Moodle course.
270 * @param string $tagcontents The raw contents of the XML element
272 protected function process_group_tag($tagcontents) {
276 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
277 $createnewcourses = $this->get_config('createnewcourses');
278 $createnewcategories = $this->get_config('createnewcategories');
280 if ($createnewcourses) {
281 require_once("$CFG->dirroot/course/lib.php");
283 // Process tag contents.
284 $group = new stdClass();
285 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
286 $group->coursecode = trim($matches[1]);
289 if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
290 $group->long = trim($matches[1]);
292 if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
293 $group->short = trim($matches[1]);
295 if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
296 $group->full = trim($matches[1]);
299 if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
300 $group->category = trim($matches[1]);
303 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
305 if (empty($group->coursecode)) {
306 $this->log_line('Error: Unable to find course code in \'group\' element.');
308 // First, truncate the course code if desired.
309 if (intval($truncatecoursecodes) > 0) {
310 $group->coursecode = ($truncatecoursecodes > 0)
311 ? substr($group->coursecode, 0, intval($truncatecoursecodes))
312 : $group->coursecode;
315 // For compatibility with the (currently inactive) course aliasing, we need this to be an array.
316 $group->coursecode = array($group->coursecode);
318 // Third, check if the course(s) exist.
319 foreach ($group->coursecode as $coursecode) {
320 $coursecode = trim($coursecode);
321 if (!$DB->get_field('course', 'id', array('idnumber' => $coursecode))) {
322 if (!$createnewcourses) {
323 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
326 // Create the (hidden) course(s) if not found
327 $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.
330 $course = new stdClass();
331 foreach ($this->coursemappings as $courseattr => $imsname) {
333 if ($imsname == 'ignore') {
337 // Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
338 if ($imsname == 'coursecode') {
339 $course->{$courseattr} = $coursecode;
340 } else if (!empty($group->{$imsname})) {
341 $course->{$courseattr} = $group->{$imsname};
343 $this->log_line('No ' . $imsname . ' description tag found for '
344 .$coursecode . ' coursecode, using ' . $coursecode . ' instead');
345 $course->{$courseattr} = $coursecode;
349 $course->idnumber = $coursecode;
350 $course->format = $courseconfig->format;
351 $course->visible = $courseconfig->visible;
352 $course->newsitems = $courseconfig->newsitems;
353 $course->showgrades = $courseconfig->showgrades;
354 $course->showreports = $courseconfig->showreports;
355 $course->maxbytes = $courseconfig->maxbytes;
356 $course->groupmode = $courseconfig->groupmode;
357 $course->groupmodeforce = $courseconfig->groupmodeforce;
358 $course->enablecompletion = $courseconfig->enablecompletion;
359 // Insert default names for teachers/students, from the current language.
361 // Handle course categorisation (taken from the group.org.orgunit field if present).
362 if (!empty($group->category)) {
363 // If the category is defined and exists in Moodle, we want to store it in that one.
364 if ($catid = $DB->get_field('course_categories', 'id', array('name' => $group->category))) {
365 $course->category = $catid;
366 } else if ($createnewcategories) {
367 // Else if we're allowed to create new categories, let's create this one.
368 $newcat = new stdClass();
369 $newcat->name = $group->category;
370 $newcat->visible = 0;
371 $catid = $DB->insert_record('course_categories', $newcat);
372 $course->category = $catid;
373 $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
375 // If not found and not allowed to create, stick with default.
376 $this->log_line('Category '.$group->category.' not found in Moodle database, so using '.
377 'default category instead.');
378 $course->category = $this->get_default_category_id();
381 $course->category = $this->get_default_category_id();
383 $course->startdate = time();
384 // Choose a sort order that puts us at the start of the list!
385 $course->sortorder = 0;
387 $course = create_course($course);
389 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
391 } else if ($recstatus == 3 && ($courseid = $DB->get_field('course', 'id', array('idnumber' => $coursecode)))) {
392 // If course does exist, but recstatus==3 (delete), then set the course as hidden.
393 $DB->set_field('course', 'visible', '0', array('id' => $courseid));
400 * Process the person tag. This defines a Moodle user.
402 * @param string $tagcontents The raw contents of the XML element
404 protected function process_person_tag($tagcontents) {
407 // Get plugin configs.
408 $imssourcedidfallback = $this->get_config('imssourcedidfallback');
409 $fixcaseusernames = $this->get_config('fixcaseusernames');
410 $fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
411 $imsdeleteusers = $this->get_config('imsdeleteusers');
412 $createnewusers = $this->get_config('createnewusers');
414 $person = new stdClass();
415 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
416 $person->idnumber = trim($matches[1]);
418 if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
419 $person->firstname = trim($matches[1]);
421 if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
422 $person->lastname = trim($matches[1]);
424 if (preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)) {
425 $person->username = trim($matches[1]);
427 if ($imssourcedidfallback && trim($person->username) == '') {
428 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
429 // NB We don't use an "elseif" because the tag may be supplied-but-empty.
430 $person->username = $person->idnumber;
432 if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {
433 $person->email = trim($matches[1]);
435 if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {
436 $person->url = trim($matches[1]);
438 if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {
439 $person->city = trim($matches[1]);
441 if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {
442 $person->country = trim($matches[1]);
445 // Fix case of some of the fields if required.
446 if ($fixcaseusernames && isset($person->username)) {
447 $person->username = strtolower($person->username);
449 if ($fixcasepersonalnames) {
450 if (isset($person->firstname)) {
451 $person->firstname = ucwords(strtolower($person->firstname));
453 if (isset($person->lastname)) {
454 $person->lastname = ucwords(strtolower($person->lastname));
458 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
460 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on.
461 if ($recstatus == 3) {
463 if ($imsdeleteusers) { // If we're allowed to delete user records.
464 // Do not dare to hack the user.deleted field directly in database!!!
465 $params = array('username' => $person->username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted ' => 0);
466 if ($user = $DB->get_record('user', $params)) {
467 if (delete_user($user)) {
468 $this->log_line("Deleted user '$person->username' (ID number $person->idnumber).");
470 $this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");
473 $this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");
476 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
479 } else { // Add or update record.
481 // If the user exists (matching sourcedid) then we don't need to do anything.
482 if (!$DB->get_field('user', 'id', array('idnumber' => $person->idnumber)) && $createnewusers) {
483 // If they don't exist and haven't a defined username, we log this as a potential problem.
484 if ((!isset($person->username)) || (strlen($person->username) == 0)) {
485 $this->log_line("Cannot create new user for ID # $person->idnumber".
486 "- no username listed in IMS data for this person.");
487 } else if ($DB->get_field('user', 'id', array('username' => $person->username))) {
488 // If their idnumber is not registered but their user ID is, then add their idnumber to their record.
489 $DB->set_field('user', 'idnumber', $person->idnumber, array('username' => $person->username));
492 // If they don't exist and they have a defined username, and $createnewusers == true, we create them.
493 $person->lang = $CFG->lang;
494 // TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.
495 $auth = explode(',', $CFG->auth);
496 $auth = reset($auth);
497 $person->auth = $auth;
498 $person->confirmed = 1;
499 $person->timemodified = time();
500 $person->mnethostid = $CFG->mnet_localhost_id;
501 $id = $DB->insert_record('user', $person);
502 $this->log_line("Created user record ('.$id.') for user '$person->username' (ID number $person->idnumber).");
504 } else if ($createnewusers) {
505 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
507 // It is totally wrong to mess with deleted users flag directly in database!!!
508 // There is no official way to undelete user, sorry..
510 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
518 * Process the membership tag. This defines whether the specified Moodle users
519 * should be added/removed as teachers/students.
521 * @param string $tagcontents The raw contents of the XML element
523 protected function process_membership_tag($tagcontents) {
526 // Get plugin configs.
527 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
528 $imscapitafix = $this->get_config('imscapitafix');
533 // In order to reduce the number of db queries required, group name/id associations are cached in this array.
536 $ship = new stdClass();
538 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
539 $ship->coursecode = ($truncatecoursecodes > 0)
540 ? substr(trim($matches[1]), 0, intval($truncatecoursecodes))
542 $ship->courseid = $DB->get_field('course', 'id', array('idnumber' => $ship->coursecode));
544 if ($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)) {
545 $courseobj = new stdClass();
546 $courseobj->id = $ship->courseid;
548 foreach ($membermatches as $mmatch) {
549 $member = new stdClass();
550 $memberstoreobj = new stdClass();
551 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)) {
552 $member->idnumber = trim($matches[1]);
554 if (preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)) {
555 // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides.
556 $member->roletype = trim($matches[1]);
557 } else if ($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)) {
558 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of
559 // the IMS specification! 01 means Student, 02 means Instructor, 3 means ContentDeveloper,
560 // and there are more besides.
561 $member->roletype = trim($matches[1]);
563 if (preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)) {
564 // 1 means active, 0 means inactive - treat this as enrol vs unenrol.
565 $member->status = trim($matches[1]);
568 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
569 if ($recstatus == 3) {
570 // See above - recstatus of 3 (==delete) is treated the same as status of 0.
574 $timeframe = new stdClass();
575 $timeframe->begin = 0;
577 if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {
578 $timeframe = $this->decode_timeframe($matches[1]);
580 if (preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is',
581 $mmatch[1], $matches)) {
582 $member->groupname = trim($matches[1]);
583 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause.
586 // Add or remove this student or teacher to the course...
587 $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber' => $member->idnumber));
588 $memberstoreobj->enrol = 'imsenterprise';
589 $memberstoreobj->course = $ship->courseid;
590 $memberstoreobj->time = time();
591 $memberstoreobj->timemodified = time();
592 if ($memberstoreobj->userid) {
594 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
595 // Zero means this roletype is supposed to be skipped.
596 $moodleroleid = $this->rolemappings[$member->roletype];
597 if (!$moodleroleid) {
598 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid "
599 ."($member->idnumber) in course $memberstoreobj->course");
603 if (intval($member->status) == 1) {
606 $einstance = $DB->get_record('enrol',
607 array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));
608 if (empty($einstance)) {
609 // Only add an enrol instance to the course if non-existent.
610 $enrolid = $this->add_instance($courseobj);
611 $einstance = $DB->get_record('enrol', array('id' => $enrolid));
614 $this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);
616 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) "
617 ."to role $member->roletype in course $memberstoreobj->course");
620 // At this point we can also ensure the group membership is recorded if present.
621 if (isset($member->groupname)) {
622 // Create the group if it doesn't exist - either way, make sure we know the group ID.
623 if (isset($groupids[$member->groupname])) {
624 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available.
626 $params = array('courseid' => $ship->courseid, 'name' => $member->groupname);
627 if ($groupid = $DB->get_field('groups', 'id', $params)) {
628 $member->groupid = $groupid;
629 $groupids[$member->groupname] = $groupid; // Store ID in cache.
631 // Attempt to create the group.
632 $group = new stdClass();
633 $group->name = $member->groupname;
634 $group->courseid = $ship->courseid;
635 $group->timecreated = time();
636 $group->timemodified = time();
637 $groupid = $DB->insert_record('groups', $group);
638 $this->log_line('Added a new group for this course: '.$group->name);
639 $groupids[$member->groupname] = $groupid; // Store ID in cache.
640 $member->groupid = $groupid;
641 // Invalidate the course group data cache just in case.
642 cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($ship->courseid));
645 // Add the user-to-group association if it doesn't already exist.
646 if ($member->groupid) {
647 groups_add_member($member->groupid, $memberstoreobj->userid,
648 'enrol_imsenterprise', $einstance->id);
652 } else if ($this->get_config('imsunenrol')) {
655 $einstances = $DB->get_records('enrol',
656 array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));
657 foreach ($einstances as $einstance) {
658 // Unenrol the user from all imsenterprise enrolment instances.
659 $this->unenrol_user($einstance, $memberstoreobj->userid);
663 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
668 $this->log_line("Added $memberstally users to course $ship->coursecode");
669 if ($membersuntally > 0) {
670 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
673 } // End process_membership_tag().
676 * Process the properties tag. The only data from this element
677 * that is relevant is whether a <target> is specified.
679 * @param string $tagcontents The raw contents of the XML element
681 protected function process_properties_tag($tagcontents) {
682 $imsrestricttarget = $this->get_config('imsrestricttarget');
684 if ($imsrestricttarget) {
685 if (!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))) {
686 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
687 $this->continueprocessing = false;
693 * Store logging information. This does two things: uses the {@link mtrace()}
694 * function to print info to screen/STDOUT, and also writes log to a text file
695 * if a path has been specified.
696 * @param string $string Text to write (newline will be added automatically)
698 protected function log_line($string) {
704 fwrite($this->logfp, $string . "\n");
709 * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
711 * @param string $string tag to decode.
712 * @return stdClass beginning and/or ending is returned, in unix time, zero indicating not specified.
714 protected static function decode_timeframe($string) {
715 $ret = new stdClass();
716 $ret->begin = $ret->end = 0;
717 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
718 // because otherwise the time markers should be ignored (participation should be
719 // allowed outside the period).
720 if (preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)) {
721 $ret->begin = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
723 if (preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)) {
724 $ret->end = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
730 * Load the role mappings (from the config), so we can easily refer to
731 * how an IMS-E role corresponds to a Moodle role
733 protected function load_role_mappings() {
734 require_once('locallib.php');
736 $imsroles = new imsenterprise_roles();
737 $imsroles = $imsroles->get_imsroles();
739 $this->rolemappings = array();
740 foreach ($imsroles as $imsrolenum => $imsrolename) {
741 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
746 * Load the name mappings (from the config), so we can easily refer to
747 * how an IMS-E course properties corresponds to a Moodle course properties
749 protected function load_course_mappings() {
750 require_once('locallib.php');
752 $imsnames = new imsenterprise_courses();
753 $courseattrs = $imsnames->get_courseattrs();
755 $this->coursemappings = array();
756 foreach ($courseattrs as $courseattr) {
757 $this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
762 * Called whenever anybody tries (from the normal interface) to remove a group
763 * member which is registered as being created by this component. (Not called
764 * when deleting an entire group or course at once.)
765 * @param int $itemid Item ID that was stored in the group_members entry
766 * @param int $groupid Group ID
767 * @param int $userid User ID being removed from group
768 * @return bool True if the remove is permitted, false to give an error
770 public function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {
776 * Get the default category id (often known as 'Miscellaneous'),
777 * statically cached to avoid multiple DB lookups on big imports.
779 * @return int id of default category.
781 private function get_default_category_id() {
783 require_once($CFG->libdir.'/coursecatlib.php');
785 static $defaultcategoryid = null;
787 if ($defaultcategoryid === null) {
788 $category = coursecat::get_default();
789 $defaultcategoryid = $category->id;
792 return $defaultcategoryid;