Added check for <text> fields that contain markup (an error!)
[moodle-linuxchix.git] / enrol / imsenterprise / enrol.php
blob3d364bdf902da0b7cfbf6f1737736597319fbd3f
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->libdir/blocklib.php");
8 require_once($CFG->dirroot.'/group/lib.php');
10 // The following flags are set in the configuration
11 // $CFG->enrol_imsfilelocation: where is the file we are looking for?
12 // $CFG->enrol_logtolocation: if you want to store a log of processing, specify filepath here
13 // $CFG->enrol_allowinternal: allow internal enrolment in courses
14 // $CFG->enrol_emailadmins: email a notification to the admin
15 // $CFG->enrol_createnewusers: should this script create user accounts for those who don't seem to be registered yet?
16 // $CFG->enrol_imsdeleteusers: should this script mark user accounts as deleted, if the data requests this?
17 // $CFG->enrol_fixcaseusernames: whether to force all usernames to lowercase
18 // $CFG->enrol_fixcasepersonalnames: convert personal names, e.g. from "TOM VEK" to "Tom Vek"
19 // $CFG->enrol_truncatecoursecodes: if this number is greater than zero, truncate the codes found in the IMS data to the given number of characters
20 // $CFG->enrol_imsunenrol: allow this script to UNENROL students/tutors from courses (if the data marks them as having left the course)
21 // $CFG->enrol_createnewcourses: should this script create a (hidden, empty) course for any course that doesn't seem to have been registered yet?
22 // $CFG->enrol_createnewcategories: should this script create a (hidden) category if Moodle doesn't have one by the same name as the desired one?
23 // $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.
24 // $CFG->enrol_includephoto: Process IMS <photo> tag to create user photo. Be warned that this may add significant server load.
28 Note for programmers:
30 This class uses regular expressions to mine the data file. The main reason is
31 that XML handling changes from PHP 4 to PHP 5, so this should work on both.
33 One drawback is that the pattern-matching doesn't (currently) handle XML
34 namespaces - it only copes with a <group> tag if it says <group>, and not
35 (for example) <ims:group>.
37 This should also be able to handle VERY LARGE FILES - so the entire IMS file is
38 NOT loaded into memory at once. It's handled line-by-line, 'forgetting' tags as
39 soon as they are processed.
41 N.B. The "sourcedid" ID code is translated to Moodle's "idnumber" field, both
42 for users and for courses.
47 class enrolment_plugin_imsenterprise {
49 var $log;
51 // The "roles" hard-coded in the IMS specification are:
52 var $imsroles = array(
53 '01'=>'Learner',
54 '02'=>'Instructor',
55 '03'=>'Content Developer',
56 '04'=>'Member',
57 '05'=>'Manager',
58 '06'=>'Mentor',
59 '07'=>'Administrator',
60 '08'=>'TeachingAssistant',
62 // PLEASE NOTE: It may seem odd that "Content Developer" has a space in it
63 // but "TeachingAssistant" doesn't. That's what the spec says though!!!
66 /**
67 * This function is only used when first setting up the plugin, to
68 * decide which role assignments to recommend by default.
69 * For example, IMS role '01' is 'Learner', so may map to 'student' in Moodle.
71 function determine_default_rolemapping($imscode) {
72 switch($imscode) {
73 case '01':
74 case '04':
75 $shortname = 'student';
76 break;
77 case '06':
78 case '08':
79 $shortname = 'teacher';
80 break;
81 case '02':
82 case '03':
83 $shortname = 'editingteacher';
84 break;
85 case '05':
86 case '07':
87 $shortname = 'admin';
88 break;
89 default:
90 return 0; // Zero for no match
92 return get_field('role', 'id', 'shortname', $shortname);
97 /// Override the base config_form() function
98 function config_form($frm) {
99 global $CFG, $imsroles;
101 $vars = array('enrol_imsfilelocation', 'enrol_createnewusers', 'enrol_fixcaseusernames', 'enrol_fixcasepersonalnames', 'enrol_truncatecoursecodes',
102 'enrol_createnewcourses', 'enrol_createnewcategories', 'enrol_createnewusers', 'enrol_mailadmins',
103 'enrol_imsunenrol', 'enrol_imssourcedidfallback', 'enrol_imscapitafix', 'enrol_imsrestricttarget', 'enrol_imsdeleteusers',
104 'enrol_imse_imsrolemap01','enrol_imse_imsrolemap02','enrol_imse_imsrolemap03','enrol_imse_imsrolemap04',
105 'enrol_imse_imsrolemap05','enrol_imse_imsrolemap06','enrol_imse_imsrolemap07','enrol_imse_imsrolemap08');
106 foreach ($vars as $var) {
107 if (!isset($frm->$var)) {
108 $frm->$var = '';
111 include ("$CFG->dirroot/enrol/imsenterprise/config.html");
115 /// Override the base process_config() function
116 function process_config($config) {
118 if (!isset($config->enrol_imsfilelocation)) {
119 $config->enrol_imsfilelocation = '';
121 set_config('enrol_imsfilelocation', $config->enrol_imsfilelocation);
123 if (!isset($config->enrol_logtolocation)) {
124 $config->enrol_logtolocation = '';
126 set_config('enrol_logtolocation', $config->enrol_logtolocation);
128 if (!isset($config->enrol_fixcaseusernames)) {
129 $config->enrol_fixcaseusernames = '';
131 set_config('enrol_fixcaseusernames', $config->enrol_fixcaseusernames);
133 if (!isset($config->enrol_fixcasepersonalnames)) {
134 $config->enrol_fixcasepersonalnames = '';
136 set_config('enrol_fixcasepersonalnames', $config->enrol_fixcasepersonalnames);
138 if (!isset($config->enrol_truncatecoursecodes)) {
139 $config->enrol_truncatecoursecodes = 0;
141 set_config('enrol_truncatecoursecodes', intval($config->enrol_truncatecoursecodes));
143 if (!isset($config->enrol_createnewcourses)) {
144 $config->enrol_createnewcourses = '';
146 set_config('enrol_createnewcourses', $config->enrol_createnewcourses);
148 if (!isset($config->enrol_createnewcategories)) {
149 $config->enrol_createnewcategories = '';
151 set_config('enrol_createnewcategories', $config->enrol_createnewcategories);
153 if (!isset($config->enrol_createnewusers)) {
154 $config->enrol_createnewusers = '';
156 set_config('enrol_createnewusers', $config->enrol_createnewusers);
158 if (!isset($config->enrol_imsdeleteusers)) {
159 $config->enrol_imsdeleteusers = '';
161 set_config('enrol_imsdeleteusers', $config->enrol_imsdeleteusers);
163 if (!isset($config->enrol_mailadmins)) {
164 $config->enrol_mailadmins = '';
166 set_config('enrol_mailadmins', $config->enrol_mailadmins);
168 if (!isset($config->enrol_imsunenrol)) {
169 $config->enrol_imsunenrol = '';
171 set_config('enrol_imsunenrol', $config->enrol_imsunenrol);
173 if (!isset($config->enrol_imssourcedidfallback)) {
174 $config->enrol_imssourcedidfallback = '';
176 set_config('enrol_imssourcedidfallback', $config->enrol_imssourcedidfallback);
178 if (!isset($config->enrol_imscapitafix)) {
179 $config->enrol_imscapitafix = '';
181 set_config('enrol_imscapitafix', $config->enrol_imscapitafix);
183 //Antoni Mas. 07/12/2005. Incloem la opci de la foto dels usuaris
184 if (!isset($config->enrol_processphoto)) {
185 $config->enrol_processphoto = '';
187 set_config('enrol_processphoto', $config->enrol_processphoto);
189 if (!isset($config->enrol_imsrestricttarget)) {
190 $config->enrol_imsrestricttarget = '';
192 set_config('enrol_imsrestricttarget', $config->enrol_imsrestricttarget);
196 foreach($this->imsroles as $imsrolenum=>$imsrolename){
197 $configref = 'enrol_imse_imsrolemap' . $imsrolenum;
198 if (!isset($config->$configref)) {
199 echo "<p>Resetting config->$configref</p>";
200 $config->$configref = 0;
202 set_config('enrol_imse_imsrolemap' . $imsrolenum, $config->$configref);
206 set_config('enrol_ims_prev_md5', ''); // Forget the MD5 - to force re-processing if we change the config setting
207 set_config('enrol_ims_prev_time', ''); // Ditto
208 return true;
212 function get_access_icons($course){}
215 * Read in an IMS Enterprise file.
216 * Originally designed to handle v1.1 files but should be able to handle
217 * earlier types as well, I believe.
220 function cron() {
221 global $CFG;
223 if (empty($CFG->enrol_imsfilelocation)) {
224 // $filename = "$CFG->dirroot/enrol/imsenterprise/example.xml"; // Default location
225 $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location
226 } else {
227 $filename = $CFG->enrol_imsfilelocation;
230 $this->logfp = false; // File pointer for writing log data to
231 if(!empty($CFG->enrol_logtolocation)) {
232 $this->logfp = fopen($CFG->enrol_logtolocation, 'a');
237 if ( file_exists($filename) ) {
238 @set_time_limit(0);
239 $starttime = time();
241 $this->log_line('----------------------------------------------------------------------');
242 $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
243 $this->log_line('Found file '.$filename);
244 $this->xmlcache = '';
246 // Make sure we understand how to map the IMS-E roles to Moodle roles
247 $this->load_role_mappings();
249 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
250 $filemtime = filemtime($filename);
252 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
253 // This is so we avoid wasting the server's efforts processing a file unnecessarily
254 if(empty($CFG->enrol_ims_prev_path) || ($filename != $CFG->enrol_ims_prev_path)){
255 $fileisnew = true;
256 }elseif(isset($CFG->enrol_ims_prev_time) && ($filemtime <= $CFG->enrol_ims_prev_time)){
257 $fileisnew = false;
258 $this->log_line('File modification time is not more recent than last update - skipping processing.');
259 }elseif(isset($CFG->enrol_ims_prev_md5) && ($md5 == $CFG->enrol_ims_prev_md5)){
260 $fileisnew = false;
261 $this->log_line('File MD5 hash is same as on last update - skipping processing.');
262 }else{
263 $fileisnew = true; // Let's process it!
266 if($fileisnew){
268 $listoftags = array('group', 'person', 'member', 'membership', 'comments', 'properties'); // The list of tags which should trigger action (even if only cache trimming)
269 $this->continueprocessing = true; // The <properties> tag is allowed to halt processing if we're demanding a matching target
271 // FIRST PASS: Run through the file and process the group/person entries
272 if (($fh = fopen($filename, "r")) != false) {
274 $line = 0;
275 while ((!feof($fh)) && $this->continueprocessing) {
277 $line++;
278 $curline = fgets($fh);
279 $this->xmlcache .= $curline; // Add a line onto the XML cache
281 while(true){
282 // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
283 // Must always make sure to remove tags from cache so they don't clog up our memory
284 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
285 $this->process_group_tag($tagcontents);
286 $this->remove_tag_from_cache('group');
287 }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
288 $this->process_person_tag($tagcontents);
289 $this->remove_tag_from_cache('person');
290 }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
291 $this->process_membership_tag($tagcontents);
292 $this->remove_tag_from_cache('membership');
293 }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
294 $this->remove_tag_from_cache('comments');
295 }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
296 $this->process_properties_tag($tagcontents);
297 $this->remove_tag_from_cache('properties');
298 }else{
299 break;
301 } // End of while-tags-are-detected
302 } // end of while loop
303 fclose($fh);
304 fix_course_sortorder();
305 } // end of if(file_open) for first pass
310 SECOND PASS REMOVED
311 Since the IMS specification v1.1 insists that "memberships" should come last,
312 and since vendors seem to have done this anyway (even with 1.0),
313 we can sensibly perform the import in one fell swoop.
316 // SECOND PASS: Now go through the file and process the membership entries
317 $this->xmlcache = '';
318 if (($fh = fopen($filename, "r")) != false) {
319 $line = 0;
320 while ((!feof($fh)) && $this->continueprocessing) {
321 $line++;
322 $curline = fgets($fh);
323 $this->xmlcache .= $curline; // Add a line onto the XML cache
325 while(true){
326 // Must always make sure to remove tags from cache so they don't clog up our memory
327 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
328 $this->remove_tag_from_cache('group');
329 }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
330 $this->remove_tag_from_cache('person');
331 }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
332 $this->process_membership_tag($tagcontents);
333 $this->remove_tag_from_cache('membership');
334 }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
335 $this->remove_tag_from_cache('comments');
336 }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
337 $this->remove_tag_from_cache('properties');
338 }else{
339 break;
342 } // end of while loop
343 fclose($fh);
344 } // end of if(file_open) for second pass
349 $timeelapsed = time() - $starttime;
350 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
353 } // END of "if file is new"
356 // These variables are stored so we can compare them against the IMS file, next time round.
357 set_config('enrol_ims_prev_time', $filemtime);
358 set_config('enrol_ims_prev_md5', $md5);
359 set_config('enrol_ims_prev_path', $filename);
363 }else{ // end of if(file_exists)
364 $this->log_line('File not found: '.$filename);
367 if (!empty($CFG->enrol_mailadmins)) {
368 $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
369 if(!empty($CFG->enrol_logtolocation)){
370 if($this->logfp){
371 $msg .= "Log data has been written to:\n";
372 $msg .= "$CFG->enrol_logtolocation\n";
373 $msg .= "(Log file size: ".ceil(filesize($CFG->enrol_logtolocation)/1024)."Kb)\n\n";
374 }else{
375 $msg .= "The log file appears not to have been successfully written.\nCheck that the file is writeable by the server:\n";
376 $msg .= "$CFG->enrol_logtolocation\n\n";
378 }else{
379 $msg .= "Logging is currently not active.";
382 email_to_user(get_admin(), get_admin(), "Moodle IMS Enterprise enrolment notification", $msg);
383 $this->log_line('Notification email sent to administrator.');
387 if($this->logfp){
388 fclose($this->logfp);
392 } // end of cron() function
395 * Check if a complete tag is found in the cached data, which usually happens
396 * when the end of the tag has only just been loaded into the cache.
397 * Returns either false, or the contents of the tag (including start and end).
398 * @param string $tagname Name of tag to look for
399 * @param string $latestline The very last line in the cache (used for speeding up the match)
401 function full_tag_found_in_cache($tagname, $latestline){ // Return entire element if found. Otherwise return false.
402 if(strpos(strtolower($latestline), '</'.strtolower($tagname).'>')===false){
403 return false;
404 }elseif(preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)){
405 return $matches[1];
406 }else return false;
410 * Remove complete tag from the cached data (including all its contents) - so
411 * that the cache doesn't grow to unmanageable size
412 * @param string $tagname Name of tag to look for
414 function remove_tag_from_cache($tagname){ // Trim the cache so we're not in danger of running out of memory.
415 ///echo "<p>remove_tag_from_cache: $tagname</p>"; flush(); ob_flush();
416 // echo "<p>remove_tag_from_cache:<br />".htmlspecialchars($this->xmlcache);
417 $this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1)); // "1" so that we replace only the FIRST instance
418 // echo "<br />".htmlspecialchars($this->xmlcache)."</p>";
422 * Very simple convenience function to return the "recstatus" found in person/group/role tags.
423 * 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
424 * @param string $tagdata the tag XML data
425 * @param string $tagname the name of the tag we're interested in
427 function get_recstatus($tagdata, $tagname){
428 if(preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)){
429 // echo "<p>get_recstatus($tagname) found status of $matches[1]</p>";
430 return intval($matches[1]);
431 }else{
432 // echo "<p>get_recstatus($tagname) found nothing</p>";
433 return 0; // Unspecified
438 * Process the group tag. This defines a Moodle course.
439 * @param string $tagconents The raw contents of the XML element
441 function process_group_tag($tagcontents){
442 global $CFG;
444 // Process tag contents
445 unset($group);
446 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
447 $group->coursecode = trim($matches[1]);
449 if(preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)){
450 $group->description = trim($matches[1]);
452 if(preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)){
453 $group->category = trim($matches[1]);
456 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
457 //echo "<p>get_recstatus for this group returned $recstatus</p>";
459 if(!(strlen($group->coursecode)>0)){
460 $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.');
461 }else{
462 // First, truncate the course code if desired
463 if(intval($CFG->enrol_truncatecoursecodes)>0){
464 $group->coursecode = ($CFG->enrol_truncatecoursecodes > 0)
465 ? substr($group->coursecode, 0, intval($CFG->enrol_truncatecoursecodes))
466 : $group->coursecode;
469 /* -----------Course aliasing is DEACTIVATED until a more general method is in place---------------
471 // Second, look in the course alias table to see if the code should be translated to something else
472 if($aliases = get_field('enrol_coursealias', 'toids', 'fromid', $group->coursecode)){
473 $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases");
474 // Alias is allowed to be a comma-separated list, so let's split it
475 $group->coursecode = explode(',', $aliases);
479 // For compatibility with the (currently inactive) course aliasing, we need this to be an array
480 $group->coursecode = array($group->coursecode);
482 // Third, check if the course(s) exist
483 foreach($group->coursecode as $coursecode){
484 $coursecode = trim($coursecode);
485 if(!get_field('course', 'id', 'idnumber', $coursecode)) {
486 if(!$CFG->enrol_createnewcourses) {
487 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
488 } else {
489 // Create the (hidden) course(s) if not found
490 $course = new object();
491 $course->fullname = $group->description;
492 $course->shortname = $coursecode;
493 $course->idnumber = $coursecode;
494 $course->format = 'topics';
495 $course->visible = 0;
496 // Insert default names for teachers/students, from the current language
497 $site = get_site();
498 if (current_language() == $CFG->lang) {
499 $course->teacher = $site->teacher;
500 $course->teachers = $site->teachers;
501 $course->student = $site->student;
502 $course->students = $site->students;
503 } else {
504 $course->teacher = get_string("defaultcourseteacher");
505 $course->teachers = get_string("defaultcourseteachers");
506 $course->student = get_string("defaultcoursestudent");
507 $course->students = get_string("defaultcoursestudents");
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 = get_field('course_categories', 'id', 'name', addslashes($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 = 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);
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;
530 }else{
531 $course->category = 1;
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 $sortinfo = get_record_sql('SELECT MIN(sortorder) AS min,
538 MAX(sortorder) AS max
539 FROM ' . $CFG->prefix . 'course WHERE category<>0');
540 if (is_object($sortinfo)) { // no courses?
541 $max = $sortinfo->max;
542 $min = $sortinfo->min;
543 unset($sortinfo);
544 $course->sortorder = $min - 1;
545 }else{
546 $course->sortorder = 1000;
548 if($course->id = insert_record('course', addslashes_object($course))){
550 // Setup the blocks
551 $page = page_create_object(PAGE_COURSE_VIEW, $course->id);
552 blocks_repopulate_page($page); // Return value not checked because you can always edit later
554 $section = new object();
555 $section->course = $course->id; // Create a default section.
556 $section->section = 0;
557 $section->id = insert_record("course_sections", $section);
559 add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
561 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
562 }else{
563 $this->log_line('Failed to create course '.$coursecode.' in Moodle');
566 }elseif($recstatus==3 && ($courseid = get_field('course', 'id', 'idnumber', $coursecode))){
567 // If course does exist, but recstatus==3 (delete), then set the course as hidden
568 set_field('course', 'visible', '0', 'id', $courseid);
570 } // End of foreach(coursecode)
572 } // End process_group_tag()
575 * Process the person tag. This defines a Moodle user.
576 * @param string $tagconents The raw contents of the XML element
578 function process_person_tag($tagcontents){
579 global $CFG;
581 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
582 $person->idnumber = trim($matches[1]);
584 if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){
585 $person->firstname = trim($matches[1]);
587 if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){
588 $person->lastname = trim($matches[1]);
590 if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){
591 $person->username = trim($matches[1]);
593 if($CFG->enrol_imssourcedidfallback && trim($person->username)==''){
594 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
595 // NB We don't use an "elseif" because the tag may be supplied-but-empty
596 $person->username = $person->idnumber;
598 if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){
599 $person->email = trim($matches[1]);
601 if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){
602 $person->url = trim($matches[1]);
604 if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){
605 $person->city = trim($matches[1]);
607 if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){
608 $person->country = trim($matches[1]);
611 // Fix case of some of the fields if required
612 if($CFG->enrol_fixcaseusernames && isset($person->username)){
613 $person->username = strtolower($person->username);
615 if($CFG->enrol_fixcasepersonalnames){
616 if(isset($person->firstname)){
617 $person->firstname = ucwords(strtolower($person->firstname));
619 if(isset($person->lastname)){
620 $person->lastname = ucwords(strtolower($person->lastname));
624 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
627 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on
628 // In the "users" table we can do this by setting deleted=1
629 if($recstatus==3){
631 if($CFG->enrol_imsdeleteusers){ // If we're allowed to delete user records
632 // Make sure their "deleted" field is set to one
633 set_field('user', 'deleted', 1, 'username', $person->username);
634 $this->log_line("Marked user record for user '$person->username' (ID number $person->idnumber) as deleted.");
635 }else{
636 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
639 }else{ // Add or update record
642 // If the user exists (matching sourcedid) then we don't need to do anything.
643 if(!get_field('user', 'id', 'idnumber', $person->idnumber) && $CFG->enrol_createnewusers){
644 // If they don't exist and haven't a defined username, we log this as a potential problem.
645 if((!isset($person->username)) || (strlen($person->username)==0)){
646 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person.");
647 }elseif(get_field('user', 'id', 'username', $person->username)){
648 // If their idnumber is not registered but their user ID is, then add their idnumber to their record
649 set_field('user', 'idnumber', addslashes($person->idnumber), 'username', $person->username);
650 }else{
652 // If they don't exist and they have a defined username, and $CFG->enrol_createnewusers == true, we create them.
653 $person->lang = 'manual'; //TODO: this needs more work due tu multiauth changes
654 $person->auth = $CFG->auth;
655 $person->confirmed = 1;
656 $person->timemodified = time();
657 $person->mnethostid = $CFG->mnet_localhost_id;
658 if($id = insert_record('user', addslashes_object($person))){
660 Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib.
662 //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load
663 // it into the user's profile. Beware that this may cause a heavy overhead on the server.
664 if($CFG->enrol_processphoto){
665 if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){
666 $person->urlphoto = trim($matches[1]);
668 //Habilitam el flag que ens indica que el personatge t foto prpia.
669 $person->picture = 1;
670 //Llibreria creada per nosaltres mateixos.
671 require_once($CFG->dirroot.'/lib/gdlib.php');
672 if ($usernew->picture = save_profile_image($id, $person->urlphoto,'user')) {
673 set_field('user', 'picture', $usernew->picture, 'id', $id); /// Note picture in DB
677 $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber).");
678 }else{
679 $this->log_line("Database error while trying to create user record for user '$person->username' (ID number $person->idnumber).");
682 }elseif($CFG->enrol_createnewusers){
683 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
685 // Make sure their "deleted" field is set to zero.
686 set_field('user', 'deleted', 0, 'idnumber', $person->idnumber);
687 }else{
688 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
691 } // End of are-we-deleting-or-adding
693 } // End process_person_tag()
696 * Process the membership tag. This defines whether the specified Moodle users
697 * should be added/removed as teachers/students.
698 * @param string $tagconents The raw contents of the XML element
700 function process_membership_tag($tagcontents){
701 global $CFG;
702 $memberstally = 0;
703 $membersuntally = 0;
705 // In order to reduce the number of db queries required, group name/id associations are cached in this array:
706 $groupids = array();
708 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
709 $ship->coursecode = ($CFG->enrol_truncatecoursecodes > 0)
710 ? substr(trim($matches[1]), 0, intval($CFG->enrol_truncatecoursecodes))
711 : trim($matches[1]);
712 $ship->courseid = get_field('course', 'id', 'idnumber', $ship->coursecode);
714 if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){
715 foreach($membermatches as $mmatch){
716 unset($member);
717 unset($memberstoreobj);
718 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){
719 $member->idnumber = trim($matches[1]);
721 if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){
722 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
723 }elseif($CFG->enrol_imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){
724 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification!
725 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
727 if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){
728 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol
731 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
732 if($recstatus==3){
733 $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0
734 //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>";
737 $timeframe->begin = 0;
738 $timeframe->end = 0;
739 if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){
740 $timeframe = $this->decode_timeframe($matches[1]);
742 if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){
743 $member->groupname = trim($matches[1]);
744 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause
747 $rolecontext = get_context_instance(CONTEXT_COURSE, $ship->courseid);
748 $rolecontext = $rolecontext->id; // All we really want is the ID
749 //$this->log_line("Context instance for course $ship->courseid is...");
750 //print_r($rolecontext);
752 // Add or remove this student or teacher to the course...
753 $memberstoreobj->userid = get_field('user', 'id', 'idnumber', $member->idnumber);
754 $memberstoreobj->enrol = 'imsenterprise';
755 $memberstoreobj->course = $ship->courseid;
756 $memberstoreobj->time = time();
757 $memberstoreobj->timemodified = time();
758 if($memberstoreobj->userid){
760 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
761 // Zero means this roletype is supposed to be skipped.
762 $moodleroleid = $this->rolemappings[$member->roletype];
763 if(!$moodleroleid){
764 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course");
765 continue;
768 if(intval($member->status) == 1){
770 // Enrol unsing the generic role_assign() function
772 if ((!role_assign($moodleroleid, $memberstoreobj->userid, 0, $rolecontext, $timeframe->begin, $timeframe->end, 0, 'imsenterprise')) && (trim($memberstoreobj->userid)!='')) {
773 $this->log_line("Error enrolling user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
774 }else{
775 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
776 $memberstally++;
778 // At this point we can also ensure the group membership is recorded if present
779 if(isset($member->groupname)){
780 // Create the group if it doesn't exist - either way, make sure we know the group ID
781 if(isset($groupids[$member->groupname])){
782 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available
783 }else{
784 if($groupid = get_field('groups', 'id', 'name', addslashes($member->groupname), 'courseid', $ship->courseid)){
785 $member->groupid = $groupid;
786 $groupids[$member->groupname] = $groupid; // Store ID in cache
787 }else{
788 // Attempt to create the group
789 $group->name = addslashes($member->groupname);
790 $group->courseid = $ship->courseid;
791 $group->timecreated = time();
792 $group->timemodified = time();
793 $groupid = insert_record('groups', $group);
794 $this->log_line('Added a new group for this course: '.$group->name);
795 $groupids[$member->groupname] = $groupid; // Store ID in cache
796 $member->groupid = $groupid;
799 // Add the user-to-group association if it doesn't already exist
800 if($member->groupid) {
801 groups_add_member($member->groupid, $memberstoreobj->userid);
803 } // End of group-enrolment (from member.role.extension.cohort tag)
806 }elseif($CFG->enrol_imsunenrol){
807 // Unenrol
809 if (! role_unassign($moodleroleid, $memberstoreobj->userid, 0, $rolecontext)) {
810 $this->log_line("Error unenrolling $memberstoreobj->userid from role $moodleroleid in course");
811 }else{
812 $membersuntally++;
813 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
819 $this->log_line("Added $memberstally users to course $ship->coursecode");
820 if($membersuntally > 0){
821 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
824 } // End process_membership_tag()
827 * Process the properties tag. The only data from this element
828 * that is relevant is whether a <target> is specified.
829 * @param string $tagconents The raw contents of the XML element
831 function process_properties_tag($tagcontents){
832 global $CFG;
834 if($CFG->enrol_imsrestricttarget){
835 if(!(preg_match('{<target>'.preg_quote($CFG->enrol_imsrestricttarget).'</target>}is', $tagcontents, $matches))){
836 $this->log_line("Skipping processing: required target \"$CFG->enrol_imsrestricttarget\" not specified in this data.");
837 $this->continueprocessing = false;
843 * Store logging information. This does two things: uses the {@link mtrace()}
844 * function to print info to screen/STDOUT, and also writes log to a text file
845 * if a path has been specified.
846 * @param string $string Text to write (newline will be added automatically)
848 function log_line($string){
849 mtrace($string);
850 if($this->logfp) {
851 fwrite($this->logfp, $string . "\n");
856 * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
858 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
859 $ret->begin = $ret->end = 0;
860 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
861 // because otherwise the time markers should be ignored (participation should be
862 // allowed outside the period)
863 if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){
864 $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
866 if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){
867 $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
869 return $ret;
870 } // End decode_timeframe
873 * Load the role mappings (from the config), so we can easily refer to
874 * how an IMS-E role corresponds to a Moodle role
876 function load_role_mappings() {
877 $this->rolemappings = array();
878 foreach($this->imsroles as $imsrolenum=>$imsrolename) {
879 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename]
880 = get_field('config', 'value', 'name', 'enrol_imse_imsrolemap' . $imsrolenum);
884 } // end of class