MDL-46427 Course: Correcting dimmed course link class on profile pages.
[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;
275         // Get configs.
276         $truncatecoursecodes    = $this->get_config('truncatecoursecodes');
277         $createnewcourses       = $this->get_config('createnewcourses');
278         $createnewcategories    = $this->get_config('createnewcategories');
280         // Process tag contents.
281         $group = new stdClass();
282         if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
283             $group->coursecode = trim($matches[1]);
284         }
286         if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
287             $group->long = trim($matches[1]);
288         }
289         if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
290             $group->short = trim($matches[1]);
291         }
292         if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
293             $group->full = trim($matches[1]);
294         }
296         if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
297             $group->category = trim($matches[1]);
298         }
300         $recstatus = ($this->get_recstatus($tagcontents, 'group'));
302         if (empty($group->coursecode)) {
303             $this->log_line('Error: Unable to find course code in \'group\' element.');
304         } else {
305             // First, truncate the course code if desired.
306             if (intval($truncatecoursecodes) > 0) {
307                 $group->coursecode = ($truncatecoursecodes > 0)
308                     ? substr($group->coursecode, 0, intval($truncatecoursecodes))
309                     : $group->coursecode;
310             }
312             // For compatibility with the (currently inactive) course aliasing, we need this to be an array.
313             $group->coursecode = array($group->coursecode);
315             // Third, check if the course(s) exist.
316             foreach ($group->coursecode as $coursecode) {
317                 $coursecode = trim($coursecode);
318                 if (!$DB->get_field('course', 'id', array('idnumber' => $coursecode))) {
319                     if (!$createnewcourses) {
320                         $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
321                     } else {
323                         // Create the (hidden) course(s) if not found
324                         $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.
326                         // New course.
327                         $course = new stdClass();
328                         foreach ($this->coursemappings as $courseattr => $imsname) {
330                             if ($imsname == 'ignore') {
331                                 continue;
332                             }
334                             // Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
335                             if ($imsname == 'coursecode') {
336                                 $course->{$courseattr} = $coursecode;
337                             } else if (!empty($group->{$imsname})) {
338                                 $course->{$courseattr} = $group->{$imsname};
339                             } else {
340                                 $this->log_line('No ' . $imsname . ' description tag found for '
341                                     .$coursecode . ' coursecode, using ' . $coursecode . ' instead');
342                                 $course->{$courseattr} = $coursecode;
343                             }
344                         }
346                         $course->idnumber = $coursecode;
347                         $course->format = $courseconfig->format;
348                         $course->visible = $courseconfig->visible;
349                         $course->newsitems = $courseconfig->newsitems;
350                         $course->showgrades = $courseconfig->showgrades;
351                         $course->showreports = $courseconfig->showreports;
352                         $course->maxbytes = $courseconfig->maxbytes;
353                         $course->groupmode = $courseconfig->groupmode;
354                         $course->groupmodeforce = $courseconfig->groupmodeforce;
355                         $course->enablecompletion = $courseconfig->enablecompletion;
356                         // Insert default names for teachers/students, from the current language.
358                         // Handle course categorisation (taken from the group.org.orgunit field if present).
359                         if (!empty($group->category)) {
360                             // If the category is defined and exists in Moodle, we want to store it in that one.
361                             if ($catid = $DB->get_field('course_categories', 'id', array('name' => $group->category))) {
362                                 $course->category = $catid;
363                             } else if ($createnewcategories) {
364                                 // Else if we're allowed to create new categories, let's create this one.
365                                 $newcat = new stdClass();
366                                 $newcat->name = $group->category;
367                                 $newcat->visible = 0;
368                                 $catid = $DB->insert_record('course_categories', $newcat);
369                                 $course->category = $catid;
370                                 $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
371                             } else {
372                                 // If not found and not allowed to create, stick with default.
373                                 $this->log_line('Category '.$group->category.' not found in Moodle database, so using '.
374                                     'default category instead.');
375                                 $course->category = $this->get_default_category_id();
376                             }
377                         } else {
378                             $course->category = $this->get_default_category_id();
379                         }
380                         $course->startdate = time();
381                         // Choose a sort order that puts us at the start of the list!
382                         $course->sortorder = 0;
384                         $course = create_course($course);
386                         $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
387                     }
388                 } else if ($recstatus == 3 && ($courseid = $DB->get_field('course', 'id', array('idnumber' => $coursecode)))) {
389                     // If course does exist, but recstatus==3 (delete), then set the course as hidden.
390                     $DB->set_field('course', 'visible', '0', array('id' => $courseid));
391                 }
392             }
393         }
394     }
396     /**
397      * Process the person tag. This defines a Moodle user.
398      *
399      * @param string $tagcontents The raw contents of the XML element
400      */
401     protected function process_person_tag($tagcontents) {
402         global $CFG, $DB;
404         // Get plugin configs.
405         $imssourcedidfallback   = $this->get_config('imssourcedidfallback');
406         $fixcaseusernames       = $this->get_config('fixcaseusernames');
407         $fixcasepersonalnames   = $this->get_config('fixcasepersonalnames');
408         $imsdeleteusers         = $this->get_config('imsdeleteusers');
409         $createnewusers         = $this->get_config('createnewusers');
411         $person = new stdClass();
412         if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
413             $person->idnumber = trim($matches[1]);
414         }
415         if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
416             $person->firstname = trim($matches[1]);
417         }
418         if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
419             $person->lastname = trim($matches[1]);
420         }
421         if (preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)) {
422             $person->username = trim($matches[1]);
423         }
424         if ($imssourcedidfallback && trim($person->username) == '') {
425             // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
426             // NB We don't use an "elseif" because the tag may be supplied-but-empty.
427             $person->username = $person->idnumber;
428         }
429         if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {
430             $person->email = trim($matches[1]);
431         }
432         if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {
433             $person->url = trim($matches[1]);
434         }
435         if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {
436             $person->city = trim($matches[1]);
437         }
438         if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {
439             $person->country = trim($matches[1]);
440         }
442         // Fix case of some of the fields if required.
443         if ($fixcaseusernames && isset($person->username)) {
444             $person->username = strtolower($person->username);
445         }
446         if ($fixcasepersonalnames) {
447             if (isset($person->firstname)) {
448                 $person->firstname = ucwords(strtolower($person->firstname));
449             }
450             if (isset($person->lastname)) {
451                 $person->lastname = ucwords(strtolower($person->lastname));
452             }
453         }
455         $recstatus = ($this->get_recstatus($tagcontents, 'person'));
457         // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on.
458         if ($recstatus == 3) {
460             if ($imsdeleteusers) { // If we're allowed to delete user records.
461                 // Do not dare to hack the user.deleted field directly in database!!!
462                 $params = array('username' => $person->username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted ' => 0);
463                 if ($user = $DB->get_record('user', $params)) {
464                     if (delete_user($user)) {
465                         $this->log_line("Deleted user '$person->username' (ID number $person->idnumber).");
466                     } else {
467                         $this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");
468                     }
469                 } else {
470                     $this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");
471                 }
472             } else {
473                 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
474             }
476         } else { // Add or update record.
478             // If the user exists (matching sourcedid) then we don't need to do anything.
479             if (!$DB->get_field('user', 'id', array('idnumber' => $person->idnumber)) && $createnewusers) {
480                 // If they don't exist and haven't a defined username, we log this as a potential problem.
481                 if ((!isset($person->username)) || (strlen($person->username) == 0)) {
482                     $this->log_line("Cannot create new user for ID # $person->idnumber".
483                         "- no username listed in IMS data for this person.");
484                 } else if ($DB->get_field('user', 'id', array('username' => $person->username))) {
485                     // If their idnumber is not registered but their user ID is, then add their idnumber to their record.
486                     $DB->set_field('user', 'idnumber', $person->idnumber, array('username' => $person->username));
487                 } else {
489                     // If they don't exist and they have a defined username, and $createnewusers == true, we create them.
490                     $person->lang = $CFG->lang;
491                     // TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.
492                     $auth = explode(',', $CFG->auth);
493                     $auth = reset($auth);
494                     $person->auth = $auth;
495                     $person->confirmed = 1;
496                     $person->timemodified = time();
497                     $person->mnethostid = $CFG->mnet_localhost_id;
498                     $id = $DB->insert_record('user', $person);
499                     $this->log_line("Created user record ('.$id.') for user '$person->username' (ID number $person->idnumber).");
500                 }
501             } else if ($createnewusers) {
502                 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
504                 // It is totally wrong to mess with deleted users flag directly in database!!!
505                 // There is no official way to undelete user, sorry..
506             } else {
507                 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
508             }
510         }
512     }
514     /**
515      * Process the membership tag. This defines whether the specified Moodle users
516      * should be added/removed as teachers/students.
517      *
518      * @param string $tagcontents The raw contents of the XML element
519      */
520     protected function process_membership_tag($tagcontents) {
521         global $DB;
523         // Get plugin configs.
524         $truncatecoursecodes = $this->get_config('truncatecoursecodes');
525         $imscapitafix = $this->get_config('imscapitafix');
527         $memberstally = 0;
528         $membersuntally = 0;
530         // In order to reduce the number of db queries required, group name/id associations are cached in this array.
531         $groupids = array();
533         $ship = new stdClass();
535         if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
536             $ship->coursecode = ($truncatecoursecodes > 0)
537                 ? substr(trim($matches[1]), 0, intval($truncatecoursecodes))
538                 : trim($matches[1]);
539             $ship->courseid = $DB->get_field('course', 'id', array('idnumber' => $ship->coursecode));
540         }
541         if ($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)) {
542             $courseobj = new stdClass();
543             $courseobj->id = $ship->courseid;
545             foreach ($membermatches as $mmatch) {
546                 $member = new stdClass();
547                 $memberstoreobj = new stdClass();
548                 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)) {
549                     $member->idnumber = trim($matches[1]);
550                 }
551                 if (preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)) {
552                     // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides.
553                     $member->roletype = trim($matches[1]);
554                 } else if ($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)) {
555                     // The XML that comes out of Capita Student Records seems to contain a misinterpretation of
556                     // the IMS specification! 01 means Student, 02 means Instructor, 3 means ContentDeveloper,
557                     // and there are more besides.
558                     $member->roletype = trim($matches[1]);
559                 }
560                 if (preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)) {
561                     // 1 means active, 0 means inactive - treat this as enrol vs unenrol.
562                     $member->status = trim($matches[1]);
563                 }
565                 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
566                 if ($recstatus == 3) {
567                     // See above - recstatus of 3 (==delete) is treated the same as status of 0.
568                     $member->status = 0;
569                 }
571                 $timeframe = new stdClass();
572                 $timeframe->begin = 0;
573                 $timeframe->end = 0;
574                 if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {
575                     $timeframe = $this->decode_timeframe($matches[1]);
576                 }
577                 if (preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is',
578                         $mmatch[1], $matches)) {
579                     $member->groupname = trim($matches[1]);
580                     // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause.
581                 }
583                 // Add or remove this student or teacher to the course...
584                 $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber' => $member->idnumber));
585                 $memberstoreobj->enrol = 'imsenterprise';
586                 $memberstoreobj->course = $ship->courseid;
587                 $memberstoreobj->time = time();
588                 $memberstoreobj->timemodified = time();
589                 if ($memberstoreobj->userid) {
591                     // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
592                     // Zero means this roletype is supposed to be skipped.
593                     $moodleroleid = $this->rolemappings[$member->roletype];
594                     if (!$moodleroleid) {
595                         $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid "
596                             ."($member->idnumber) in course $memberstoreobj->course");
597                         continue;
598                     }
600                     if (intval($member->status) == 1) {
601                         // Enrol the member.
603                         $einstance = $DB->get_record('enrol',
604                             array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));
605                         if (empty($einstance)) {
606                             // Only add an enrol instance to the course if non-existent.
607                             $enrolid = $this->add_instance($courseobj);
608                             $einstance = $DB->get_record('enrol', array('id' => $enrolid));
609                         }
611                         $this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);
613                         $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) "
614                             ."to role $member->roletype in course $memberstoreobj->course");
615                         $memberstally++;
617                         // At this point we can also ensure the group membership is recorded if present.
618                         if (isset($member->groupname)) {
619                             // Create the group if it doesn't exist - either way, make sure we know the group ID.
620                             if (isset($groupids[$member->groupname])) {
621                                 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available.
622                             } else {
623                                 $params = array('courseid' => $ship->courseid, 'name' => $member->groupname);
624                                 if ($groupid = $DB->get_field('groups', 'id', $params)) {
625                                     $member->groupid = $groupid;
626                                     $groupids[$member->groupname] = $groupid; // Store ID in cache.
627                                 } else {
628                                     // Attempt to create the group.
629                                     $group = new stdClass();
630                                     $group->name = $member->groupname;
631                                     $group->courseid = $ship->courseid;
632                                     $group->timecreated = time();
633                                     $group->timemodified = time();
634                                     $groupid = $DB->insert_record('groups', $group);
635                                     $this->log_line('Added a new group for this course: '.$group->name);
636                                     $groupids[$member->groupname] = $groupid; // Store ID in cache.
637                                     $member->groupid = $groupid;
638                                     // Invalidate the course group data cache just in case.
639                                     cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($ship->courseid));
640                                 }
641                             }
642                             // Add the user-to-group association if it doesn't already exist.
643                             if ($member->groupid) {
644                                 groups_add_member($member->groupid, $memberstoreobj->userid,
645                                     'enrol_imsenterprise', $einstance->id);
646                             }
647                         }
649                     } else if ($this->get_config('imsunenrol')) {
650                         // Unenrol member.
652                         $einstances = $DB->get_records('enrol',
653                             array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));
654                         foreach ($einstances as $einstance) {
655                             // Unenrol the user from all imsenterprise enrolment instances.
656                             $this->unenrol_user($einstance, $memberstoreobj->userid);
657                         }
659                         $membersuntally++;
660                         $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
661                     }
663                 }
664             }
665             $this->log_line("Added $memberstally users to course $ship->coursecode");
666             if ($membersuntally > 0) {
667                 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
668             }
669         }
670     } // End process_membership_tag().
672     /**
673      * Process the properties tag. The only data from this element
674      * that is relevant is whether a <target> is specified.
675      *
676      * @param string $tagcontents The raw contents of the XML element
677      */
678     protected function process_properties_tag($tagcontents) {
679         $imsrestricttarget = $this->get_config('imsrestricttarget');
681         if ($imsrestricttarget) {
682             if (!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))) {
683                 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
684                 $this->continueprocessing = false;
685             }
686         }
687     }
689     /**
690      * Store logging information. This does two things: uses the {@link mtrace()}
691      * function to print info to screen/STDOUT, and also writes log to a text file
692      * if a path has been specified.
693      * @param string $string Text to write (newline will be added automatically)
694      */
695     protected function log_line($string) {
697         if (!PHPUNIT_TEST) {
698             mtrace($string);
699         }
700         if ($this->logfp) {
701             fwrite($this->logfp, $string . "\n");
702         }
703     }
705     /**
706      * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
707      *
708      * @param string $string tag to decode.
709      * @return stdClass beginning and/or ending is returned, in unix time, zero indicating not specified.
710      */
711     protected static function decode_timeframe($string) {
712         $ret = new stdClass();
713         $ret->begin = $ret->end = 0;
714         // Explanatory note: The matching will ONLY match if the attribute restrict="1"
715         // because otherwise the time markers should be ignored (participation should be
716         // allowed outside the period).
717         if (preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)) {
718             $ret->begin = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
719         }
720         if (preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)) {
721             $ret->end = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
722         }
723         return $ret;
724     }
726     /**
727      * Load the role mappings (from the config), so we can easily refer to
728      * how an IMS-E role corresponds to a Moodle role
729      */
730     protected function load_role_mappings() {
731         require_once('locallib.php');
733         $imsroles = new imsenterprise_roles();
734         $imsroles = $imsroles->get_imsroles();
736         $this->rolemappings = array();
737         foreach ($imsroles as $imsrolenum => $imsrolename) {
738             $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
739         }
740     }
742     /**
743      * Load the name mappings (from the config), so we can easily refer to
744      * how an IMS-E course properties corresponds to a Moodle course properties
745      */
746     protected function load_course_mappings() {
747         require_once('locallib.php');
749         $imsnames = new imsenterprise_courses();
750         $courseattrs = $imsnames->get_courseattrs();
752         $this->coursemappings = array();
753         foreach ($courseattrs as $courseattr) {
754             $this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
755         }
756     }
758     /**
759      * Called whenever anybody tries (from the normal interface) to remove a group
760      * member which is registered as being created by this component. (Not called
761      * when deleting an entire group or course at once.)
762      * @param int $itemid Item ID that was stored in the group_members entry
763      * @param int $groupid Group ID
764      * @param int $userid User ID being removed from group
765      * @return bool True if the remove is permitted, false to give an error
766      */
767     public function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {
768         return false;
769     }
772     /**
773      * Get the default category id (often known as 'Miscellaneous'),
774      * statically cached to avoid multiple DB lookups on big imports.
775      *
776      * @return int id of default category.
777      */
778     private function get_default_category_id() {
779         global $CFG;
780         require_once($CFG->libdir.'/coursecatlib.php');
782         static $defaultcategoryid = null;
784         if ($defaultcategoryid === null) {
785             $category = coursecat::get_default();
786             $defaultcategoryid = $category->id;
787         }
789         return $defaultcategoryid;
790     }