enrol_imsenterprise: MDL-46363 including course lib when create_course is
[moodle.git] / enrol / imsenterprise / lib.php
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
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.
8 //
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.
13 //
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/>.
17 /**
18  * IMS Enterprise file enrolment plugin.
19  *
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
27  */
29 defined('MOODLE_INTERNAL') || die();
31 require_once($CFG->dirroot.'/group/lib.php');
34 /**
35  * IMS Enterprise file enrolment plugin.
36  *
37  * @copyright  2010 Eugene Venter
38  * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
39  */
40 class enrol_imsenterprise_plugin extends enrol_plugin {
42     /**
43      * @var $logfp resource file pointer for writing log data to.
44      */
45     protected $logfp;
47     /**
48      * @var $continueprocessing bool flag to determine if processing should continue.
49      */
50     protected $continueprocessing;
52     /**
53      * @var $xmlcache string cache of xml lines.
54      */
55     protected $xmlcache;
57     /**
58      * @var $coursemappings array of mappings between IMS data fields and moodle course fields.
59      */
60     protected $coursemappings;
62     /**
63      * @var $rolemappings array of mappings between IMS roles and moodle roles.
64      */
65     protected $rolemappings;
67     /**
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.
71      *
72      */
73     public function cron() {
74         global $CFG;
76         // Get configs.
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.
86         } else {
87             $filename = $imsfilelocation;
88         }
90         $this->logfp = false;
91         if (!empty($logtolocation)) {
92             $this->logfp = fopen($logtolocation, 'a');
93         }
95         $fileisnew = false;
96         if ( file_exists($filename) ) {
97             core_php_time_limit::raise();
98             $starttime = time();
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)) {
116                 $fileisnew = true;
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.');
121             } else {
122                 $fileisnew = true; // Let's process it!
123             }
125             if ($fileisnew) {
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) {
133                     $line = 0;
134                     while ((!feof($fh)) && $this->continueprocessing) {
136                         $line++;
137                         $curline = fgets($fh);
138                         $this->xmlcache .= $curline; // Add a line onto the XML cache.
140                         while (true) {
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');
157                             } else {
158                                 break;
159                             }
160                         }
161                     }
162                     fclose($fh);
163                     fix_course_sortorder();
164                 }
166                 $timeelapsed = time() - $starttime;
167                 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
169             }
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);
176         } else {
177             $this->log_line('File not found: '.$filename);
178         }
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)) {
184                 if ($this->logfp) {
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";
188                 } else {
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";
192                 }
193             } else {
194                 $msg .= "Logging is currently not active.";
195             }
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.');
212         }
214         if ($this->logfp) {
215             fclose($this->logfp);
216         }
218     }
220     /**
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.
223      *
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).
227      */
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) {
231             return false;
232         } else if (preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)) {
233             return $matches[1];
234         } else {
235             return false;
236         }
237     }
239     /**
240      * Remove complete tag from the cached data (including all its contents) - so
241      * that the cache doesn't grow to unmanageable size
242      *
243      * @param string $tagname Name of tag to look for
244      */
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));
249     }
251     /**
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".
254      *
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
258      */
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]);
262         } else {
263             return 0; // Unspecified.
264         }
265     }
267     /**
268      * Process the group tag. This defines a Moodle course.
269      *
270      * @param string $tagcontents The raw contents of the XML element
271      */
272     protected function process_group_tag($tagcontents) {
273         global $DB, $CFG;
275         // Get configs.
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");
282         }
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]);
287         }
289         if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
290             $group->long = trim($matches[1]);
291         }
292         if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
293             $group->short = trim($matches[1]);
294         }
295         if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
296             $group->full = trim($matches[1]);
297         }
299         if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
300             $group->category = trim($matches[1]);
301         }
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.');
307         } else {
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;
313             }
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.");
324                     } else {
326                         // Create the (hidden) course(s) if not found
327                         $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.
329                         // New course.
330                         $course = new stdClass();
331                         foreach ($this->coursemappings as $courseattr => $imsname) {
333                             if ($imsname == 'ignore') {
334                                 continue;
335                             }
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};
342                             } else {
343                                 $this->log_line('No ' . $imsname . ' description tag found for '
344                                     .$coursecode . ' coursecode, using ' . $coursecode . ' instead');
345                                 $course->{$courseattr} = $coursecode;
346                             }
347                         }
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");
374                             } else {
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();
379                             }
380                         } else {
381                             $course->category = $this->get_default_category_id();
382                         }
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)");
390                     }
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));
394                 }
395             }
396         }
397     }
399     /**
400      * Process the person tag. This defines a Moodle user.
401      *
402      * @param string $tagcontents The raw contents of the XML element
403      */
404     protected function process_person_tag($tagcontents) {
405         global $CFG, $DB;
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]);
417         }
418         if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
419             $person->firstname = trim($matches[1]);
420         }
421         if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
422             $person->lastname = trim($matches[1]);
423         }
424         if (preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)) {
425             $person->username = trim($matches[1]);
426         }
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;
431         }
432         if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {
433             $person->email = trim($matches[1]);
434         }
435         if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {
436             $person->url = trim($matches[1]);
437         }
438         if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {
439             $person->city = trim($matches[1]);
440         }
441         if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {
442             $person->country = trim($matches[1]);
443         }
445         // Fix case of some of the fields if required.
446         if ($fixcaseusernames && isset($person->username)) {
447             $person->username = strtolower($person->username);
448         }
449         if ($fixcasepersonalnames) {
450             if (isset($person->firstname)) {
451                 $person->firstname = ucwords(strtolower($person->firstname));
452             }
453             if (isset($person->lastname)) {
454                 $person->lastname = ucwords(strtolower($person->lastname));
455             }
456         }
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).");
469                     } else {
470                         $this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");
471                     }
472                 } else {
473                     $this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");
474                 }
475             } else {
476                 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
477             }
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));
490                 } else {
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).");
503                 }
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..
509             } else {
510                 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
511             }
513         }
515     }
517     /**
518      * Process the membership tag. This defines whether the specified Moodle users
519      * should be added/removed as teachers/students.
520      *
521      * @param string $tagcontents The raw contents of the XML element
522      */
523     protected function process_membership_tag($tagcontents) {
524         global $DB;
526         // Get plugin configs.
527         $truncatecoursecodes = $this->get_config('truncatecoursecodes');
528         $imscapitafix = $this->get_config('imscapitafix');
530         $memberstally = 0;
531         $membersuntally = 0;
533         // In order to reduce the number of db queries required, group name/id associations are cached in this array.
534         $groupids = 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))
541                 : trim($matches[1]);
542             $ship->courseid = $DB->get_field('course', 'id', array('idnumber' => $ship->coursecode));
543         }
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]);
553                 }
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]);
562                 }
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]);
566                 }
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.
571                     $member->status = 0;
572                 }
574                 $timeframe = new stdClass();
575                 $timeframe->begin = 0;
576                 $timeframe->end = 0;
577                 if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {
578                     $timeframe = $this->decode_timeframe($matches[1]);
579                 }
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.
584                 }
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");
600                         continue;
601                     }
603                     if (intval($member->status) == 1) {
604                         // Enrol the member.
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));
612                         }
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");
618                         $memberstally++;
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.
625                             } else {
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.
630                                 } else {
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));
643                                 }
644                             }
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);
649                             }
650                         }
652                     } else if ($this->get_config('imsunenrol')) {
653                         // Unenrol member.
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);
660                         }
662                         $membersuntally++;
663                         $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
664                     }
666                 }
667             }
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");
671             }
672         }
673     } // End process_membership_tag().
675     /**
676      * Process the properties tag. The only data from this element
677      * that is relevant is whether a <target> is specified.
678      *
679      * @param string $tagcontents The raw contents of the XML element
680      */
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;
688             }
689         }
690     }
692     /**
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)
697      */
698     protected function log_line($string) {
700         if (!PHPUNIT_TEST) {
701             mtrace($string);
702         }
703         if ($this->logfp) {
704             fwrite($this->logfp, $string . "\n");
705         }
706     }
708     /**
709      * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
710      *
711      * @param string $string tag to decode.
712      * @return stdClass beginning and/or ending is returned, in unix time, zero indicating not specified.
713      */
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]);
722         }
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]);
725         }
726         return $ret;
727     }
729     /**
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
732      */
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);
742         }
743     }
745     /**
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
748      */
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);
758         }
759     }
761     /**
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
769      */
770     public function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {
771         return false;
772     }
775     /**
776      * Get the default category id (often known as 'Miscellaneous'),
777      * statically cached to avoid multiple DB lookups on big imports.
778      *
779      * @return int id of default category.
780      */
781     private function get_default_category_id() {
782         global $CFG;
783         require_once($CFG->libdir.'/coursecatlib.php');
785         static $defaultcategoryid = null;
787         if ($defaultcategoryid === null) {
788             $category = coursecat::get_default();
789             $defaultcategoryid = $category->id;
790         }
792         return $defaultcategoryid;
793     }