e10bb2415aefc40e3aebae76e44f25d09e0a38b6
[moodle.git] / enrol / imsenterprise / enrol.php
1 <?php
2 /**
3 * @author Dan Stowell
4 * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
5 * @package enrol_imsenterprise
6 */
7 require_once($CFG->dirroot.'/group/lib.php');
9 // The following flags are set in the configuration
10 // $CFG->enrol_imsfilelocation:        where is the file we are looking for?
11 // $CFG->enrol_logtolocation:          if you want to store a log of processing, specify filepath here
12 // $CFG->enrol_allowinternal:          allow internal enrolment in courses
13 // $CFG->enrol_emailadmins:            email a notification to the admin
14 // $CFG->enrol_createnewusers:         should this script create user accounts for those who don't seem to be registered yet?
15 // $CFG->enrol_imsdeleteusers:         should this script mark user accounts as deleted, if the data requests this?
16 // $CFG->enrol_fixcaseusernames:       whether to force all usernames to lowercase
17 // $CFG->enrol_fixcasepersonalnames:   convert personal names, e.g. from "TOM VEK" to "Tom Vek"
18 // $CFG->enrol_truncatecoursecodes:    if this number is greater than zero, truncate the codes found in the IMS data to the given number of characters
19 // $CFG->enrol_imsunenrol:             allow this script to UNENROL students/tutors from courses (if the data marks them as having left the course)
20 // $CFG->enrol_createnewcourses:       should this script create a (hidden, empty) course for any course that doesn't seem to have been registered yet?
21 // $CFG->enrol_createnewcategories:    should this script create a (hidden) category if Moodle doesn't have one by the same name as the desired one?
22 // $CFG->enrol_imssourcedidfallback:   some systems don't output a <userid> element (contrary to the specifications). If this is the case, activating this setting will cause us to use the <sourcedid><id> element instead as the userid. This may or may not be desirable in your situation.
23 // $CFG->enrol_includephoto:           Process IMS <photo> tag to create user photo. Be warned that this may add significant server load.
25 /*
27 Note for programmers:
29 This class uses regular expressions to mine the data file. The main reason is
30 that XML handling changes from PHP 4 to PHP 5, so this should work on both.
32 One drawback is that the pattern-matching doesn't (currently) handle XML
33 namespaces - it only copes with a <group> tag if it says <group>, and not
34 (for example) <ims:group>.
36 This should also be able to handle VERY LARGE FILES - so the entire IMS file is
37 NOT loaded into memory at once. It's handled line-by-line, 'forgetting' tags as
38 soon as they are processed.
40 N.B. The "sourcedid" ID code is translated to Moodle's "idnumber" field, both
41 for users and for courses.
43 */
46 class enrolment_plugin_imsenterprise {
48     var $log;
50 // The "roles" hard-coded in the IMS specification are:
51 var $imsroles = array(
52 '01'=>'Learner',
53 '02'=>'Instructor',
54 '03'=>'Content Developer',
55 '04'=>'Member',
56 '05'=>'Manager',
57 '06'=>'Mentor',
58 '07'=>'Administrator',
59 '08'=>'TeachingAssistant',
60 );
61 // PLEASE NOTE: It may seem odd that "Content Developer" has a space in it
62 // but "TeachingAssistant" doesn't. That's what the spec says though!!!
65 /**
66 * This function is only used when first setting up the plugin, to
67 * decide which role assignments to recommend by default.
68 * For example, IMS role '01' is 'Learner', so may map to 'student' in Moodle.
69 */
70 function determine_default_rolemapping($imscode) {
71     global $DB;
73     switch($imscode) {
74         case '01':
75         case '04':
76             $shortname = 'student';
77             break;
78         case '06':
79         case '08':
80             $shortname = 'teacher';
81             break;
82         case '02':
83         case '03':
84             $shortname = 'editingteacher';
85             break;
86         case '05':
87         case '07':
88             $shortname = 'admin';
89             break;
90         default:
91             return 0; // Zero for no match
92     }
93     return $DB->get_field('role', 'id', array('shortname'=>$shortname));
94 }
98 /// Override the base config_form() function
99 function config_form($frm) {
100     global $CFG, $OUTPUT, $imsroles;
102     $vars = array('enrol_imsfilelocation', 'enrol_logtolocation', 'enrol_createnewusers', 'enrol_fixcaseusernames', 'enrol_fixcasepersonalnames', 'enrol_truncatecoursecodes',
103             'enrol_createnewcourses', 'enrol_createnewcategories', 'enrol_createnewusers', 'enrol_mailadmins',
104             'enrol_imsunenrol', 'enrol_imssourcedidfallback', 'enrol_imscapitafix', 'enrol_imsrestricttarget', 'enrol_imsdeleteusers',
105             'enrol_imse_imsrolemap01','enrol_imse_imsrolemap02','enrol_imse_imsrolemap03','enrol_imse_imsrolemap04',
106             'enrol_imse_imsrolemap05','enrol_imse_imsrolemap06','enrol_imse_imsrolemap07','enrol_imse_imsrolemap08');
107     foreach ($vars as $var) {
108         if (!isset($frm->$var)) {
109             $frm->$var = '';
110         }
111     }
112     include ("$CFG->dirroot/enrol/imsenterprise/config.html");
116 /// Override the base process_config() function
117 function process_config($config) {
119     if (!isset($config->enrol_imsfilelocation)) {
120         $config->enrol_imsfilelocation = '';
121     }
122     set_config('enrol_imsfilelocation', $config->enrol_imsfilelocation);
124     if (!isset($config->enrol_logtolocation)) {
125         $config->enrol_logtolocation = '';
126     }
127     set_config('enrol_logtolocation', $config->enrol_logtolocation);
129     if (!isset($config->enrol_fixcaseusernames)) {
130         $config->enrol_fixcaseusernames = '';
131     }
132     set_config('enrol_fixcaseusernames', $config->enrol_fixcaseusernames);
134     if (!isset($config->enrol_fixcasepersonalnames)) {
135         $config->enrol_fixcasepersonalnames = '';
136     }
137     set_config('enrol_fixcasepersonalnames', $config->enrol_fixcasepersonalnames);
139     if (!isset($config->enrol_truncatecoursecodes)) {
140         $config->enrol_truncatecoursecodes = 0;
141     }
142     set_config('enrol_truncatecoursecodes', intval($config->enrol_truncatecoursecodes));
144     if (!isset($config->enrol_createnewcourses)) {
145         $config->enrol_createnewcourses = '';
146     }
147     set_config('enrol_createnewcourses', $config->enrol_createnewcourses);
149     if (!isset($config->enrol_createnewcategories)) {
150         $config->enrol_createnewcategories = '';
151     }
152     set_config('enrol_createnewcategories', $config->enrol_createnewcategories);
154     if (!isset($config->enrol_createnewusers)) {
155         $config->enrol_createnewusers = '';
156     }
157     set_config('enrol_createnewusers', $config->enrol_createnewusers);
159     if (!isset($config->enrol_imsdeleteusers)) {
160         $config->enrol_imsdeleteusers = '';
161     }
162     set_config('enrol_imsdeleteusers', $config->enrol_imsdeleteusers);
164     if (!isset($config->enrol_mailadmins)) {
165         $config->enrol_mailadmins = '';
166     }
167     set_config('enrol_mailadmins', $config->enrol_mailadmins);
169     if (!isset($config->enrol_imsunenrol)) {
170         $config->enrol_imsunenrol = '';
171     }
172     set_config('enrol_imsunenrol', $config->enrol_imsunenrol);
174     if (!isset($config->enrol_imssourcedidfallback)) {
175         $config->enrol_imssourcedidfallback = '';
176     }
177     set_config('enrol_imssourcedidfallback', $config->enrol_imssourcedidfallback);
179     if (!isset($config->enrol_imscapitafix)) {
180         $config->enrol_imscapitafix = '';
181     }
182     set_config('enrol_imscapitafix', $config->enrol_imscapitafix);
184     //Antoni Mas. 07/12/2005. Incloem la opci de la foto dels usuaris
185     if (!isset($config->enrol_processphoto)) {
186         $config->enrol_processphoto = '';
187     }
188     set_config('enrol_processphoto', $config->enrol_processphoto);
190     if (!isset($config->enrol_imsrestricttarget)) {
191         $config->enrol_imsrestricttarget = '';
192     }
193     set_config('enrol_imsrestricttarget', $config->enrol_imsrestricttarget);
197     foreach($this->imsroles as $imsrolenum=>$imsrolename){
198         $configref = 'enrol_imse_imsrolemap' . $imsrolenum;
199         if (!isset($config->$configref)) {
200             echo "<p>Resetting config->$configref</p>";
201             $config->$configref = 0;
202         }
203         set_config('enrol_imse_imsrolemap' . $imsrolenum, $config->$configref);
204     }
207     set_config('enrol_ims_prev_md5',  ''); // Forget the MD5 - to force re-processing if we change the config setting
208     set_config('enrol_ims_prev_time', ''); // Ditto
209     return true;
213 function get_access_icons($course){}
215 /**
216 * Read in an IMS Enterprise file.
217 * Originally designed to handle v1.1 files but should be able to handle
218 * earlier types as well, I believe.
220 */
221 function cron() {
222     global $CFG;
224     if (empty($CFG->enrol_imsfilelocation)) {
225         // $filename = "$CFG->dirroot/enrol/imsenterprise/example.xml";  // Default location
226         $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml";  // Default location
227     } else {
228         $filename = $CFG->enrol_imsfilelocation;
229     }
231     $this->logfp = false; // File pointer for writing log data to
232     if(!empty($CFG->enrol_logtolocation)) {
233         $this->logfp = fopen($CFG->enrol_logtolocation, 'a');
234     }
238     if ( file_exists($filename) ) {
239         @set_time_limit(0);
240         $starttime = time();
242         $this->log_line('----------------------------------------------------------------------');
243         $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
244         $this->log_line('Found file '.$filename);
245         $this->xmlcache = '';
247         // Make sure we understand how to map the IMS-E roles to Moodle roles
248         $this->load_role_mappings();
250         $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
251         $filemtime = filemtime($filename);
253         // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
254         // This is so we avoid wasting the server's efforts processing a file unnecessarily
255         if(empty($CFG->enrol_ims_prev_path)  || ($filename != $CFG->enrol_ims_prev_path)){
256             $fileisnew = true;
257         }elseif(isset($CFG->enrol_ims_prev_time) && ($filemtime <= $CFG->enrol_ims_prev_time)){
258             $fileisnew = false;
259             $this->log_line('File modification time is not more recent than last update - skipping processing.');
260         }elseif(isset($CFG->enrol_ims_prev_md5) && ($md5 == $CFG->enrol_ims_prev_md5)){
261             $fileisnew = false;
262             $this->log_line('File MD5 hash is same as on last update - skipping processing.');
263         }else{
264             $fileisnew = true; // Let's process it!
265         }
267         if($fileisnew){
269             $listoftags = array('group', 'person', 'member', 'membership', 'comments', 'properties'); // The list of tags which should trigger action (even if only cache trimming)
270             $this->continueprocessing = true; // The <properties> tag is allowed to halt processing if we're demanding a matching target
272             // FIRST PASS: Run through the file and process the group/person entries
273             if (($fh = fopen($filename, "r")) != false) {
275                 $line = 0;
276                 while ((!feof($fh)) && $this->continueprocessing) {
278                     $line++;
279                     $curline = fgets($fh);
280                     $this->xmlcache .= $curline; // Add a line onto the XML cache
282                     while(true){
283                       // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
284                       // Must always make sure to remove tags from cache so they don't clog up our memory
285                       if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
286                           $this->process_group_tag($tagcontents);
287                           $this->remove_tag_from_cache('group');
288                       }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
289                           $this->process_person_tag($tagcontents);
290                           $this->remove_tag_from_cache('person');
291                       }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
292                           $this->process_membership_tag($tagcontents);
293                           $this->remove_tag_from_cache('membership');
294                       }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
295                           $this->remove_tag_from_cache('comments');
296                       }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
297                           $this->process_properties_tag($tagcontents);
298                           $this->remove_tag_from_cache('properties');
299                       }else{
300                     break;
301                   }
302                 } // End of while-tags-are-detected
303                 } // end of while loop
304                 fclose($fh);
305                 fix_course_sortorder();
306             } // end of if(file_open) for first pass
308             /*
311             SECOND PASS REMOVED
312             Since the IMS specification v1.1 insists that "memberships" should come last,
313             and since vendors seem to have done this anyway (even with 1.0),
314             we can sensibly perform the import in one fell swoop.
317             // SECOND PASS: Now go through the file and process the membership entries
318             $this->xmlcache = '';
319             if (($fh = fopen($filename, "r")) != false) {
320                 $line = 0;
321                 while ((!feof($fh)) && $this->continueprocessing) {
322                     $line++;
323                     $curline = fgets($fh);
324                     $this->xmlcache .= $curline; // Add a line onto the XML cache
326                     while(true){
327                   // Must always make sure to remove tags from cache so they don't clog up our memory
328                   if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
329                           $this->remove_tag_from_cache('group');
330                       }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
331                           $this->remove_tag_from_cache('person');
332                       }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
333                           $this->process_membership_tag($tagcontents);
334                           $this->remove_tag_from_cache('membership');
335                       }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
336                           $this->remove_tag_from_cache('comments');
337                       }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
338                           $this->remove_tag_from_cache('properties');
339                       }else{
340                     break;
341                   }
342                 }
343                 } // end of while loop
344                 fclose($fh);
345             } // end of if(file_open) for second pass
348            */
350             $timeelapsed = time() - $starttime;
351             $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
354         } // END of "if file is new"
357         // These variables are stored so we can compare them against the IMS file, next time round.
358         set_config('enrol_ims_prev_time', $filemtime);
359         set_config('enrol_ims_prev_md5',  $md5);
360         set_config('enrol_ims_prev_path', $filename);
364     }else{ // end of if(file_exists)
365         $this->log_line('File not found: '.$filename);
366     }
368     if (!empty($CFG->enrol_mailadmins)) {
369         $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
370         if(!empty($CFG->enrol_logtolocation)){
371             if($this->logfp){
372                 $msg .= "Log data has been written to:\n";
373                 $msg .= "$CFG->enrol_logtolocation\n";
374                 $msg .= "(Log file size: ".ceil(filesize($CFG->enrol_logtolocation)/1024)."Kb)\n\n";
375             }else{
376                 $msg .= "The log file appears not to have been successfully written.\nCheck that the file is writeable by the server:\n";
377                 $msg .= "$CFG->enrol_logtolocation\n\n";
378             }
379         }else{
380             $msg .= "Logging is currently not active.";
381         }
383         $eventdata = new object();
384         $eventdata->modulename        = 'moodle';
385         $eventdata->userfrom          = get_admin();
386         $eventdata->userto            = get_admin();
387         $eventdata->subject           = "Moodle IMS Enterprise enrolment notification";
388         $eventdata->fullmessage       = $msg;
389         $eventdata->fullmessageformat = FORMAT_PLAIN;
390         $eventdata->fullmessagehtml   = '';
391         $eventdata->smallmessage      = '';
392         message_send($eventdata);
394         $this->log_line('Notification email sent to administrator.');
396     }
398     if($this->logfp){
399       fclose($this->logfp);
400     }
403 } // end of cron() function
405 /**
406 * Check if a complete tag is found in the cached data, which usually happens
407 * when the end of the tag has only just been loaded into the cache.
408 * Returns either false, or the contents of the tag (including start and end).
409 * @param string $tagname Name of tag to look for
410 * @param string $latestline The very last line in the cache (used for speeding up the match)
411 */
412 function full_tag_found_in_cache($tagname, $latestline){ // Return entire element if found. Otherwise return false.
413     if(strpos(strtolower($latestline), '</'.strtolower($tagname).'>')===false){
414         return false;
415     }elseif(preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)){
416         return $matches[1];
417     }else return false;
420 /**
421 * Remove complete tag from the cached data (including all its contents) - so
422 * that the cache doesn't grow to unmanageable size
423 * @param string $tagname Name of tag to look for
424 */
425 function remove_tag_from_cache($tagname){ // Trim the cache so we're not in danger of running out of memory.
426     ///echo "<p>remove_tag_from_cache: $tagname</p>";  flush();  ob_flush();
427     //  echo "<p>remove_tag_from_cache:<br />".htmlspecialchars($this->xmlcache);
428     $this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1)); // "1" so that we replace only the FIRST instance
429     //  echo "<br />".htmlspecialchars($this->xmlcache)."</p>";
432 /**
433 * Very simple convenience function to return the "recstatus" found in person/group/role tags.
434 * 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
435 * @param string $tagdata the tag XML data
436 * @param string $tagname the name of the tag we're interested in
437 */
438 function get_recstatus($tagdata, $tagname){
439     if(preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)){
440         // echo "<p>get_recstatus($tagname) found status of $matches[1]</p>";
441         return intval($matches[1]);
442     }else{
443         // echo "<p>get_recstatus($tagname) found nothing</p>";
444         return 0; // Unspecified
445     }
448 /**
449 * Process the group tag. This defines a Moodle course.
450 * @param string $tagconents The raw contents of the XML element
451 */
452 function process_group_tag($tagcontents){
453     global $CFG, $DB;
455     // Process tag contents
456     unset($group);
457     if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
458         $group->coursecode = trim($matches[1]);
459     }
460     if(preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)){
461         $group->description = trim($matches[1]);
462     }
463     if(preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)){
464         $group->category = trim($matches[1]);
465     }
467     $recstatus = ($this->get_recstatus($tagcontents, 'group'));
468     //echo "<p>get_recstatus for this group returned $recstatus</p>";
470     if(!(strlen($group->coursecode)>0)){
471         $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.');
472     }else{
473         // First, truncate the course code if desired
474         if(intval($CFG->enrol_truncatecoursecodes)>0){
475             $group->coursecode = ($CFG->enrol_truncatecoursecodes > 0)
476                      ? substr($group->coursecode, 0, intval($CFG->enrol_truncatecoursecodes))
477                      : $group->coursecode;
478         }
480         /* -----------Course aliasing is DEACTIVATED until a more general method is in place---------------
482        // Second, look in the course alias table to see if the code should be translated to something else
483         if($aliases = $DB->get_field('enrol_coursealias', 'toids', array('fromid'=>$group->coursecode))){
484             $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases");
485             // Alias is allowed to be a comma-separated list, so let's split it
486             $group->coursecode = explode(',', $aliases);
487         }
488        */
490         // For compatibility with the (currently inactive) course aliasing, we need this to be an array
491         $group->coursecode = array($group->coursecode);
493         // Third, check if the course(s) exist
494         foreach($group->coursecode as $coursecode){
495             $coursecode = trim($coursecode);
496             if(!$DB->get_field('course', 'id', array('idnumber'=>$coursecode))) {
497               if(!$CFG->enrol_createnewcourses) {
498                   $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
499               } else {
500                 // Create the (hidden) course(s) if not found
501                 $course = new object();
502                 $course->fullname = $group->description;
503                 $course->shortname = $coursecode;
504                 $course->idnumber = $coursecode;
505                 $course->format = 'topics';
506                 $course->visible = 0;
507                 // Insert default names for teachers/students, from the current language
508                 $site = get_site();
510                 // Handle course categorisation (taken from the group.org.orgunit field if present)
511                 if(strlen($group->category)>0){
512                     // If the category is defined and exists in Moodle, we want to store it in that one
513                     if($catid = $DB->get_field('course_categories', 'id', array('name'=>$group->category))){
514                         $course->category = $catid;
515                     }elseif($CFG->enrol_createnewcategories){
516                         // Else if we're allowed to create new categories, let's create this one
517                         $newcat->name = $group->category;
518                        $newcat->visible = 0;
519                        if($catid = $DB->insert_record('course_categories', $newcat)){
520                            $course->category = $catid;
521                            $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
522                        }else{
523                            $this->log_line('Failed to create new category: '.$newcat->name);
524                        }
525                     }else{
526                         // If not found and not allowed to create, stick with default
527                         $this->log_line('Category '.$group->category.' not found in Moodle database, so using default category instead.');
528                         $course->category = 1;
529                     }
530                 }else{
531                     $course->category = 1;
532                 }
533                 $course->timecreated = time();
534                 $course->startdate = time();
535                 $course->numsections = 1;
536                 // Choose a sort order that puts us at the start of the list!
537                 $course->sortorder = 0;
539                 if ($courseid = $DB->insert_record('course', $course)) {
541                     // Setup the blocks
542                     $course = $DB->get_record('course', array('id' => $courseid));
543                     blocks_add_default_course_blocks($course);
545                     $section = new object();
546                     $section->course = $course->id;   // Create a default section.
547                     $section->section = 0;
548                     $section->summaryformat = FORMAT_HTML;
549                     $section->id = $DB->insert_record("course_sections", $section);
551                     add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
553                     $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
554                 }else{
555                     $this->log_line('Failed to create course '.$coursecode.' in Moodle');
556                 }
557               }
558             }elseif($recstatus==3 && ($courseid = $DB->get_field('course', 'id', array('idnumber'=>$coursecode)))){
559                 // If course does exist, but recstatus==3 (delete), then set the course as hidden
560                 $DB->set_field('course', 'visible', '0', array('id'=>$courseid));
561             }
562         } // End of foreach(coursecode)
563     }
564 } // End process_group_tag()
566 /**
567 * Process the person tag. This defines a Moodle user.
568 * @param string $tagconents The raw contents of the XML element
569 */
570 function process_person_tag($tagcontents){
571     global $CFG, $DB;
573     if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
574         $person->idnumber = trim($matches[1]);
575     }
576     if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){
577         $person->firstname = trim($matches[1]);
578     }
579     if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){
580         $person->lastname = trim($matches[1]);
581     }
582     if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){
583         $person->username = trim($matches[1]);
584     }
585     if($CFG->enrol_imssourcedidfallback && trim($person->username)==''){
586       // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
587       // NB We don't use an "elseif" because the tag may be supplied-but-empty
588         $person->username = $person->idnumber;
589     }
590     if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){
591         $person->email = trim($matches[1]);
592     }
593     if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){
594         $person->url = trim($matches[1]);
595     }
596     if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){
597         $person->city = trim($matches[1]);
598     }
599     if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){
600         $person->country = trim($matches[1]);
601     }
603     // Fix case of some of the fields if required
604     if($CFG->enrol_fixcaseusernames && isset($person->username)){
605         $person->username = strtolower($person->username);
606     }
607     if($CFG->enrol_fixcasepersonalnames){
608         if(isset($person->firstname)){
609             $person->firstname = ucwords(strtolower($person->firstname));
610         }
611         if(isset($person->lastname)){
612             $person->lastname = ucwords(strtolower($person->lastname));
613         }
614     }
616     $recstatus = ($this->get_recstatus($tagcontents, 'person'));
619     // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on
620     // In the "users" table we can do this by setting deleted=1
621     if($recstatus==3){
623         if($CFG->enrol_imsdeleteusers){ // If we're allowed to delete user records
624             // Make sure their "deleted" field is set to one
625             $DB->set_field('user', 'deleted', 1, array('username'=>$person->username));
626             $this->log_line("Marked user record for user '$person->username' (ID number $person->idnumber) as deleted.");
627         }else{
628             $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
629         }
631     }else{ // Add or update record
634         // If the user exists (matching sourcedid) then we don't need to do anything.
635         if(!$DB->get_field('user', 'id', array('idnumber'=>$person->idnumber)) && $CFG->enrol_createnewusers){
636             // If they don't exist and haven't a defined username, we log this as a potential problem.
637             if((!isset($person->username)) || (strlen($person->username)==0)){
638                 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person.");
639             } else if ($DB->get_field('user', 'id', array('username'=>$person->username))){
640                 // If their idnumber is not registered but their user ID is, then add their idnumber to their record
641                 $DB->set_field('user', 'idnumber', $person->idnumber, array('username'=>$person->username));
642             } else {
644             // If they don't exist and they have a defined username, and $CFG->enrol_createnewusers == true, we create them.
645             $person->lang = 'manual'; //TODO: this needs more work due tu multiauth changes
646             $person->auth = $CFG->auth;
647             $person->confirmed = 1;
648             $person->timemodified = time();
649             $person->mnethostid = $CFG->mnet_localhost_id;
650             if($id = $DB->insert_record('user', $person)){
651     /*
652     Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib.
654                                  //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load
655                                  // it into the user's profile. Beware that this may cause a heavy overhead on the server.
656                                  if($CFG->enrol_processphoto){
657                                    if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){
658                                      $person->urlphoto = trim($matches[1]);
659                                    }
660                                    //Habilitam el flag que ens indica que el personatge t foto prpia.
661                                    $person->picture = 1;
662                                    //Llibreria creada per nosaltres mateixos.
663                                    require_once($CFG->dirroot.'/lib/gdlib.php');
664                                    if ($usernew->picture = save_profile_image($id, $person->urlphoto,'user')) { TODO: use process_new_icon() instead
665                                      $DB->set_field('user', 'picture', $usernew->picture, array('id'=>$id));  /// Note picture in DB
666                                    }
667                                  }
668     */
669                     $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber).");
670                 }else{
671                     $this->log_line("Database error while trying to create user record for user '$person->username' (ID number $person->idnumber).");
672                 }
673             }
674         }elseif($CFG->enrol_createnewusers){
675             $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
677             // Make sure their "deleted" field is set to zero.
678             $DB->set_field('user', 'deleted', 0, array('idnumber'=>$person->idnumber));
679         }else{
680             $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
681         }
683     } // End of are-we-deleting-or-adding
685 } // End process_person_tag()
687 /**
688 * Process the membership tag. This defines whether the specified Moodle users
689 * should be added/removed as teachers/students.
690 * @param string $tagconents The raw contents of the XML element
691 */
692 function process_membership_tag($tagcontents){
693     global $CFG, $DB;
694     $memberstally = 0;
695     $membersuntally = 0;
697     // In order to reduce the number of db queries required, group name/id associations are cached in this array:
698     $groupids = array();
700     if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
701         $ship->coursecode = ($CFG->enrol_truncatecoursecodes > 0)
702                                  ? substr(trim($matches[1]), 0, intval($CFG->enrol_truncatecoursecodes))
703                                  : trim($matches[1]);
704         $ship->courseid = $DB->get_field('course', 'id', array('idnumber'=>$ship->coursecode));
705     }
706     if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){
707         foreach($membermatches as $mmatch){
708             unset($member);
709             unset($memberstoreobj);
710             if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){
711                 $member->idnumber = trim($matches[1]);
712             }
713             if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){
714                 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
715             }elseif($CFG->enrol_imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){
716                 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification!
717                 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
718             }
719             if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){
720                 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol
721             }
723             $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
724             if($recstatus==3){
725               $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0
726               //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>";
727             }
729             $timeframe->begin = 0;
730             $timeframe->end = 0;
731             if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){
732                 $timeframe = $this->decode_timeframe($matches[1]);
733             }
734             if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){
735                 $member->groupname = trim($matches[1]);
736                 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause
737             }
739             $rolecontext = get_context_instance(CONTEXT_COURSE, $ship->courseid);
740             $rolecontext = $rolecontext->id; // All we really want is the ID
741 //$this->log_line("Context instance for course $ship->courseid is...");
742 //print_r($rolecontext);
744             // Add or remove this student or teacher to the course...
745             $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber'=>$member->idnumber));
746             $memberstoreobj->enrol = 'imsenterprise';
747             $memberstoreobj->course = $ship->courseid;
748             $memberstoreobj->time = time();
749             $memberstoreobj->timemodified = time();
750             if($memberstoreobj->userid){
752                 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
753                 // Zero means this roletype is supposed to be skipped.
754                 $moodleroleid = $this->rolemappings[$member->roletype];
755                 if(!$moodleroleid){
756                     $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course");
757                     continue;
758                 }
760                 if(intval($member->status) == 1){
762                     // Enrol unsing the generic role_assign() function
764                     //TODO: some real enrolment here
765                     if ((!role_assign($moodleroleid, $memberstoreobj->userid, $rolecontext, 'enrol_imsenterprise')) && (trim($memberstoreobj->userid)!='')) {
766                         $this->log_line("Error enrolling user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
767                     }else{
768                         $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
769                         $memberstally++;
771                         // At this point we can also ensure the group membership is recorded if present
772                         if(isset($member->groupname)){
773                             // Create the group if it doesn't exist - either way, make sure we know the group ID
774                             if(isset($groupids[$member->groupname])){
775                                 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available
776                             }else{
777                                 if($groupid = $DB->get_field('groups', 'id', 'name', $member->groupname, array('courseid'=>$ship->courseid))){
778                                     $member->groupid = $groupid;
779                                     $groupids[$member->groupname] = $groupid; // Store ID in cache
780                                 }else{
781                                     // Attempt to create the group
782                                     $group->name = $member->groupname;
783                                     $group->courseid = $ship->courseid;
784                                     $group->timecreated = time();
785                                     $group->timemodified = time();
786                                     $groupid = $DB->insert_record('groups', $group);
787                                     $this->log_line('Added a new group for this course: '.$group->name);
788                                     $groupids[$member->groupname] = $groupid; // Store ID in cache
789                                     $member->groupid = $groupid;
790                                 }
791                             }
792                             // Add the user-to-group association if it doesn't already exist
793                             if($member->groupid) {
794                                 groups_add_member($member->groupid, $memberstoreobj->userid);
795                             }
796                         } // End of group-enrolment (from member.role.extension.cohort tag)
798                     }
799                 }elseif($CFG->enrol_imsunenrol){
800                     // Unenrol
802                     role_unassign($moodleroleid, $memberstoreobj->userid, 0, $rolecontext, 'imsenterprise');
803                     $membersuntally++;
804                     $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
805                 }
807             }
808         }
809         $this->log_line("Added $memberstally users to course $ship->coursecode");
810         if($membersuntally > 0){
811             $this->log_line("Removed $membersuntally users from course $ship->coursecode");
812         }
813     }
814 } // End process_membership_tag()
816 /**
817 * Process the properties tag. The only data from this element
818 * that is relevant is whether a <target> is specified.
819 * @param string $tagconents The raw contents of the XML element
820 */
821 function process_properties_tag($tagcontents){
822     global $CFG;
824     if($CFG->enrol_imsrestricttarget){
825         if(!(preg_match('{<target>'.preg_quote($CFG->enrol_imsrestricttarget).'</target>}is', $tagcontents, $matches))){
826             $this->log_line("Skipping processing: required target \"$CFG->enrol_imsrestricttarget\" not specified in this data.");
827             $this->continueprocessing = false;
828         }
829     }
832 /**
833 * Store logging information. This does two things: uses the {@link mtrace()}
834 * function to print info to screen/STDOUT, and also writes log to a text file
835 * if a path has been specified.
836 * @param string $string Text to write (newline will be added automatically)
837 */
838 function log_line($string){
839     mtrace($string);
840     if($this->logfp) {
841         fwrite($this->logfp, $string . "\n");
842     }
845 /**
846 * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
847 */
848 function decode_timeframe($string){ // Pass me the INNER CONTENTS of a <timeframe> tag - beginning and/or ending is returned, in unix time, zero indicating not specified
849     $ret->begin = $ret->end = 0;
850     // Explanatory note: The matching will ONLY match if the attribute restrict="1"
851     // because otherwise the time markers should be ignored (participation should be
852     // allowed outside the period)
853     if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){
854         $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
855     }
856     if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){
857         $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
858     }
859     return $ret;
860 } // End decode_timeframe
862 /**
863 * Load the role mappings (from the config), so we can easily refer to
864 * how an IMS-E role corresponds to a Moodle role
865 */
866 function load_role_mappings() {
867     global $DB;
869     $this->rolemappings = array();
870     foreach($this->imsroles as $imsrolenum=>$imsrolename) {
871         $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename]
872             = $DB->get_field('config', 'value', array('name'=>'enrol_imse_imsrolemap' . $imsrolenum));
873     }
876 } // end of class