|
Moodle
2.2.1
http://www.collinsharper.com
|
00001 <?php 00002 // This file is part of Moodle - http://moodle.org/ 00003 // 00004 // Moodle is free software: you can redistribute it and/or modify 00005 // it under the terms of the GNU General Public License as published by 00006 // the Free Software Foundation, either version 3 of the License, or 00007 // (at your option) any later version. 00008 // 00009 // Moodle is distributed in the hope that it will be useful, 00010 // but WITHOUT ANY WARRANTY; without even the implied warranty of 00011 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00012 // GNU General Public License for more details. 00013 // 00014 // You should have received a copy of the GNU General Public License 00015 // along with Moodle. If not, see <http://www.gnu.org/licenses/>. 00016 00030 defined('MOODLE_INTERNAL') || die(); 00031 00032 /* 00033 00034 Note for programmers: 00035 00036 This class uses regular expressions to mine the data file. The main reason is 00037 that XML handling changes from PHP 4 to PHP 5, so this should work on both. 00038 00039 One drawback is that the pattern-matching doesn't (currently) handle XML 00040 namespaces - it only copes with a <group> tag if it says <group>, and not 00041 (for example) <ims:group>. 00042 00043 This should also be able to handle VERY LARGE FILES - so the entire IMS file is 00044 NOT loaded into memory at once. It's handled line-by-line, 'forgetting' tags as 00045 soon as they are processed. 00046 00047 N.B. The "sourcedid" ID code is translated to Moodle's "idnumber" field, both 00048 for users and for courses. 00049 00050 */ 00051 00052 require_once($CFG->dirroot.'/group/lib.php'); 00053 00054 00055 class enrol_imsenterprise_plugin extends enrol_plugin { 00056 00057 var $log; 00058 00059 00066 function cron() { 00067 global $CFG; 00068 00069 // Get configs 00070 $imsfilelocation = $this->get_config('imsfilelocation'); 00071 $logtolocation = $this->get_config('logtolocation'); 00072 $mailadmins = $this->get_config('mailadmins'); 00073 $prev_time = $this->get_config('prev_time'); 00074 $prev_md5 = $this->get_config('prev_md5'); 00075 $prev_path = $this->get_config('prev_path'); 00076 00077 if (empty($imsfilelocation)) { 00078 // $filename = "$CFG->dirroot/enrol/imsenterprise/example.xml"; // Default location 00079 $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location 00080 } else { 00081 $filename = $imsfilelocation; 00082 } 00083 00084 $this->logfp = false; // File pointer for writing log data to 00085 if(!empty($logtolocation)) { 00086 $this->logfp = fopen($logtolocation, 'a'); 00087 } 00088 00089 if ( file_exists($filename) ) { 00090 @set_time_limit(0); 00091 $starttime = time(); 00092 00093 $this->log_line('----------------------------------------------------------------------'); 00094 $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time())); 00095 $this->log_line('Found file '.$filename); 00096 $this->xmlcache = ''; 00097 00098 // Make sure we understand how to map the IMS-E roles to Moodle roles 00099 $this->load_role_mappings(); 00100 00101 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron 00102 $filemtime = filemtime($filename); 00103 00104 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash) 00105 // This is so we avoid wasting the server's efforts processing a file unnecessarily 00106 if(empty($prev_path) || ($filename != $prev_path)) { 00107 $fileisnew = true; 00108 } elseif(isset($prev_time) && ($filemtime <= $prev_time)) { 00109 $fileisnew = false; 00110 $this->log_line('File modification time is not more recent than last update - skipping processing.'); 00111 } elseif(isset($prev_md5) && ($md5 == $prev_md5)) { 00112 $fileisnew = false; 00113 $this->log_line('File MD5 hash is same as on last update - skipping processing.'); 00114 } else { 00115 $fileisnew = true; // Let's process it! 00116 } 00117 00118 if($fileisnew) { 00119 00120 $listoftags = array('group', 'person', 'member', 'membership', 'comments', 'properties'); // The list of tags which should trigger action (even if only cache trimming) 00121 $this->continueprocessing = true; // The <properties> tag is allowed to halt processing if we're demanding a matching target 00122 00123 // FIRST PASS: Run through the file and process the group/person entries 00124 if (($fh = fopen($filename, "r")) != false) { 00125 00126 $line = 0; 00127 while ((!feof($fh)) && $this->continueprocessing) { 00128 00129 $line++; 00130 $curline = fgets($fh); 00131 $this->xmlcache .= $curline; // Add a line onto the XML cache 00132 00133 while (true) { 00134 // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it. 00135 // Must always make sure to remove tags from cache so they don't clog up our memory 00136 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)) { 00137 $this->process_group_tag($tagcontents); 00138 $this->remove_tag_from_cache('group'); 00139 } elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)) { 00140 $this->process_person_tag($tagcontents); 00141 $this->remove_tag_from_cache('person'); 00142 } elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)) { 00143 $this->process_membership_tag($tagcontents); 00144 $this->remove_tag_from_cache('membership'); 00145 } elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)) { 00146 $this->remove_tag_from_cache('comments'); 00147 } elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)) { 00148 $this->process_properties_tag($tagcontents); 00149 $this->remove_tag_from_cache('properties'); 00150 } else { 00151 break; 00152 } 00153 } // End of while-tags-are-detected 00154 } // end of while loop 00155 fclose($fh); 00156 fix_course_sortorder(); 00157 } // end of if(file_open) for first pass 00158 00159 /* 00160 00161 00162 SECOND PASS REMOVED 00163 Since the IMS specification v1.1 insists that "memberships" should come last, 00164 and since vendors seem to have done this anyway (even with 1.0), 00165 we can sensibly perform the import in one fell swoop. 00166 00167 00168 // SECOND PASS: Now go through the file and process the membership entries 00169 $this->xmlcache = ''; 00170 if (($fh = fopen($filename, "r")) != false) { 00171 $line = 0; 00172 while ((!feof($fh)) && $this->continueprocessing) { 00173 $line++; 00174 $curline = fgets($fh); 00175 $this->xmlcache .= $curline; // Add a line onto the XML cache 00176 00177 while(true){ 00178 // Must always make sure to remove tags from cache so they don't clog up our memory 00179 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){ 00180 $this->remove_tag_from_cache('group'); 00181 }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){ 00182 $this->remove_tag_from_cache('person'); 00183 }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){ 00184 $this->process_membership_tag($tagcontents); 00185 $this->remove_tag_from_cache('membership'); 00186 }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){ 00187 $this->remove_tag_from_cache('comments'); 00188 }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){ 00189 $this->remove_tag_from_cache('properties'); 00190 }else{ 00191 break; 00192 } 00193 } 00194 } // end of while loop 00195 fclose($fh); 00196 } // end of if(file_open) for second pass 00197 00198 00199 */ 00200 00201 $timeelapsed = time() - $starttime; 00202 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.'); 00203 00204 00205 } // END of "if file is new" 00206 00207 00208 // These variables are stored so we can compare them against the IMS file, next time round. 00209 $this->set_config('prev_time', $filemtime); 00210 $this->set_config('prev_md5', $md5); 00211 $this->set_config('prev_path', $filename); 00212 00213 00214 00215 }else{ // end of if(file_exists) 00216 $this->log_line('File not found: '.$filename); 00217 } 00218 00219 if (!empty($mailadmins)) { 00220 $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n"; 00221 if(!empty($logtolocation)){ 00222 if($this->logfp){ 00223 $msg .= "Log data has been written to:\n"; 00224 $msg .= "$logtolocation\n"; 00225 $msg .= "(Log file size: ".ceil(filesize($logtolocation)/1024)."Kb)\n\n"; 00226 }else{ 00227 $msg .= "The log file appears not to have been successfully written.\nCheck that the file is writeable by the server:\n"; 00228 $msg .= "$logtolocation\n\n"; 00229 } 00230 }else{ 00231 $msg .= "Logging is currently not active."; 00232 } 00233 00234 $eventdata = new stdClass(); 00235 $eventdata->modulename = 'moodle'; 00236 $eventdata->component = 'imsenterprise'; 00237 $eventdata->name = 'imsenterprise_enrolment'; 00238 $eventdata->userfrom = get_admin(); 00239 $eventdata->userto = get_admin(); 00240 $eventdata->subject = "Moodle IMS Enterprise enrolment notification"; 00241 $eventdata->fullmessage = $msg; 00242 $eventdata->fullmessageformat = FORMAT_PLAIN; 00243 $eventdata->fullmessagehtml = ''; 00244 $eventdata->smallmessage = ''; 00245 message_send($eventdata); 00246 00247 $this->log_line('Notification email sent to administrator.'); 00248 00249 } 00250 00251 if($this->logfp){ 00252 fclose($this->logfp); 00253 } 00254 00255 00256 } // end of cron() function 00257 00265 function full_tag_found_in_cache($tagname, $latestline){ // Return entire element if found. Otherwise return false. 00266 if(strpos(strtolower($latestline), '</'.strtolower($tagname).'>')===false){ 00267 return false; 00268 }elseif(preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)){ 00269 return $matches[1]; 00270 }else return false; 00271 } 00272 00278 function remove_tag_from_cache($tagname){ // Trim the cache so we're not in danger of running out of memory. 00280 // echo "<p>remove_tag_from_cache:<br />".htmlspecialchars($this->xmlcache); 00281 $this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1)); // "1" so that we replace only the FIRST instance 00282 // echo "<br />".htmlspecialchars($this->xmlcache)."</p>"; 00283 } 00284 00291 function get_recstatus($tagdata, $tagname){ 00292 if(preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)){ 00293 // echo "<p>get_recstatus($tagname) found status of $matches[1]</p>"; 00294 return intval($matches[1]); 00295 }else{ 00296 // echo "<p>get_recstatus($tagname) found nothing</p>"; 00297 return 0; // Unspecified 00298 } 00299 } 00300 00305 function process_group_tag($tagcontents) { 00306 global $DB; 00307 00308 // Get configs 00309 $truncatecoursecodes = $this->get_config('truncatecoursecodes'); 00310 $createnewcourses = $this->get_config('createnewcourses'); 00311 $createnewcategories = $this->get_config('createnewcategories'); 00312 00313 // Process tag contents 00314 $group = new stdClass(); 00315 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) { 00316 $group->coursecode = trim($matches[1]); 00317 } 00318 if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)){ 00319 $group->description = trim($matches[1]); 00320 } 00321 if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) { 00322 $group->shortName = trim($matches[1]); 00323 } 00324 if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) { 00325 $group->category = trim($matches[1]); 00326 } 00327 00328 $recstatus = ($this->get_recstatus($tagcontents, 'group')); 00329 //echo "<p>get_recstatus for this group returned $recstatus</p>"; 00330 00331 if (!(strlen($group->coursecode)>0)) { 00332 $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.'); 00333 } else { 00334 // First, truncate the course code if desired 00335 if (intval($truncatecoursecodes)>0) { 00336 $group->coursecode = ($truncatecoursecodes > 0) 00337 ? substr($group->coursecode, 0, intval($truncatecoursecodes)) 00338 : $group->coursecode; 00339 } 00340 00341 /* -----------Course aliasing is DEACTIVATED until a more general method is in place--------------- 00342 00343 // Second, look in the course alias table to see if the code should be translated to something else 00344 if($aliases = $DB->get_field('enrol_coursealias', 'toids', array('fromid'=>$group->coursecode))){ 00345 $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases"); 00346 // Alias is allowed to be a comma-separated list, so let's split it 00347 $group->coursecode = explode(',', $aliases); 00348 } 00349 */ 00350 00351 // For compatibility with the (currently inactive) course aliasing, we need this to be an array 00352 $group->coursecode = array($group->coursecode); 00353 00354 // Third, check if the course(s) exist 00355 foreach ($group->coursecode as $coursecode) { 00356 $coursecode = trim($coursecode); 00357 if (!$DB->get_field('course', 'id', array('idnumber'=>$coursecode))) { 00358 if (!$createnewcourses) { 00359 $this->log_line("Course $coursecode not found in Moodle's course idnumbers."); 00360 } else { 00361 // Set shortname to description or description to shortname if one is set but not the other. 00362 $nodescription = !isset($group->description); 00363 $noshortname = !isset($group->shortName); 00364 if ( $nodescription && $noshortname) { 00365 // If neither short nor long description are set let if fail 00366 $this->log_line("Neither long nor short name are set for $coursecode"); 00367 } else if ($nodescription) { 00368 // If short and ID exist, then give the long short's value, then give short the ID's value 00369 $group->description = $group->shortName; 00370 $group->shortName = $coursecode; 00371 } else if ($noshortname) { 00372 // If long and ID exist, then map long to long, then give short the ID's value. 00373 $group->shortName = $coursecode; 00374 } 00375 // Create the (hidden) course(s) if not found 00376 $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults 00377 $course = new stdClass(); 00378 $course->fullname = $group->description; 00379 $course->shortname = $group->shortName;; 00380 $course->idnumber = $coursecode; 00381 $course->format = $courseconfig->format; 00382 $course->visible = $courseconfig->visible; 00383 $course->numsections = $courseconfig->numsections; 00384 $course->hiddensections = $courseconfig->hiddensections; 00385 $course->newsitems = $courseconfig->newsitems; 00386 $course->showgrades = $courseconfig->showgrades; 00387 $course->showreports = $courseconfig->showreports; 00388 $course->maxbytes = $courseconfig->maxbytes; 00389 $course->groupmode = $courseconfig->groupmode; 00390 $course->groupmodeforce = $courseconfig->groupmodeforce; 00391 $course->enablecompletion = $courseconfig->enablecompletion; 00392 $course->completionstartonenrol = $courseconfig->completionstartonenrol; 00393 // Insert default names for teachers/students, from the current language 00394 00395 // Handle course categorisation (taken from the group.org.orgunit field if present) 00396 if (strlen($group->category)>0) { 00397 // If the category is defined and exists in Moodle, we want to store it in that one 00398 if ($catid = $DB->get_field('course_categories', 'id', array('name'=>$group->category))) { 00399 $course->category = $catid; 00400 } else if ($createnewcategories) { 00401 // Else if we're allowed to create new categories, let's create this one 00402 $newcat = new stdClass(); 00403 $newcat->name = $group->category; 00404 $newcat->visible = 0; 00405 $catid = $DB->insert_record('course_categories', $newcat); 00406 $course->category = $catid; 00407 $this->log_line("Created new (hidden) category, #$catid: $newcat->name"); 00408 } else { 00409 // If not found and not allowed to create, stick with default 00410 $this->log_line('Category '.$group->category.' not found in Moodle database, so using default category instead.'); 00411 $course->category = 1; 00412 } 00413 } else { 00414 $course->category = 1; 00415 } 00416 $course->timecreated = time(); 00417 $course->startdate = time(); 00418 // Choose a sort order that puts us at the start of the list! 00419 $course->sortorder = 0; 00420 00421 $courseid = $DB->insert_record('course', $course); 00422 00423 // Setup default enrolment plugins 00424 $course->id = $courseid; 00425 enrol_course_updated(true, $course, null); 00426 00427 // Setup the blocks 00428 $course = $DB->get_record('course', array('id' => $courseid)); 00429 blocks_add_default_course_blocks($course); 00430 00431 $section = new stdClass(); 00432 $section->course = $course->id; // Create a default section. 00433 $section->section = 0; 00434 $section->summaryformat = FORMAT_HTML; 00435 $section->id = $DB->insert_record("course_sections", $section); 00436 00437 add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)"); 00438 00439 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)"); 00440 } 00441 } else if ($recstatus==3 && ($courseid = $DB->get_field('course', 'id', array('idnumber'=>$coursecode)))) { 00442 // If course does exist, but recstatus==3 (delete), then set the course as hidden 00443 $DB->set_field('course', 'visible', '0', array('id'=>$courseid)); 00444 } 00445 } // End of foreach(coursecode) 00446 } 00447 } // End process_group_tag() 00448 00453 function process_person_tag($tagcontents){ 00454 global $CFG, $DB; 00455 00456 // Get plugin configs 00457 $imssourcedidfallback = $this->get_config('imssourcedidfallback'); 00458 $fixcaseusernames = $this->get_config('fixcaseusernames'); 00459 $fixcasepersonalnames = $this->get_config('fixcasepersonalnames'); 00460 $imsdeleteusers = $this->get_config('imsdeleteusers'); 00461 $createnewusers = $this->get_config('createnewusers'); 00462 00463 $person = new stdClass(); 00464 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){ 00465 $person->idnumber = trim($matches[1]); 00466 } 00467 if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){ 00468 $person->firstname = trim($matches[1]); 00469 } 00470 if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){ 00471 $person->lastname = trim($matches[1]); 00472 } 00473 if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){ 00474 $person->username = trim($matches[1]); 00475 } 00476 if($imssourcedidfallback && trim($person->username)==''){ 00477 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied 00478 // NB We don't use an "elseif" because the tag may be supplied-but-empty 00479 $person->username = $person->idnumber; 00480 } 00481 if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){ 00482 $person->email = trim($matches[1]); 00483 } 00484 if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){ 00485 $person->url = trim($matches[1]); 00486 } 00487 if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){ 00488 $person->city = trim($matches[1]); 00489 } 00490 if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){ 00491 $person->country = trim($matches[1]); 00492 } 00493 00494 // Fix case of some of the fields if required 00495 if($fixcaseusernames && isset($person->username)){ 00496 $person->username = strtolower($person->username); 00497 } 00498 if($fixcasepersonalnames){ 00499 if(isset($person->firstname)){ 00500 $person->firstname = ucwords(strtolower($person->firstname)); 00501 } 00502 if(isset($person->lastname)){ 00503 $person->lastname = ucwords(strtolower($person->lastname)); 00504 } 00505 } 00506 00507 $recstatus = ($this->get_recstatus($tagcontents, 'person')); 00508 00509 00510 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on 00511 // In the "users" table we can do this by setting deleted=1 00512 if($recstatus==3){ 00513 00514 if($imsdeleteusers){ // If we're allowed to delete user records 00515 // Make sure their "deleted" field is set to one 00516 $DB->set_field('user', 'deleted', 1, array('username'=>$person->username)); 00517 $this->log_line("Marked user record for user '$person->username' (ID number $person->idnumber) as deleted."); 00518 }else{ 00519 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber)."); 00520 } 00521 00522 }else{ // Add or update record 00523 00524 00525 // If the user exists (matching sourcedid) then we don't need to do anything. 00526 if(!$DB->get_field('user', 'id', array('idnumber'=>$person->idnumber)) && $createnewusers){ 00527 // If they don't exist and haven't a defined username, we log this as a potential problem. 00528 if((!isset($person->username)) || (strlen($person->username)==0)){ 00529 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person."); 00530 } else if ($DB->get_field('user', 'id', array('username'=>$person->username))){ 00531 // If their idnumber is not registered but their user ID is, then add their idnumber to their record 00532 $DB->set_field('user', 'idnumber', $person->idnumber, array('username'=>$person->username)); 00533 } else { 00534 00535 // If they don't exist and they have a defined username, and $createnewusers == true, we create them. 00536 $person->lang = 'manual'; //TODO: this needs more work due tu multiauth changes 00537 $person->auth = $CFG->auth; 00538 $person->confirmed = 1; 00539 $person->timemodified = time(); 00540 $person->mnethostid = $CFG->mnet_localhost_id; 00541 $id = $DB->insert_record('user', $person); 00542 /* 00543 Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib. 00544 00545 //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load 00546 // it into the user's profile. Beware that this may cause a heavy overhead on the server. 00547 if($CFG->enrol_processphoto){ 00548 if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){ 00549 $person->urlphoto = trim($matches[1]); 00550 } 00551 //Habilitam el flag que ens indica que el personatge t foto prpia. 00552 $person->picture = 1; 00553 //Llibreria creada per nosaltres mateixos. 00554 require_once($CFG->dirroot.'/lib/gdlib.php'); 00555 if ($usernew->picture = save_profile_image($id, $person->urlphoto,'user')) { TODO: use process_new_icon() instead 00556 $DB->set_field('user', 'picture', $usernew->picture, array('id'=>$id)); /// Note picture in DB 00557 } 00558 } 00559 */ 00560 $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber)."); 00561 } 00562 } elseif ($createnewusers) { 00563 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber)."); 00564 00565 // Make sure their "deleted" field is set to zero. 00566 $DB->set_field('user', 'deleted', 0, array('idnumber'=>$person->idnumber)); 00567 }else{ 00568 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber)."); 00569 } 00570 00571 } // End of are-we-deleting-or-adding 00572 00573 } // End process_person_tag() 00574 00580 function process_membership_tag($tagcontents){ 00581 global $DB; 00582 00583 // Get plugin configs 00584 $truncatecoursecodes = $this->get_config('truncatecoursecodes'); 00585 $imscapitafix = $this->get_config('imscapitafix'); 00586 00587 $memberstally = 0; 00588 $membersuntally = 0; 00589 00590 // In order to reduce the number of db queries required, group name/id associations are cached in this array: 00591 $groupids = array(); 00592 00593 $ship = new stdClass(); 00594 00595 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){ 00596 $ship->coursecode = ($truncatecoursecodes > 0) 00597 ? substr(trim($matches[1]), 0, intval($truncatecoursecodes)) 00598 : trim($matches[1]); 00599 $ship->courseid = $DB->get_field('course', 'id', array('idnumber'=>$ship->coursecode)); 00600 } 00601 if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){ 00602 $courseobj = new stdClass(); 00603 $courseobj->id = $ship->courseid; 00604 00605 foreach($membermatches as $mmatch){ 00606 $member = new stdClass(); 00607 $memberstoreobj = new stdClass(); 00608 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){ 00609 $member->idnumber = trim($matches[1]); 00610 } 00611 if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){ 00612 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides 00613 } elseif($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){ 00614 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification! 00615 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides 00616 } 00617 if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){ 00618 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol 00619 } 00620 00621 $recstatus = ($this->get_recstatus($mmatch[1], 'role')); 00622 if($recstatus==3){ 00623 $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0 00624 //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>"; 00625 } 00626 00627 $timeframe = new stdClass(); 00628 $timeframe->begin = 0; 00629 $timeframe->end = 0; 00630 if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){ 00631 $timeframe = $this->decode_timeframe($matches[1]); 00632 } 00633 if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){ 00634 $member->groupname = trim($matches[1]); 00635 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause 00636 } 00637 00638 $rolecontext = get_context_instance(CONTEXT_COURSE, $ship->courseid); 00639 $rolecontext = $rolecontext->id; // All we really want is the ID 00640 //$this->log_line("Context instance for course $ship->courseid is..."); 00641 //print_r($rolecontext); 00642 00643 // Add or remove this student or teacher to the course... 00644 $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber'=>$member->idnumber)); 00645 $memberstoreobj->enrol = 'imsenterprise'; 00646 $memberstoreobj->course = $ship->courseid; 00647 $memberstoreobj->time = time(); 00648 $memberstoreobj->timemodified = time(); 00649 if($memberstoreobj->userid){ 00650 00651 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to. 00652 // Zero means this roletype is supposed to be skipped. 00653 $moodleroleid = $this->rolemappings[$member->roletype]; 00654 if(!$moodleroleid) { 00655 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course"); 00656 continue; 00657 } 00658 00659 if(intval($member->status) == 1) { 00660 // Enrol the member 00661 00662 $einstance = $DB->get_record('enrol', 00663 array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol)); 00664 if (empty($einstance)) { 00665 // Only add an enrol instance to the course if non-existent 00666 $enrolid = $this->add_instance($courseobj); 00667 $einstance = $DB->get_record('enrol', array('id' => $enrolid)); 00668 } 00669 00670 $this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end); 00671 00672 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course"); 00673 $memberstally++; 00674 00675 // At this point we can also ensure the group membership is recorded if present 00676 if(isset($member->groupname)){ 00677 // Create the group if it doesn't exist - either way, make sure we know the group ID 00678 if(isset($groupids[$member->groupname])) { 00679 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available 00680 } else { 00681 if($groupid = $DB->get_field('groups', 'id', array('courseid'=>$ship->courseid, 'name'=>$member->groupname))){ 00682 $member->groupid = $groupid; 00683 $groupids[$member->groupname] = $groupid; // Store ID in cache 00684 } else { 00685 // Attempt to create the group 00686 $group = new stdClass(); 00687 $group->name = $member->groupname; 00688 $group->courseid = $ship->courseid; 00689 $group->timecreated = time(); 00690 $group->timemodified = time(); 00691 $groupid = $DB->insert_record('groups', $group); 00692 $this->log_line('Added a new group for this course: '.$group->name); 00693 $groupids[$member->groupname] = $groupid; // Store ID in cache 00694 $member->groupid = $groupid; 00695 } 00696 } 00697 // Add the user-to-group association if it doesn't already exist 00698 if($member->groupid) { 00699 groups_add_member($member->groupid, $memberstoreobj->userid); 00700 } 00701 } // End of group-enrolment (from member.role.extension.cohort tag) 00702 00703 } elseif ($this->get_config('imsunenrol')) { 00704 // Unenrol member 00705 00706 $einstances = $DB->get_records('enrol', 00707 array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id)); 00708 foreach ($einstances as $einstance) { 00709 // Unenrol the user from all imsenterprise enrolment instances 00710 $this->unenrol_user($einstance, $memberstoreobj->userid); 00711 } 00712 00713 $membersuntally++; 00714 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course"); 00715 } 00716 00717 } 00718 } 00719 $this->log_line("Added $memberstally users to course $ship->coursecode"); 00720 if($membersuntally > 0){ 00721 $this->log_line("Removed $membersuntally users from course $ship->coursecode"); 00722 } 00723 } 00724 } // End process_membership_tag() 00725 00731 function process_properties_tag($tagcontents){ 00732 $imsrestricttarget = $this->get_config('imsrestricttarget'); 00733 00734 if ($imsrestricttarget) { 00735 if(!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))){ 00736 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data."); 00737 $this->continueprocessing = false; 00738 } 00739 } 00740 } 00741 00748 function log_line($string){ 00749 mtrace($string); 00750 if($this->logfp) { 00751 fwrite($this->logfp, $string . "\n"); 00752 } 00753 } 00754 00758 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 00759 $ret = new stdClass(); 00760 $ret->begin = $ret->end = 0; 00761 // Explanatory note: The matching will ONLY match if the attribute restrict="1" 00762 // because otherwise the time markers should be ignored (participation should be 00763 // allowed outside the period) 00764 if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){ 00765 $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]); 00766 } 00767 if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){ 00768 $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]); 00769 } 00770 return $ret; 00771 } // End decode_timeframe 00772 00777 function load_role_mappings() { 00778 require_once('locallib.php'); 00779 global $DB; 00780 00781 $imsroles = new imsenterprise_roles(); 00782 $imsroles = $imsroles->get_imsroles(); 00783 00784 $this->rolemappings = array(); 00785 foreach($imsroles as $imsrolenum=>$imsrolename) { 00786 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum); 00787 } 00788 } 00789 00790 } // end of class 00791 00792