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