Merge branch 'master_MDL-29290' of git://github.com/danmarsden/moodle
[moodle.git] / enrol / imsenterprise / lib.php
CommitLineData
8c25f6f2 1<?php
fa36ca3d
AB
2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
8c25f6f2 16
fa36ca3d
AB
17/**
18 * IMS Enterprise file enrolment plugin.
19 *
20 * This plugin lets the user specify an IMS Enterprise file to be processed.
21 * The IMS Enterprise file is mainly parsed on a regular cron,
22 * but can also be imported via the UI (Admin Settings).
97c65be5
PS
23 * @package enrol
24 * @subpackage imsenterprise
25 * @copyright 2010 Eugene Venter
26 * @author Eugene Venter - based on code by Dan Stowell
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
fa36ca3d 28 */
8c25f6f2 29
97c65be5
PS
30defined('MOODLE_INTERNAL') || die();
31
8c25f6f2 32/*
33
34Note for programmers:
35
2dcccb0f 36This class uses regular expressions to mine the data file. The main reason is
8c25f6f2 37that XML handling changes from PHP 4 to PHP 5, so this should work on both.
38
2dcccb0f 39One drawback is that the pattern-matching doesn't (currently) handle XML
8c25f6f2 40namespaces - it only copes with a <group> tag if it says <group>, and not
41(for example) <ims:group>.
42
2dcccb0f 43This should also be able to handle VERY LARGE FILES - so the entire IMS file is
44NOT loaded into memory at once. It's handled line-by-line, 'forgetting' tags as
8c25f6f2 45soon as they are processed.
46
2dcccb0f 47N.B. The "sourcedid" ID code is translated to Moodle's "idnumber" field, both
8c25f6f2 48for users and for courses.
49
50*/
51
fa36ca3d 52require_once($CFG->dirroot.'/group/lib.php');
26ba0912 53
54
fa36ca3d 55class enrol_imsenterprise_plugin extends enrol_plugin {
8c25f6f2 56
fa36ca3d 57var $log;
8c25f6f2 58
8c25f6f2 59
60/**
61* Read in an IMS Enterprise file.
2dcccb0f 62* Originally designed to handle v1.1 files but should be able to handle
8c25f6f2 63* earlier types as well, I believe.
64*
65*/
66function cron() {
67 global $CFG;
68
fa36ca3d
AB
69 // Get configs
70 $imsfilelocation = $this->get_config('imsfilelocation');
71 $logtolocation = $this->get_config('logtolocation');
72 $mailadmins = $this->get_config('mailadmins');
73 $prev_time = $this->get_config('prev_time');
74 $prev_md5 = $this->get_config('prev_md5');
75 $prev_path = $this->get_config('prev_path');
76
77 if (empty($imsfilelocation)) {
8c25f6f2 78 // $filename = "$CFG->dirroot/enrol/imsenterprise/example.xml"; // Default location
79 $filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location
80 } else {
fa36ca3d 81 $filename = $imsfilelocation;
8c25f6f2 82 }
83
84 $this->logfp = false; // File pointer for writing log data to
fa36ca3d
AB
85 if(!empty($logtolocation)) {
86 $this->logfp = fopen($logtolocation, 'a');
8c25f6f2 87 }
88
8c25f6f2 89 if ( file_exists($filename) ) {
90 @set_time_limit(0);
2dcccb0f 91 $starttime = time();
92
8c25f6f2 93 $this->log_line('----------------------------------------------------------------------');
94 $this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
95 $this->log_line('Found file '.$filename);
96 $this->xmlcache = '';
2dcccb0f 97
26ba0912 98 // Make sure we understand how to map the IMS-E roles to Moodle roles
99 $this->load_role_mappings();
8c25f6f2 100
101 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
102 $filemtime = filemtime($filename);
2dcccb0f 103
8c25f6f2 104 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
105 // This is so we avoid wasting the server's efforts processing a file unnecessarily
fa36ca3d 106 if(empty($prev_path) || ($filename != $prev_path)) {
8c25f6f2 107 $fileisnew = true;
fa36ca3d 108 } elseif(isset($prev_time) && ($filemtime <= $prev_time)) {
8c25f6f2 109 $fileisnew = false;
110 $this->log_line('File modification time is not more recent than last update - skipping processing.');
fa36ca3d 111 } elseif(isset($prev_md5) && ($md5 == $prev_md5)) {
8c25f6f2 112 $fileisnew = false;
113 $this->log_line('File MD5 hash is same as on last update - skipping processing.');
fa36ca3d 114 } else {
8c25f6f2 115 $fileisnew = true; // Let's process it!
116 }
117
fa36ca3d 118 if($fileisnew) {
2dcccb0f 119
8c25f6f2 120 $listoftags = array('group', 'person', 'member', 'membership', 'comments', 'properties'); // The list of tags which should trigger action (even if only cache trimming)
121 $this->continueprocessing = true; // The <properties> tag is allowed to halt processing if we're demanding a matching target
2dcccb0f 122
8c25f6f2 123 // FIRST PASS: Run through the file and process the group/person entries
124 if (($fh = fopen($filename, "r")) != false) {
2dcccb0f 125
8c25f6f2 126 $line = 0;
127 while ((!feof($fh)) && $this->continueprocessing) {
2dcccb0f 128
8c25f6f2 129 $line++;
130 $curline = fgets($fh);
131 $this->xmlcache .= $curline; // Add a line onto the XML cache
2dcccb0f 132
fa36ca3d 133 while (true) {
8c25f6f2 134 // If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
135 // Must always make sure to remove tags from cache so they don't clog up our memory
fa36ca3d 136 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)) {
8c25f6f2 137 $this->process_group_tag($tagcontents);
138 $this->remove_tag_from_cache('group');
fa36ca3d 139 } elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)) {
8c25f6f2 140 $this->process_person_tag($tagcontents);
141 $this->remove_tag_from_cache('person');
fa36ca3d 142 } elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)) {
8c25f6f2 143 $this->process_membership_tag($tagcontents);
144 $this->remove_tag_from_cache('membership');
fa36ca3d 145 } elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)) {
8c25f6f2 146 $this->remove_tag_from_cache('comments');
fa36ca3d 147 } elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)) {
8c25f6f2 148 $this->process_properties_tag($tagcontents);
149 $this->remove_tag_from_cache('properties');
fa36ca3d
AB
150 } else {
151 break;
152 }
153 } // End of while-tags-are-detected
8c25f6f2 154 } // end of while loop
155 fclose($fh);
6f3dc679 156 fix_course_sortorder();
8c25f6f2 157 } // end of if(file_open) for first pass
2dcccb0f 158
8c25f6f2 159 /*
2dcccb0f 160
161
8c25f6f2 162 SECOND PASS REMOVED
163 Since the IMS specification v1.1 insists that "memberships" should come last,
164 and since vendors seem to have done this anyway (even with 1.0),
165 we can sensibly perform the import in one fell swoop.
2dcccb0f 166
167
8c25f6f2 168 // SECOND PASS: Now go through the file and process the membership entries
169 $this->xmlcache = '';
170 if (($fh = fopen($filename, "r")) != false) {
171 $line = 0;
172 while ((!feof($fh)) && $this->continueprocessing) {
173 $line++;
174 $curline = fgets($fh);
175 $this->xmlcache .= $curline; // Add a line onto the XML cache
2dcccb0f 176
8c25f6f2 177 while(true){
178 // Must always make sure to remove tags from cache so they don't clog up our memory
179 if($tagcontents = $this->full_tag_found_in_cache('group', $curline)){
180 $this->remove_tag_from_cache('group');
181 }elseif($tagcontents = $this->full_tag_found_in_cache('person', $curline)){
182 $this->remove_tag_from_cache('person');
183 }elseif($tagcontents = $this->full_tag_found_in_cache('membership', $curline)){
184 $this->process_membership_tag($tagcontents);
185 $this->remove_tag_from_cache('membership');
186 }elseif($tagcontents = $this->full_tag_found_in_cache('comments', $curline)){
187 $this->remove_tag_from_cache('comments');
188 }elseif($tagcontents = $this->full_tag_found_in_cache('properties', $curline)){
189 $this->remove_tag_from_cache('properties');
190 }else{
191 break;
192 }
193 }
194 } // end of while loop
195 fclose($fh);
196 } // end of if(file_open) for second pass
2dcccb0f 197
198
8c25f6f2 199 */
200
201 $timeelapsed = time() - $starttime;
202 $this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
203
204
205 } // END of "if file is new"
2dcccb0f 206
207
8c25f6f2 208 // These variables are stored so we can compare them against the IMS file, next time round.
fa36ca3d
AB
209 $this->set_config('prev_time', $filemtime);
210 $this->set_config('prev_md5', $md5);
211 $this->set_config('prev_path', $filename);
8c25f6f2 212
213
214
215 }else{ // end of if(file_exists)
216 $this->log_line('File not found: '.$filename);
217 }
218
fa36ca3d 219 if (!empty($mailadmins)) {
8c25f6f2 220 $msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
fa36ca3d 221 if(!empty($logtolocation)){
8c25f6f2 222 if($this->logfp){
223 $msg .= "Log data has been written to:\n";
fa36ca3d
AB
224 $msg .= "$logtolocation\n";
225 $msg .= "(Log file size: ".ceil(filesize($logtolocation)/1024)."Kb)\n\n";
8c25f6f2 226 }else{
227 $msg .= "The log file appears not to have been successfully written.\nCheck that the file is writeable by the server:\n";
fa36ca3d 228 $msg .= "$logtolocation\n\n";
8c25f6f2 229 }
230 }else{
231 $msg .= "Logging is currently not active.";
232 }
4317f92f 233
94b9c2e8 234 $eventdata = new stdClass();
3b120e46 235 $eventdata->modulename = 'moodle';
fa36ca3d
AB
236 $eventdata->component = 'imsenterprise';
237 $eventdata->name = 'imsenterprise_enrolment';
3b120e46 238 $eventdata->userfrom = get_admin();
239 $eventdata->userto = get_admin();
240 $eventdata->subject = "Moodle IMS Enterprise enrolment notification";
241 $eventdata->fullmessage = $msg;
242 $eventdata->fullmessageformat = FORMAT_PLAIN;
243 $eventdata->fullmessagehtml = '';
4454447d 244 $eventdata->smallmessage = '';
7c7d3afa 245 message_send($eventdata);
3b120e46 246
8c25f6f2 247 $this->log_line('Notification email sent to administrator.');
248
249 }
250
251 if($this->logfp){
252 fclose($this->logfp);
253 }
2dcccb0f 254
8c25f6f2 255
256} // end of cron() function
257
258/**
259* Check if a complete tag is found in the cached data, which usually happens
260* when the end of the tag has only just been loaded into the cache.
261* Returns either false, or the contents of the tag (including start and end).
262* @param string $tagname Name of tag to look for
263* @param string $latestline The very last line in the cache (used for speeding up the match)
264*/
265function full_tag_found_in_cache($tagname, $latestline){ // Return entire element if found. Otherwise return false.
266 if(strpos(strtolower($latestline), '</'.strtolower($tagname).'>')===false){
267 return false;
268 }elseif(preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)){
269 return $matches[1];
270 }else return false;
271}
272
273/**
274* Remove complete tag from the cached data (including all its contents) - so
275* that the cache doesn't grow to unmanageable size
276* @param string $tagname Name of tag to look for
277*/
278function remove_tag_from_cache($tagname){ // Trim the cache so we're not in danger of running out of memory.
279 ///echo "<p>remove_tag_from_cache: $tagname</p>"; flush(); ob_flush();
280 // echo "<p>remove_tag_from_cache:<br />".htmlspecialchars($this->xmlcache);
281 $this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1)); // "1" so that we replace only the FIRST instance
282 // echo "<br />".htmlspecialchars($this->xmlcache)."</p>";
283}
284
285/**
286* Very simple convenience function to return the "recstatus" found in person/group/role tags.
287* 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
288* @param string $tagdata the tag XML data
289* @param string $tagname the name of the tag we're interested in
290*/
291function get_recstatus($tagdata, $tagname){
292 if(preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)){
293 // echo "<p>get_recstatus($tagname) found status of $matches[1]</p>";
294 return intval($matches[1]);
295 }else{
296 // echo "<p>get_recstatus($tagname) found nothing</p>";
297 return 0; // Unspecified
298 }
299}
300
301/**
302* Process the group tag. This defines a Moodle course.
303* @param string $tagconents The raw contents of the XML element
304*/
801fdcfd 305function process_group_tag($tagcontents) {
fa36ca3d
AB
306 global $DB;
307
308 // Get configs
309 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
310 $createnewcourses = $this->get_config('createnewcourses');
311 $createnewcategories = $this->get_config('createnewcategories');
2dcccb0f 312
8c25f6f2 313 // Process tag contents
94b9c2e8 314 $group = new stdClass();
801fdcfd 315 if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
8c25f6f2 316 $group->coursecode = trim($matches[1]);
317 }
801fdcfd 318 if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
8c25f6f2 319 $group->description = trim($matches[1]);
320 }
801fdcfd 321 if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
8c25f6f2 322 $group->category = trim($matches[1]);
323 }
2dcccb0f 324
8c25f6f2 325 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
326 //echo "<p>get_recstatus for this group returned $recstatus</p>";
2dcccb0f 327
801fdcfd 328 if (!(strlen($group->coursecode)>0)) {
8c25f6f2 329 $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.');
801fdcfd 330 } else {
8c25f6f2 331 // First, truncate the course code if desired
801fdcfd 332 if (intval($truncatecoursecodes)>0) {
fa36ca3d
AB
333 $group->coursecode = ($truncatecoursecodes > 0)
334 ? substr($group->coursecode, 0, intval($truncatecoursecodes))
8c25f6f2 335 : $group->coursecode;
336 }
2dcccb0f 337
8c25f6f2 338 /* -----------Course aliasing is DEACTIVATED until a more general method is in place---------------
2dcccb0f 339
8c25f6f2 340 // Second, look in the course alias table to see if the code should be translated to something else
a5d424df 341 if($aliases = $DB->get_field('enrol_coursealias', 'toids', array('fromid'=>$group->coursecode))){
8c25f6f2 342 $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases");
343 // Alias is allowed to be a comma-separated list, so let's split it
344 $group->coursecode = explode(',', $aliases);
345 }
346 */
2dcccb0f 347
8c25f6f2 348 // For compatibility with the (currently inactive) course aliasing, we need this to be an array
349 $group->coursecode = array($group->coursecode);
2dcccb0f 350
8c25f6f2 351 // Third, check if the course(s) exist
801fdcfd 352 foreach ($group->coursecode as $coursecode) {
8c25f6f2 353 $coursecode = trim($coursecode);
801fdcfd
DM
354 if (!$DB->get_field('course', 'id', array('idnumber'=>$coursecode))) {
355 if (!$createnewcourses) {
20d694ac
DM
356 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
357 } else {
358 // Create the (hidden) course(s) if not found
359 $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults
360 $course = new stdClass();
361 $course->fullname = $group->description;
362 $course->shortname = $coursecode;
363 $course->idnumber = $coursecode;
364 $course->format = $courseconfig->format;
365 $course->visible = $courseconfig->visible;
366 $course->numsections = $courseconfig->numsections;
367 $course->hiddensections = $courseconfig->hiddensections;
368 $course->newsitems = $courseconfig->newsitems;
369 $course->showgrades = $courseconfig->showgrades;
370 $course->showreports = $courseconfig->showreports;
371 $course->maxbytes = $courseconfig->maxbytes;
372 $course->groupmode = $courseconfig->groupmode;
373 $course->groupmodeforce = $courseconfig->groupmodeforce;
374 $course->enablecompletion = $courseconfig->enablecompletion;
375 $course->completionstartonenrol = $courseconfig->completionstartonenrol;
376 // Insert default names for teachers/students, from the current language
20d694ac
DM
377
378 // Handle course categorisation (taken from the group.org.orgunit field if present)
801fdcfd 379 if (strlen($group->category)>0) {
20d694ac 380 // If the category is defined and exists in Moodle, we want to store it in that one
801fdcfd 381 if ($catid = $DB->get_field('course_categories', 'id', array('name'=>$group->category))) {
20d694ac 382 $course->category = $catid;
801fdcfd 383 } else if ($createnewcategories) {
20d694ac
DM
384 // Else if we're allowed to create new categories, let's create this one
385 $newcat = new stdClass();
386 $newcat->name = $group->category;
387 $newcat->visible = 0;
388 $catid = $DB->insert_record('course_categories', $newcat);
389 $course->category = $catid;
390 $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
801fdcfd 391 } else {
20d694ac
DM
392 // If not found and not allowed to create, stick with default
393 $this->log_line('Category '.$group->category.' not found in Moodle database, so using default category instead.');
394 $course->category = 1;
395 }
801fdcfd 396 } else {
8c25f6f2 397 $course->category = 1;
398 }
20d694ac
DM
399 $course->timecreated = time();
400 $course->startdate = time();
401 // Choose a sort order that puts us at the start of the list!
402 $course->sortorder = 0;
0cbe8111 403
20d694ac 404 $courseid = $DB->insert_record('course', $course);
2dcccb0f 405
20d694ac
DM
406 // Setup the blocks
407 $course = $DB->get_record('course', array('id' => $courseid));
408 blocks_add_default_course_blocks($course);
2dcccb0f 409
20d694ac
DM
410 $section = new stdClass();
411 $section->course = $course->id; // Create a default section.
412 $section->section = 0;
413 $section->summaryformat = FORMAT_HTML;
414 $section->id = $DB->insert_record("course_sections", $section);
2dcccb0f 415
20d694ac 416 add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
2dcccb0f 417
20d694ac
DM
418 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
419 }
801fdcfd 420 } else if ($recstatus==3 && ($courseid = $DB->get_field('course', 'id', array('idnumber'=>$coursecode)))) {
8c25f6f2 421 // If course does exist, but recstatus==3 (delete), then set the course as hidden
50c5bef4 422 $DB->set_field('course', 'visible', '0', array('id'=>$courseid));
8c25f6f2 423 }
424 } // End of foreach(coursecode)
425 }
426} // End process_group_tag()
427
428/**
429* Process the person tag. This defines a Moodle user.
430* @param string $tagconents The raw contents of the XML element
431*/
432function process_person_tag($tagcontents){
50c5bef4 433 global $CFG, $DB;
8c25f6f2 434
fa36ca3d
AB
435 // Get plugin configs
436 $imssourcedidfallback = $this->get_config('imssourcedidfallback');
437 $fixcaseusernames = $this->get_config('fixcaseusernames');
438 $fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
439 $imsdeleteusers = $this->get_config('imsdeleteusers');
440 $createnewusers = $this->get_config('createnewusers');
441
94b9c2e8 442 $person = new stdClass();
8c25f6f2 443 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
444 $person->idnumber = trim($matches[1]);
445 }
446 if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){
447 $person->firstname = trim($matches[1]);
448 }
449 if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){
450 $person->lastname = trim($matches[1]);
451 }
452 if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){
453 $person->username = trim($matches[1]);
22016388 454 }
fa36ca3d 455 if($imssourcedidfallback && trim($person->username)==''){
8c25f6f2 456 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
450a0a7d 457 // NB We don't use an "elseif" because the tag may be supplied-but-empty
8c25f6f2 458 $person->username = $person->idnumber;
459 }
460 if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){
461 $person->email = trim($matches[1]);
462 }
463 if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){
464 $person->url = trim($matches[1]);
465 }
466 if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){
467 $person->city = trim($matches[1]);
468 }
469 if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){
470 $person->country = trim($matches[1]);
471 }
472
473 // Fix case of some of the fields if required
fa36ca3d 474 if($fixcaseusernames && isset($person->username)){
8c25f6f2 475 $person->username = strtolower($person->username);
476 }
fa36ca3d 477 if($fixcasepersonalnames){
8c25f6f2 478 if(isset($person->firstname)){
479 $person->firstname = ucwords(strtolower($person->firstname));
480 }
481 if(isset($person->lastname)){
482 $person->lastname = ucwords(strtolower($person->lastname));
483 }
484 }
485
486 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
487
488
489 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on
490 // In the "users" table we can do this by setting deleted=1
491 if($recstatus==3){
2dcccb0f 492
fa36ca3d 493 if($imsdeleteusers){ // If we're allowed to delete user records
8c25f6f2 494 // Make sure their "deleted" field is set to one
50c5bef4 495 $DB->set_field('user', 'deleted', 1, array('username'=>$person->username));
8c25f6f2 496 $this->log_line("Marked user record for user '$person->username' (ID number $person->idnumber) as deleted.");
497 }else{
498 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
499 }
2dcccb0f 500
8c25f6f2 501 }else{ // Add or update record
2dcccb0f 502
8c25f6f2 503
504 // If the user exists (matching sourcedid) then we don't need to do anything.
fa36ca3d 505 if(!$DB->get_field('user', 'id', array('idnumber'=>$person->idnumber)) && $createnewusers){
8c25f6f2 506 // If they don't exist and haven't a defined username, we log this as a potential problem.
507 if((!isset($person->username)) || (strlen($person->username)==0)){
508 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person.");
50c5bef4 509 } else if ($DB->get_field('user', 'id', array('username'=>$person->username))){
8c25f6f2 510 // If their idnumber is not registered but their user ID is, then add their idnumber to their record
50c5bef4 511 $DB->set_field('user', 'idnumber', $person->idnumber, array('username'=>$person->username));
512 } else {
2dcccb0f 513
fa36ca3d 514 // If they don't exist and they have a defined username, and $createnewusers == true, we create them.
2dcccb0f 515 $person->lang = 'manual'; //TODO: this needs more work due tu multiauth changes
67e3f5b5 516 $person->auth = $CFG->auth;
8c25f6f2 517 $person->confirmed = 1;
518 $person->timemodified = time();
2dcccb0f 519 $person->mnethostid = $CFG->mnet_localhost_id;
9d97f08e 520 $id = $DB->insert_record('user', $person);
8c25f6f2 521 /*
522 Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib.
2dcccb0f 523
524 //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load
8c25f6f2 525 // it into the user's profile. Beware that this may cause a heavy overhead on the server.
526 if($CFG->enrol_processphoto){
527 if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){
528 $person->urlphoto = trim($matches[1]);
529 }
530 //Habilitam el flag que ens indica que el personatge t foto prpia.
2dcccb0f 531 $person->picture = 1;
8c25f6f2 532 //Llibreria creada per nosaltres mateixos.
533 require_once($CFG->dirroot.'/lib/gdlib.php');
edfd6a5e 534 if ($usernew->picture = save_profile_image($id, $person->urlphoto,'user')) { TODO: use process_new_icon() instead
50c5bef4 535 $DB->set_field('user', 'picture', $usernew->picture, array('id'=>$id)); /// Note picture in DB
8c25f6f2 536 }
537 }
538 */
9d97f08e 539 $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber).");
8c25f6f2 540 }
fa36ca3d 541 } elseif ($createnewusers) {
8c25f6f2 542 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
2dcccb0f 543
8c25f6f2 544 // Make sure their "deleted" field is set to zero.
50c5bef4 545 $DB->set_field('user', 'deleted', 0, array('idnumber'=>$person->idnumber));
8c25f6f2 546 }else{
547 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
548 }
2dcccb0f 549
8c25f6f2 550 } // End of are-we-deleting-or-adding
551
552} // End process_person_tag()
553
554/**
2dcccb0f 555* Process the membership tag. This defines whether the specified Moodle users
8c25f6f2 556* should be added/removed as teachers/students.
557* @param string $tagconents The raw contents of the XML element
558*/
559function process_membership_tag($tagcontents){
fa36ca3d
AB
560 global $DB;
561
562 // Get plugin configs
563 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
564 $imscapitafix = $this->get_config('imscapitafix');
565
26ba0912 566 $memberstally = 0;
567 $membersuntally = 0;
2dcccb0f 568
8c25f6f2 569 // In order to reduce the number of db queries required, group name/id associations are cached in this array:
570 $groupids = array();
2dcccb0f 571
8c25f6f2 572 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
fa36ca3d
AB
573 $ship->coursecode = ($truncatecoursecodes > 0)
574 ? substr(trim($matches[1]), 0, intval($truncatecoursecodes))
8c25f6f2 575 : trim($matches[1]);
50c5bef4 576 $ship->courseid = $DB->get_field('course', 'id', array('idnumber'=>$ship->coursecode));
8c25f6f2 577 }
578 if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){
fa36ca3d
AB
579 $courseobj = new stdClass();
580 $courseobj->id = $ship->courseid;
581
8c25f6f2 582 foreach($membermatches as $mmatch){
583 unset($member);
584 unset($memberstoreobj);
585 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){
586 $member->idnumber = trim($matches[1]);
587 }
588 if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){
589 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
fa36ca3d 590 } elseif($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){
8c25f6f2 591 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification!
592 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
593 }
594 if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){
595 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol
596 }
597
598 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
599 if($recstatus==3){
600 $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0
601 //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>";
602 }
603
604 $timeframe->begin = 0;
605 $timeframe->end = 0;
606 if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){
607 $timeframe = $this->decode_timeframe($matches[1]);
608 }
609 if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){
610 $member->groupname = trim($matches[1]);
611 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause
612 }
2dcccb0f 613
26ba0912 614 $rolecontext = get_context_instance(CONTEXT_COURSE, $ship->courseid);
615 $rolecontext = $rolecontext->id; // All we really want is the ID
616//$this->log_line("Context instance for course $ship->courseid is...");
617//print_r($rolecontext);
2dcccb0f 618
8c25f6f2 619 // Add or remove this student or teacher to the course...
50c5bef4 620 $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber'=>$member->idnumber));
8c25f6f2 621 $memberstoreobj->enrol = 'imsenterprise';
622 $memberstoreobj->course = $ship->courseid;
623 $memberstoreobj->time = time();
624 $memberstoreobj->timemodified = time();
26ba0912 625 if($memberstoreobj->userid){
2dcccb0f 626
26ba0912 627 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
628 // Zero means this roletype is supposed to be skipped.
629 $moodleroleid = $this->rolemappings[$member->roletype];
fa36ca3d 630 if(!$moodleroleid) {
26ba0912 631 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course");
632 continue;
633 }
2dcccb0f 634
fa36ca3d
AB
635 if(intval($member->status) == 1) {
636 // Enrol the member
26ba0912 637
fa36ca3d
AB
638 $einstance = $DB->get_record('enrol',
639 array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));
640 if (empty($einstance)) {
641 // Only add an enrol instance to the course if non-existent
642 $enrolid = $this->add_instance($courseobj);
643 $einstance = $DB->get_record('enrol', array('id' => $enrolid));
644 }
26ba0912 645
fa36ca3d
AB
646 $this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);
647
648 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
649 $memberstally++;
650
651 // At this point we can also ensure the group membership is recorded if present
652 if(isset($member->groupname)){
653 // Create the group if it doesn't exist - either way, make sure we know the group ID
654 if(isset($groupids[$member->groupname])) {
655 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available
656 } else {
fb57247e 657 if($groupid = $DB->get_field('groups', 'id', array('courseid'=>$ship->courseid, 'name'=>$member->groupname))){
fa36ca3d
AB
658 $member->groupid = $groupid;
659 $groupids[$member->groupname] = $groupid; // Store ID in cache
660 } else {
661 // Attempt to create the group
662 $group->name = $member->groupname;
663 $group->courseid = $ship->courseid;
664 $group->timecreated = time();
665 $group->timemodified = time();
666 $groupid = $DB->insert_record('groups', $group);
667 $this->log_line('Added a new group for this course: '.$group->name);
668 $groupids[$member->groupname] = $groupid; // Store ID in cache
669 $member->groupid = $groupid;
26ba0912 670 }
fa36ca3d
AB
671 }
672 // Add the user-to-group association if it doesn't already exist
673 if($member->groupid) {
674 groups_add_member($member->groupid, $memberstoreobj->userid);
675 }
676 } // End of group-enrolment (from member.role.extension.cohort tag)
677
678 } elseif ($this->get_config('imsunenrol')) {
679 // Unenrol member
680
681 $einstances = $DB->get_records('enrol',
682 array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));
683 foreach ($einstances as $einstance) {
684 // Unenrol the user from all imsenterprise enrolment instances
685 $this->unenrol_user($einstance, $memberstoreobj->userid);
8c25f6f2 686 }
26ba0912 687
df997f84
PS
688 $membersuntally++;
689 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
26ba0912 690 }
2dcccb0f 691
8c25f6f2 692 }
8c25f6f2 693 }
26ba0912 694 $this->log_line("Added $memberstally users to course $ship->coursecode");
695 if($membersuntally > 0){
696 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
8c25f6f2 697 }
698 }
699} // End process_membership_tag()
700
701/**
702* Process the properties tag. The only data from this element
703* that is relevant is whether a <target> is specified.
704* @param string $tagconents The raw contents of the XML element
705*/
706function process_properties_tag($tagcontents){
fa36ca3d 707 $imsrestricttarget = $this->get_config('imsrestricttarget');
2dcccb0f 708
fa36ca3d
AB
709 if ($imsrestricttarget) {
710 if(!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))){
711 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
8c25f6f2 712 $this->continueprocessing = false;
713 }
714 }
715}
716
717/**
718* Store logging information. This does two things: uses the {@link mtrace()}
2dcccb0f 719* function to print info to screen/STDOUT, and also writes log to a text file
8c25f6f2 720* if a path has been specified.
721* @param string $string Text to write (newline will be added automatically)
722*/
723function log_line($string){
724 mtrace($string);
725 if($this->logfp) {
726 fwrite($this->logfp, $string . "\n");
727 }
728}
729
730/**
731* Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
732*/
733function 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
734 $ret->begin = $ret->end = 0;
735 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
736 // because otherwise the time markers should be ignored (participation should be
737 // allowed outside the period)
738 if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){
739 $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
740 }
741 if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){
742 $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
743 }
744 return $ret;
745} // End decode_timeframe
746
26ba0912 747/**
2dcccb0f 748* Load the role mappings (from the config), so we can easily refer to
26ba0912 749* how an IMS-E role corresponds to a Moodle role
750*/
751function load_role_mappings() {
fa36ca3d 752 require_once('locallib.php');
50c5bef4 753 global $DB;
754
fa36ca3d
AB
755 $imsroles = new imsenterprise_roles();
756 $imsroles = $imsroles->get_imsroles();
757
26ba0912 758 $this->rolemappings = array();
fa36ca3d 759 foreach($imsroles as $imsrolenum=>$imsrolename) {
365bf676 760 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
26ba0912 761 }
762}
763
8c25f6f2 764} // end of class
765
4317f92f 766