Automatic installer.php lang files by installer_builder (20070726)
[moodle-linuxchix.git] / enrol / imsenterprise / enrol.php
blobebc4530f28ec404f2f3a428144505861a80927c0
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");
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.
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.
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',
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.
70 function determine_default_rolemapping($imscode) {
71 switch($imscode) {
72 case '01':
73 case '04':
74 $shortname = 'student';
75 break;
76 case '06':
77 case '08':
78 $shortname = 'teacher';
79 break;
80 case '02':
81 case '03':
82 $shortname = 'editingteacher';
83 break;
84 case '05':
85 case '07':
86 $shortname = 'admin';
87 break;
88 default:
89 return 0; // Zero for no match
91 return get_field('role', 'id', 'shortname', $shortname);
96 /// Override the base config_form() function
97 function config_form($frm) {
98 global $CFG, $imsroles;
100 $vars = array('enrol_imsfilelocation', 'enrol_createnewusers', 'enrol_fixcaseusernames', 'enrol_fixcasepersonalnames', 'enrol_truncatecoursecodes',
101 'enrol_createnewcourses', 'enrol_createnewcategories', 'enrol_createnewusers', 'enrol_mailadmins',
102 'enrol_imsunenrol', 'enrol_imssourcedidfallback', 'enrol_imscapitafix', 'enrol_imsrestricttarget', 'enrol_imsdeleteusers',
103 'enrol_imse_imsrolemap01','enrol_imse_imsrolemap02','enrol_imse_imsrolemap03','enrol_imse_imsrolemap04',
104 'enrol_imse_imsrolemap05','enrol_imse_imsrolemap06','enrol_imse_imsrolemap07','enrol_imse_imsrolemap08');
105 foreach ($vars as $var) {
106 if (!isset($frm->$var)) {
107 $frm->$var = '';
110 include ("$CFG->dirroot/enrol/imsenterprise/config.html");
114 /// Override the base process_config() function
115 function process_config($config) {
117 if (!isset($config->enrol_imsfilelocation)) {
118 $config->enrol_imsfilelocation = '';
120 set_config('enrol_imsfilelocation', $config->enrol_imsfilelocation);
122 if (!isset($config->enrol_logtolocation)) {
123 $config->enrol_logtolocation = '';
125 set_config('enrol_logtolocation', $config->enrol_logtolocation);
127 if (!isset($config->enrol_fixcaseusernames)) {
128 $config->enrol_fixcaseusernames = '';
130 set_config('enrol_fixcaseusernames', $config->enrol_fixcaseusernames);
132 if (!isset($config->enrol_fixcasepersonalnames)) {
133 $config->enrol_fixcasepersonalnames = '';
135 set_config('enrol_fixcasepersonalnames', $config->enrol_fixcasepersonalnames);
137 if (!isset($config->enrol_truncatecoursecodes)) {
138 $config->enrol_truncatecoursecodes = 0;
140 set_config('enrol_truncatecoursecodes', intval($config->enrol_truncatecoursecodes));
142 if (!isset($config->enrol_createnewcourses)) {
143 $config->enrol_createnewcourses = '';
145 set_config('enrol_createnewcourses', $config->enrol_createnewcourses);
147 if (!isset($config->enrol_createnewcategories)) {
148 $config->enrol_createnewcategories = '';
150 set_config('enrol_createnewcategories', $config->enrol_createnewcategories);
152 if (!isset($config->enrol_createnewusers)) {
153 $config->enrol_createnewusers = '';
155 set_config('enrol_createnewusers', $config->enrol_createnewusers);
157 if (!isset($config->enrol_imsdeleteusers)) {
158 $config->enrol_imsdeleteusers = '';
160 set_config('enrol_imsdeleteusers', $config->enrol_imsdeleteusers);
162 if (!isset($config->enrol_mailadmins)) {
163 $config->enrol_mailadmins = '';
165 set_config('enrol_mailadmins', $config->enrol_mailadmins);
167 if (!isset($config->enrol_imsunenrol)) {
168 $config->enrol_imsunenrol = '';
170 set_config('enrol_imsunenrol', $config->enrol_imsunenrol);
172 if (!isset($config->enrol_imssourcedidfallback)) {
173 $config->enrol_imssourcedidfallback = '';
175 set_config('enrol_imssourcedidfallback', $config->enrol_imssourcedidfallback);
177 if (!isset($config->enrol_imscapitafix)) {
178 $config->enrol_imscapitafix = '';
180 set_config('enrol_imscapitafix', $config->enrol_imscapitafix);
182 //Antoni Mas. 07/12/2005. Incloem la opci de la foto dels usuaris
183 if (!isset($config->enrol_processphoto)) {
184 $config->enrol_processphoto = '';
186 set_config('enrol_processphoto', $config->enrol_processphoto);
188 if (!isset($config->enrol_imsrestricttarget)) {
189 $config->enrol_imsrestricttarget = '';
191 set_config('enrol_imsrestricttarget', $config->enrol_imsrestricttarget);
195 foreach($this->imsroles as $imsrolenum=>$imsrolename){
196 $configref = 'enrol_imse_imsrolemap' . $imsrolenum;
197 if (!isset($config->$configref)) {
198 echo "<p>Resetting config->$configref</p>";
199 $config->$configref = 0;
201 set_config('enrol_imse_imsrolemap' . $imsrolenum, $config->$configref);
205 set_config('enrol_ims_prev_md5', ''); // Forget the MD5 - to force re-processing if we change the config setting
206 set_config('enrol_ims_prev_time', ''); // Ditto
207 return true;
211 function get_access_icons($course){}
214 * Read in an IMS Enterprise file.
215 * Originally designed to handle v1.1 files but should be able to handle
216 * earlier types as well, I believe.
219 function cron() {
220 global $CFG;
222 if (empty($CFG->enrol_imsfilelocation)) {
223 // $filename = "$CFG->dirroot/enrol/imsenterprise/example.xml"; // Default location
224 $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location
225 } else {
226 $filename = $CFG->enrol_imsfilelocation;
229 $this->logfp = false; // File pointer for writing log data to
230 if(!empty($CFG->enrol_logtolocation)) {
231 $this->logfp = fopen($CFG->enrol_logtolocation, 'a');
236 if ( file_exists($filename) ) {
237 @set_time_limit(0);
238 $starttime = time();
240 $this->log_line('----------------------------------------------------------------------');
241 $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
242 $this->log_line('Found file '.$filename);
243 $this->xmlcache = '';
245 // Make sure we understand how to map the IMS-E roles to Moodle roles
246 $this->load_role_mappings();
248 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
249 $filemtime = filemtime($filename);
251 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
252 // This is so we avoid wasting the server's efforts processing a file unnecessarily
253 if(empty($CFG->enrol_ims_prev_path) || ($filename != $CFG->enrol_ims_prev_path)){
254 $fileisnew = true;
255 }elseif(isset($CFG->enrol_ims_prev_time) && ($filemtime <= $CFG->enrol_ims_prev_time)){
256 $fileisnew = false;
257 $this->log_line('File modification time is not more recent than last update - skipping processing.');
258 }elseif(isset($CFG->enrol_ims_prev_md5) && ($md5 == $CFG->enrol_ims_prev_md5)){
259 $fileisnew = false;
260 $this->log_line('File MD5 hash is same as on last update - skipping processing.');
261 }else{
262 $fileisnew = true; // Let's process it!
265 if($fileisnew){
267 $listoftags = array('group', 'person', 'member', 'membership', 'comments', 'properties'); // The list of tags which should trigger action (even if only cache trimming)
268 $this->continueprocessing = true; // The <properties> tag is allowed to halt processing if we're demanding a matching target
270 // FIRST PASS: Run through the file and process the group/person entries
271 if (($fh = fopen($filename, "r")) != false) {
273 $line = 0;
274 while ((!feof($fh)) && $this->continueprocessing) {
276 $line++;
277 $curline = fgets($fh);
278 $this->xmlcache .= $curline; // Add a line onto the XML cache
280 while(true){
281 // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
282 // Must always make sure to remove tags from cache so they don't clog up our memory
283 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
284 $this->process_group_tag($tagcontents);
285 $this->remove_tag_from_cache('group');
286 }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
287 $this->process_person_tag($tagcontents);
288 $this->remove_tag_from_cache('person');
289 }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
290 $this->process_membership_tag($tagcontents);
291 $this->remove_tag_from_cache('membership');
292 }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
293 $this->remove_tag_from_cache('comments');
294 }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
295 $this->process_properties_tag($tagcontents);
296 $this->remove_tag_from_cache('properties');
297 }else{
298 break;
300 } // End of while-tags-are-detected
301 } // end of while loop
302 fclose($fh);
303 } // end of if(file_open) for first pass
308 SECOND PASS REMOVED
309 Since the IMS specification v1.1 insists that "memberships" should come last,
310 and since vendors seem to have done this anyway (even with 1.0),
311 we can sensibly perform the import in one fell swoop.
314 // SECOND PASS: Now go through the file and process the membership entries
315 $this->xmlcache = '';
316 if (($fh = fopen($filename, "r")) != false) {
317 $line = 0;
318 while ((!feof($fh)) && $this->continueprocessing) {
319 $line++;
320 $curline = fgets($fh);
321 $this->xmlcache .= $curline; // Add a line onto the XML cache
323 while(true){
324 // Must always make sure to remove tags from cache so they don't clog up our memory
325 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
326 $this->remove_tag_from_cache('group');
327 }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
328 $this->remove_tag_from_cache('person');
329 }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
330 $this->process_membership_tag($tagcontents);
331 $this->remove_tag_from_cache('membership');
332 }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
333 $this->remove_tag_from_cache('comments');
334 }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
335 $this->remove_tag_from_cache('properties');
336 }else{
337 break;
340 } // end of while loop
341 fclose($fh);
342 } // end of if(file_open) for second pass
347 $timeelapsed = time() - $starttime;
348 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
351 } // END of "if file is new"
354 // These variables are stored so we can compare them against the IMS file, next time round.
355 set_config('enrol_ims_prev_time', $filemtime);
356 set_config('enrol_ims_prev_md5', $md5);
357 set_config('enrol_ims_prev_path', $filename);
361 }else{ // end of if(file_exists)
362 $this->log_line('File not found: '.$filename);
365 if (!empty($CFG->enrol_mailadmins)) {
366 $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
367 if(!empty($CFG->enrol_logtolocation)){
368 if($this->logfp){
369 $msg .= "Log data has been written to:\n";
370 $msg .= "$CFG->enrol_logtolocation\n";
371 $msg .= "(Log file size: ".ceil(filesize($CFG->enrol_logtolocation)/1024)."Kb)\n\n";
372 }else{
373 $msg .= "The log file appears not to have been successfully written.\nCheck that the file is writeable by the server:\n";
374 $msg .= "$CFG->enrol_logtolocation\n\n";
376 }else{
377 $msg .= "Logging is currently not active.";
380 email_to_user(get_admin(), get_admin(), "Moodle IMS Enterprise enrolment notification", $msg);
381 $this->log_line('Notification email sent to administrator.');
385 if($this->logfp){
386 fclose($this->logfp);
390 } // end of cron() function
393 * Check if a complete tag is found in the cached data, which usually happens
394 * when the end of the tag has only just been loaded into the cache.
395 * Returns either false, or the contents of the tag (including start and end).
396 * @param string $tagname Name of tag to look for
397 * @param string $latestline The very last line in the cache (used for speeding up the match)
399 function full_tag_found_in_cache($tagname, $latestline){ // Return entire element if found. Otherwise return false.
400 if(strpos(strtolower($latestline), '</'.strtolower($tagname).'>')===false){
401 return false;
402 }elseif(preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)){
403 return $matches[1];
404 }else return false;
408 * Remove complete tag from the cached data (including all its contents) - so
409 * that the cache doesn't grow to unmanageable size
410 * @param string $tagname Name of tag to look for
412 function remove_tag_from_cache($tagname){ // Trim the cache so we're not in danger of running out of memory.
413 ///echo "<p>remove_tag_from_cache: $tagname</p>"; flush(); ob_flush();
414 // echo "<p>remove_tag_from_cache:<br />".htmlspecialchars($this->xmlcache);
415 $this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1)); // "1" so that we replace only the FIRST instance
416 // echo "<br />".htmlspecialchars($this->xmlcache)."</p>";
420 * Very simple convenience function to return the "recstatus" found in person/group/role tags.
421 * 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
422 * @param string $tagdata the tag XML data
423 * @param string $tagname the name of the tag we're interested in
425 function get_recstatus($tagdata, $tagname){
426 if(preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)){
427 // echo "<p>get_recstatus($tagname) found status of $matches[1]</p>";
428 return intval($matches[1]);
429 }else{
430 // echo "<p>get_recstatus($tagname) found nothing</p>";
431 return 0; // Unspecified
436 * Process the group tag. This defines a Moodle course.
437 * @param string $tagconents The raw contents of the XML element
439 function process_group_tag($tagcontents){
440 global $CFG;
442 // Process tag contents
443 unset($group);
444 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
445 $group->coursecode = trim($matches[1]);
447 if(preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)){
448 $group->description = trim($matches[1]);
450 if(preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)){
451 $group->category = trim($matches[1]);
454 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
455 //echo "<p>get_recstatus for this group returned $recstatus</p>";
457 if(!(strlen($group->coursecode)>0)){
458 $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.');
459 }else{
460 // First, truncate the course code if desired
461 if(intval($CFG->enrol_truncatecoursecodes)>0){
462 $group->coursecode = ($CFG->enrol_truncatecoursecodes > 0)
463 ? substr($group->coursecode, 0, intval($CFG->enrol_truncatecoursecodes))
464 : $group->coursecode;
467 /* -----------Course aliasing is DEACTIVATED until a more general method is in place---------------
469 // Second, look in the course alias table to see if the code should be translated to something else
470 if($aliases = get_field('enrol_coursealias', 'toids', 'fromid', $group->coursecode)){
471 $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases");
472 // Alias is allowed to be a comma-separated list, so let's split it
473 $group->coursecode = explode(',', $aliases);
477 // For compatibility with the (currently inactive) course aliasing, we need this to be an array
478 $group->coursecode = array($group->coursecode);
480 // Third, check if the course(s) exist
481 foreach($group->coursecode as $coursecode){
482 $coursecode = trim($coursecode);
483 if(!get_field('course', 'id', 'idnumber', $coursecode)) {
484 if(!$CFG->enrol_createnewcourses) {
485 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
486 } else {
487 // Create the (hidden) course(s) if not found
488 $course->fullname = $group->description;
489 $course->shortname = $coursecode;
490 $course->idnumber = $coursecode;
491 $course->format = 'topics';
492 $course->visible = 0;
493 // Insert default names for teachers/students, from the current language
494 $site = get_site();
495 if (current_language() == $CFG->lang) {
496 $course->teacher = $site->teacher;
497 $course->teachers = $site->teachers;
498 $course->student = $site->student;
499 $course->students = $site->students;
500 } else {
501 $course->teacher = get_string("defaultcourseteacher");
502 $course->teachers = get_string("defaultcourseteachers");
503 $course->student = get_string("defaultcoursestudent");
504 $course->students = get_string("defaultcoursestudents");
507 // Handle course categorisation (taken from the group.org.orgunit field if present)
508 if(strlen($group->category)>0){
509 // If the category is defined and exists in Moodle, we want to store it in that one
510 if($catid = get_field('course_categories', 'id', 'name', addslashes($group->category))){
511 $course->category = $catid;
512 }elseif($CFG->enrol_createnewcategories){
513 // Else if we're allowed to create new categories, let's create this one
514 $newcat->name = $group->category;
515 $newcat->visible = 0;
516 if($catid = insert_record('course_categories', $newcat)){
517 $course->category = $catid;
518 $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
519 }else{
520 $this->log_line('Failed to create new category: '.$newcat->name);
522 }else{
523 // If not found and not allowed to create, stick with default
524 $this->log_line('Category '.$group->category.' not found in Moodle database, so using default category instead.');
525 $course->category = 1;
527 }else{
528 $course->category = 1;
530 $course->timecreated = time();
531 $course->startdate = time();
532 $course->numsections = 1;
533 // Choose a sort order that puts us at the start of the list!
534 $sortinfo = get_record_sql('SELECT MIN(sortorder) AS min,
535 MAX(sortorder) AS max
536 FROM ' . $CFG->prefix . 'course WHERE category<>0');
537 if (is_object($sortinfo)) { // no courses?
538 $max = $sortinfo->max;
539 $min = $sortinfo->min;
540 unset($sortinfo);
541 $course->sortorder = $min - 1;
542 }else{
543 $course->sortorder = 1000;
545 if($course->id = insert_record('course', $course)){
547 // Setup the blocks
548 $page = page_create_object(PAGE_COURSE_VIEW, $course->id);
549 blocks_repopulate_page($page); // Return value not checked because you can always edit later
551 $section = NULL;
552 $section->course = $course->id; // Create a default section.
553 $section->section = 0;
554 $section->id = insert_record("course_sections", $section);
556 fix_course_sortorder();
557 add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
559 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
560 }else{
561 $this->log_line('Failed to create course '.$coursecode.' in Moodle');
564 }elseif($recstatus==3 && ($courseid = get_field('course', 'id', 'idnumber', $coursecode))){
565 // If course does exist, but recstatus==3 (delete), then set the course as hidden
566 set_field('course', 'visible', '0', 'id', $courseid);
568 } // End of foreach(coursecode)
570 } // End process_group_tag()
573 * Process the person tag. This defines a Moodle user.
574 * @param string $tagconents The raw contents of the XML element
576 function process_person_tag($tagcontents){
577 global $CFG;
579 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
580 $person->idnumber = trim($matches[1]);
582 if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){
583 $person->firstname = trim($matches[1]);
585 if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){
586 $person->lastname = trim($matches[1]);
588 if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){
589 $person->username = trim($matches[1]);
591 if($CFG->enrol_imssourcedidfallback && trim($person->username)==''){
592 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
593 // NB We don't use an "elseif" because the tag may be supplied-but-empty
594 $person->username = $person->idnumber;
596 if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){
597 $person->email = trim($matches[1]);
599 if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){
600 $person->url = trim($matches[1]);
602 if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){
603 $person->city = trim($matches[1]);
605 if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){
606 $person->country = trim($matches[1]);
609 // Fix case of some of the fields if required
610 if($CFG->enrol_fixcaseusernames && isset($person->username)){
611 $person->username = strtolower($person->username);
613 if($CFG->enrol_fixcasepersonalnames){
614 if(isset($person->firstname)){
615 $person->firstname = ucwords(strtolower($person->firstname));
617 if(isset($person->lastname)){
618 $person->lastname = ucwords(strtolower($person->lastname));
622 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
625 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on
626 // In the "users" table we can do this by setting deleted=1
627 if($recstatus==3){
629 if($CFG->enrol_imsdeleteusers){ // If we're allowed to delete user records
630 // Make sure their "deleted" field is set to one
631 set_field('user', 'deleted', 1, 'username', $person->username);
632 $this->log_line("Marked user record for user '$person->username' (ID number $person->idnumber) as deleted.");
633 }else{
634 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
637 }else{ // Add or update record
640 // If the user exists (matching sourcedid) then we don't need to do anything.
641 if(!get_field('user', 'id', 'idnumber', $person->idnumber) && $CFG->enrol_createnewusers){
642 // If they don't exist and haven't a defined username, we log this as a potential problem.
643 if((!isset($person->username)) || (strlen($person->username)==0)){
644 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person.");
645 }elseif(get_field('user', 'id', 'username', $person->username)){
646 // If their idnumber is not registered but their user ID is, then add their idnumber to their record
647 set_field('user', 'idnumber', addslashes($person->idnumber), 'username', $person->username);
648 }else{
650 // If they don't exist and they have a defined username, and $CFG->enrol_createnewusers == true, we create them.
651 $person->lang = 'manual'; //TODO: this needs more work due tu multiauth changes
652 $person->auth = $CFG->auth;
653 $person->confirmed = 1;
654 $person->timemodified = time();
655 if($id = insert_record('user', addslashes_object($person))){
657 Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib.
659 //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load
660 // it into the user's profile. Beware that this may cause a heavy overhead on the server.
661 if($CFG->enrol_processphoto){
662 if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){
663 $person->urlphoto = trim($matches[1]);
665 //Habilitam el flag que ens indica que el personatge t foto prpia.
666 $person->picture = 1;
667 //Llibreria creada per nosaltres mateixos.
668 require_once($CFG->dirroot.'/lib/gdlib.php');
669 if ($usernew->picture = save_profile_image($id, $person->urlphoto,'users', true)) {
670 set_field('user', 'picture', $usernew->picture, 'id', $id); /// Note picture in DB
674 $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber).");
675 }else{
676 $this->log_line("Database error while trying to create user record for user '$person->username' (ID number $person->idnumber).");
679 }elseif($CFG->enrol_createnewusers){
680 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
682 // Make sure their "deleted" field is set to zero.
683 set_field('user', 'deleted', 0, 'idnumber', $person->idnumber);
684 }else{
685 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
688 } // End of are-we-deleting-or-adding
690 } // End process_person_tag()
693 * Process the membership tag. This defines whether the specified Moodle users
694 * should be added/removed as teachers/students.
695 * @param string $tagconents The raw contents of the XML element
697 function process_membership_tag($tagcontents){
698 global $CFG;
699 $memberstally = 0;
700 $membersuntally = 0;
702 // In order to reduce the number of db queries required, group name/id associations are cached in this array:
703 $groupids = array();
705 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
706 $ship->coursecode = ($CFG->enrol_truncatecoursecodes > 0)
707 ? substr(trim($matches[1]), 0, intval($CFG->enrol_truncatecoursecodes))
708 : trim($matches[1]);
709 $ship->courseid = get_field('course', 'id', 'idnumber', $ship->coursecode);
711 if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){
712 foreach($membermatches as $mmatch){
713 unset($member);
714 unset($memberstoreobj);
715 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){
716 $member->idnumber = trim($matches[1]);
718 if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){
719 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
720 }elseif($CFG->enrol_imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){
721 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification!
722 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
724 if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){
725 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol
728 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
729 if($recstatus==3){
730 $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0
731 //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>";
734 $timeframe->begin = 0;
735 $timeframe->end = 0;
736 if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){
737 $timeframe = $this->decode_timeframe($matches[1]);
739 if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){
740 $member->groupname = trim($matches[1]);
741 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause
744 $rolecontext = get_context_instance(CONTEXT_COURSE, $ship->courseid);
745 $rolecontext = $rolecontext->id; // All we really want is the ID
746 //$this->log_line("Context instance for course $ship->courseid is...");
747 //print_r($rolecontext);
749 // Add or remove this student or teacher to the course...
750 $memberstoreobj->userid = get_field('user', 'id', 'idnumber', $member->idnumber);
751 $memberstoreobj->enrol = 'imsenterprise';
752 $memberstoreobj->course = $ship->courseid;
753 $memberstoreobj->time = time();
754 $memberstoreobj->timemodified = time();
755 if($memberstoreobj->userid){
757 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
758 // Zero means this roletype is supposed to be skipped.
759 $moodleroleid = $this->rolemappings[$member->roletype];
760 if(!$moodleroleid){
761 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course");
762 continue;
765 if(intval($member->status) == 1){
767 // Enrol unsing the generic role_assign() function
769 if ((!role_assign($moodleroleid, $memberstoreobj->userid, 0, $rolecontext, $timeframe->begin, $timeframe->end, 0, 'imsenterprise')) && (trim($memberstoreobj->userid)!='')) {
770 $this->log_line("Error enrolling user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
771 }else{
772 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
773 $memberstally++;
775 // At this point we can also ensure the group membership is recorded if present
776 if(isset($member->groupname)){
777 // Create the group if it doesn't exist - either way, make sure we know the group ID
778 if(isset($groupids[$member->groupname])){
779 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available
780 }else{
781 if($groupid = get_field('groups', 'id', 'name', addslashes($member->groupname), 'courseid', $ship->courseid)){
782 $member->groupid = $groupid;
783 $groupids[$member->groupname] = $groupid; // Store ID in cache
784 }else{
785 // Attempt to create the group
786 $group->name = addslashes($member->groupname);
787 $group->courseid = $ship->courseid;
788 $group->timecreated = time();
789 $group->timemodified = time();
790 $groupid = insert_record('groups', $group);
791 $this->log_line('Added a new group for this course: '.$group->name);
792 $groupids[$member->groupname] = $groupid; // Store ID in cache
793 $member->groupid = $groupid;
796 // Add the user-to-group association if it doesn't already exist
797 if($member->groupid) {
798 add_user_to_group ($member->groupid, $memberstoreobj->userid);
800 } // End of group-enrolment (from member.role.extension.cohort tag)
803 }elseif($CFG->enrol_imsunenrol){
804 // Unenrol
806 if (! role_unassign($moodleroleid, $memberstoreobj->userid, 0, $rolecontext)) {
807 $this->log_line("Error unenrolling $memberstoreobj->userid from role $moodleroleid in course");
808 }else{
809 $membersuntally++;
810 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
816 $this->log_line("Added $memberstally users to course $ship->coursecode");
817 if($membersuntally > 0){
818 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
821 } // End process_membership_tag()
824 * Process the properties tag. The only data from this element
825 * that is relevant is whether a <target> is specified.
826 * @param string $tagconents The raw contents of the XML element
828 function process_properties_tag($tagcontents){
829 global $CFG;
831 if($CFG->enrol_imsrestricttarget){
832 if(!(preg_match('{<target>'.preg_quote($CFG->enrol_imsrestricttarget).'</target>}is', $tagcontents, $matches))){
833 $this->log_line("Skipping processing: required target \"$CFG->enrol_imsrestricttarget\" not specified in this data.");
834 $this->continueprocessing = false;
840 * Store logging information. This does two things: uses the {@link mtrace()}
841 * function to print info to screen/STDOUT, and also writes log to a text file
842 * if a path has been specified.
843 * @param string $string Text to write (newline will be added automatically)
845 function log_line($string){
846 mtrace($string);
847 if($this->logfp) {
848 fwrite($this->logfp, $string . "\n");
853 * Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
855 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
856 $ret->begin = $ret->end = 0;
857 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
858 // because otherwise the time markers should be ignored (participation should be
859 // allowed outside the period)
860 if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){
861 $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
863 if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){
864 $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
866 return $ret;
867 } // End decode_timeframe
870 * Load the role mappings (from the config), so we can easily refer to
871 * how an IMS-E role corresponds to a Moodle role
873 function load_role_mappings() {
874 $this->rolemappings = array();
875 foreach($this->imsroles as $imsrolenum=>$imsrolename) {
876 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename]
877 = get_field('config', 'value', 'name', 'enrol_imse_imsrolemap' . $imsrolenum);
881 } // end of class