Merge branch 'MDL-39653-master' of git://github.com/danpoltawski/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).
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
75e19bf5 88 $fileisnew = false;
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();
66dad719 100 // Make sure we understand how to map the IMS-E course names to Moodle course names.
a65c11b0 101 $this->load_course_mappings();
8c25f6f2 102
103 $md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron
104 $filemtime = filemtime($filename);
2dcccb0f 105
8c25f6f2 106 // Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
107 // This is so we avoid wasting the server's efforts processing a file unnecessarily
fa36ca3d 108 if(empty($prev_path) || ($filename != $prev_path)) {
8c25f6f2 109 $fileisnew = true;
fa36ca3d 110 } elseif(isset($prev_time) && ($filemtime <= $prev_time)) {
8c25f6f2 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 $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
75e19bf5 219 if (!empty($mailadmins) && $fileisnew) {
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';
ba4612eb 236 $eventdata->component = 'enrol_imsenterprise';
fa36ca3d 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 }
b1cabb40
AS
318
319 if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
66dad719 320 $group->long = trim($matches[1]);
8c25f6f2 321 }
b47316cd 322 if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
66dad719 323 $group->short = trim($matches[1]);
b47316cd 324 }
a4b1ba24 325 if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
b1cabb40 326 $group->full = trim($matches[1]);
a4b1ba24 327 }
b1cabb40 328
801fdcfd 329 if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
8c25f6f2 330 $group->category = trim($matches[1]);
331 }
2dcccb0f 332
8c25f6f2 333 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
334 //echo "<p>get_recstatus for this group returned $recstatus</p>";
2dcccb0f 335
801fdcfd 336 if (!(strlen($group->coursecode)>0)) {
8c25f6f2 337 $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.');
801fdcfd 338 } else {
8c25f6f2 339 // First, truncate the course code if desired
801fdcfd 340 if (intval($truncatecoursecodes)>0) {
fa36ca3d
AB
341 $group->coursecode = ($truncatecoursecodes > 0)
342 ? substr($group->coursecode, 0, intval($truncatecoursecodes))
8c25f6f2 343 : $group->coursecode;
344 }
2dcccb0f 345
8c25f6f2 346 /* -----------Course aliasing is DEACTIVATED until a more general method is in place---------------
2dcccb0f 347
8c25f6f2 348 // Second, look in the course alias table to see if the code should be translated to something else
a5d424df 349 if($aliases = $DB->get_field('enrol_coursealias', 'toids', array('fromid'=>$group->coursecode))){
8c25f6f2 350 $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases");
351 // Alias is allowed to be a comma-separated list, so let's split it
352 $group->coursecode = explode(',', $aliases);
353 }
354 */
2dcccb0f 355
8c25f6f2 356 // For compatibility with the (currently inactive) course aliasing, we need this to be an array
357 $group->coursecode = array($group->coursecode);
2dcccb0f 358
8c25f6f2 359 // Third, check if the course(s) exist
801fdcfd 360 foreach ($group->coursecode as $coursecode) {
8c25f6f2 361 $coursecode = trim($coursecode);
801fdcfd
DM
362 if (!$DB->get_field('course', 'id', array('idnumber'=>$coursecode))) {
363 if (!$createnewcourses) {
20d694ac
DM
364 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
365 } else {
a65c11b0 366
20d694ac
DM
367 // Create the (hidden) course(s) if not found
368 $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults
66dad719
DM
369
370 // New course.
20d694ac 371 $course = new stdClass();
66dad719
DM
372 foreach ($this->coursemappings as $courseattr => $imsname) {
373
374 if ($imsname == 'ignore') {
375 continue;
376 }
377
378 // Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
379 if ($imsname == 'coursecode') {
380 $course->{$courseattr} = $coursecode;
381 } else if (!empty($group->{$imsname})) {
382 $course->{$courseattr} = $group->{$imsname};
383 } else {
384 $this->log_line('No ' . $imsname . ' description tag found for ' . $coursecode . ' coursecode, using ' . $coursecode . ' instead');
385 $course->{$courseattr} = $coursecode;
386 }
a4b1ba24 387 }
66dad719 388
20d694ac
DM
389 $course->idnumber = $coursecode;
390 $course->format = $courseconfig->format;
391 $course->visible = $courseconfig->visible;
20d694ac
DM
392 $course->newsitems = $courseconfig->newsitems;
393 $course->showgrades = $courseconfig->showgrades;
394 $course->showreports = $courseconfig->showreports;
395 $course->maxbytes = $courseconfig->maxbytes;
396 $course->groupmode = $courseconfig->groupmode;
397 $course->groupmodeforce = $courseconfig->groupmodeforce;
398 $course->enablecompletion = $courseconfig->enablecompletion;
20d694ac 399 // Insert default names for teachers/students, from the current language
20d694ac
DM
400
401 // Handle course categorisation (taken from the group.org.orgunit field if present)
801fdcfd 402 if (strlen($group->category)>0) {
20d694ac 403 // If the category is defined and exists in Moodle, we want to store it in that one
801fdcfd 404 if ($catid = $DB->get_field('course_categories', 'id', array('name'=>$group->category))) {
20d694ac 405 $course->category = $catid;
801fdcfd 406 } else if ($createnewcategories) {
20d694ac
DM
407 // Else if we're allowed to create new categories, let's create this one
408 $newcat = new stdClass();
409 $newcat->name = $group->category;
410 $newcat->visible = 0;
411 $catid = $DB->insert_record('course_categories', $newcat);
412 $course->category = $catid;
413 $this->log_line("Created new (hidden) category, #$catid: $newcat->name");
801fdcfd 414 } else {
20d694ac
DM
415 // If not found and not allowed to create, stick with default
416 $this->log_line('Category '.$group->category.' not found in Moodle database, so using default category instead.');
417 $course->category = 1;
418 }
801fdcfd 419 } else {
8c25f6f2 420 $course->category = 1;
421 }
20d694ac
DM
422 $course->timecreated = time();
423 $course->startdate = time();
424 // Choose a sort order that puts us at the start of the list!
425 $course->sortorder = 0;
20d694ac 426 $courseid = $DB->insert_record('course', $course);
2dcccb0f 427
c72c6356
DM
428 // Setup default enrolment plugins
429 $course->id = $courseid;
430 enrol_course_updated(true, $course, null);
431
20d694ac
DM
432 // Setup the blocks
433 $course = $DB->get_record('course', array('id' => $courseid));
434 blocks_add_default_course_blocks($course);
2dcccb0f 435
4ede27b2
MG
436 // Create default 0-section
437 course_create_sections_if_missing($course, 0);
2dcccb0f 438
20d694ac 439 add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
2dcccb0f 440
20d694ac
DM
441 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
442 }
801fdcfd 443 } else if ($recstatus==3 && ($courseid = $DB->get_field('course', 'id', array('idnumber'=>$coursecode)))) {
8c25f6f2 444 // If course does exist, but recstatus==3 (delete), then set the course as hidden
50c5bef4 445 $DB->set_field('course', 'visible', '0', array('id'=>$courseid));
8c25f6f2 446 }
447 } // End of foreach(coursecode)
448 }
449} // End process_group_tag()
450
451/**
452* Process the person tag. This defines a Moodle user.
453* @param string $tagconents The raw contents of the XML element
454*/
455function process_person_tag($tagcontents){
50c5bef4 456 global $CFG, $DB;
8c25f6f2 457
fa36ca3d
AB
458 // Get plugin configs
459 $imssourcedidfallback = $this->get_config('imssourcedidfallback');
460 $fixcaseusernames = $this->get_config('fixcaseusernames');
461 $fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
462 $imsdeleteusers = $this->get_config('imsdeleteusers');
463 $createnewusers = $this->get_config('createnewusers');
464
94b9c2e8 465 $person = new stdClass();
8c25f6f2 466 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
467 $person->idnumber = trim($matches[1]);
468 }
469 if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){
470 $person->firstname = trim($matches[1]);
471 }
472 if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){
473 $person->lastname = trim($matches[1]);
474 }
475 if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){
476 $person->username = trim($matches[1]);
22016388 477 }
fa36ca3d 478 if($imssourcedidfallback && trim($person->username)==''){
8c25f6f2 479 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
450a0a7d 480 // NB We don't use an "elseif" because the tag may be supplied-but-empty
8c25f6f2 481 $person->username = $person->idnumber;
482 }
483 if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){
484 $person->email = trim($matches[1]);
485 }
486 if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){
487 $person->url = trim($matches[1]);
488 }
489 if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){
490 $person->city = trim($matches[1]);
491 }
492 if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){
493 $person->country = trim($matches[1]);
494 }
495
496 // Fix case of some of the fields if required
fa36ca3d 497 if($fixcaseusernames && isset($person->username)){
8c25f6f2 498 $person->username = strtolower($person->username);
499 }
fa36ca3d 500 if($fixcasepersonalnames){
8c25f6f2 501 if(isset($person->firstname)){
502 $person->firstname = ucwords(strtolower($person->firstname));
503 }
504 if(isset($person->lastname)){
505 $person->lastname = ucwords(strtolower($person->lastname));
506 }
507 }
508
509 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
510
511
512 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on
8c25f6f2 513 if($recstatus==3){
2dcccb0f 514
fa36ca3d 515 if($imsdeleteusers){ // If we're allowed to delete user records
29daca77
PS
516 // Do not dare to hack the user.deleted field directly in database!!!
517 if ($user = $DB->get_record('user', array('username'=>$person->username, 'mnethostid'=>$CFG->mnet_localhost_id, 'deleted'=>0))) {
518 if (delete_user($user)) {
519 $this->log_line("Deleted user '$person->username' (ID number $person->idnumber).");
520 } else {
521 $this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");
522 }
523 } else {
524 $this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");
525 }
8c25f6f2 526 }else{
527 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
528 }
2dcccb0f 529
8c25f6f2 530 }else{ // Add or update record
2dcccb0f 531
8c25f6f2 532
533 // If the user exists (matching sourcedid) then we don't need to do anything.
fa36ca3d 534 if(!$DB->get_field('user', 'id', array('idnumber'=>$person->idnumber)) && $createnewusers){
8c25f6f2 535 // If they don't exist and haven't a defined username, we log this as a potential problem.
536 if((!isset($person->username)) || (strlen($person->username)==0)){
537 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person.");
50c5bef4 538 } else if ($DB->get_field('user', 'id', array('username'=>$person->username))){
8c25f6f2 539 // If their idnumber is not registered but their user ID is, then add their idnumber to their record
50c5bef4 540 $DB->set_field('user', 'idnumber', $person->idnumber, array('username'=>$person->username));
541 } else {
2dcccb0f 542
fa36ca3d 543 // If they don't exist and they have a defined username, and $createnewusers == true, we create them.
29daca77
PS
544 $person->lang = $CFG->lang;
545 $auth = explode(',', $CFG->auth); //TODO: this needs more work due tu multiauth changes, use first auth for now
546 $auth = reset($auth);
547 $person->auth = $auth;
8c25f6f2 548 $person->confirmed = 1;
549 $person->timemodified = time();
2dcccb0f 550 $person->mnethostid = $CFG->mnet_localhost_id;
9d97f08e 551 $id = $DB->insert_record('user', $person);
8c25f6f2 552 /*
553 Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib.
2dcccb0f 554
555 //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load
8c25f6f2 556 // it into the user's profile. Beware that this may cause a heavy overhead on the server.
557 if($CFG->enrol_processphoto){
558 if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){
559 $person->urlphoto = trim($matches[1]);
560 }
561 //Habilitam el flag que ens indica que el personatge t foto prpia.
2dcccb0f 562 $person->picture = 1;
8c25f6f2 563 //Llibreria creada per nosaltres mateixos.
564 require_once($CFG->dirroot.'/lib/gdlib.php');
edfd6a5e 565 if ($usernew->picture = save_profile_image($id, $person->urlphoto,'user')) { TODO: use process_new_icon() instead
50c5bef4 566 $DB->set_field('user', 'picture', $usernew->picture, array('id'=>$id)); /// Note picture in DB
8c25f6f2 567 }
568 }
569 */
9d97f08e 570 $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber).");
8c25f6f2 571 }
fa36ca3d 572 } elseif ($createnewusers) {
8c25f6f2 573 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
2dcccb0f 574
29daca77
PS
575 // It is totally wrong to mess with deleted users flag directly in database!!!
576 // There is no official way to undelete user, sorry..
8c25f6f2 577 }else{
578 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
579 }
2dcccb0f 580
8c25f6f2 581 } // End of are-we-deleting-or-adding
582
583} // End process_person_tag()
584
585/**
2dcccb0f 586* Process the membership tag. This defines whether the specified Moodle users
8c25f6f2 587* should be added/removed as teachers/students.
588* @param string $tagconents The raw contents of the XML element
589*/
590function process_membership_tag($tagcontents){
fa36ca3d
AB
591 global $DB;
592
593 // Get plugin configs
594 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
595 $imscapitafix = $this->get_config('imscapitafix');
596
26ba0912 597 $memberstally = 0;
598 $membersuntally = 0;
2dcccb0f 599
8c25f6f2 600 // In order to reduce the number of db queries required, group name/id associations are cached in this array:
601 $groupids = array();
2dcccb0f 602
92701024
PS
603 $ship = new stdClass();
604
8c25f6f2 605 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
fa36ca3d
AB
606 $ship->coursecode = ($truncatecoursecodes > 0)
607 ? substr(trim($matches[1]), 0, intval($truncatecoursecodes))
8c25f6f2 608 : trim($matches[1]);
50c5bef4 609 $ship->courseid = $DB->get_field('course', 'id', array('idnumber'=>$ship->coursecode));
8c25f6f2 610 }
611 if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){
fa36ca3d
AB
612 $courseobj = new stdClass();
613 $courseobj->id = $ship->courseid;
614
8c25f6f2 615 foreach($membermatches as $mmatch){
92701024
PS
616 $member = new stdClass();
617 $memberstoreobj = new stdClass();
8c25f6f2 618 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){
619 $member->idnumber = trim($matches[1]);
620 }
621 if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){
622 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
fa36ca3d 623 } elseif($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){
8c25f6f2 624 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification!
625 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
626 }
627 if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){
628 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol
629 }
630
631 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
632 if($recstatus==3){
633 $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0
634 //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>";
635 }
636
92701024 637 $timeframe = new stdClass();
8c25f6f2 638 $timeframe->begin = 0;
639 $timeframe->end = 0;
640 if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){
641 $timeframe = $this->decode_timeframe($matches[1]);
642 }
643 if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){
644 $member->groupname = trim($matches[1]);
645 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause
646 }
2dcccb0f 647
55bcef29 648 $rolecontext = context_course::instance($ship->courseid);
26ba0912 649 $rolecontext = $rolecontext->id; // All we really want is the ID
650//$this->log_line("Context instance for course $ship->courseid is...");
651//print_r($rolecontext);
2dcccb0f 652
8c25f6f2 653 // Add or remove this student or teacher to the course...
50c5bef4 654 $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber'=>$member->idnumber));
8c25f6f2 655 $memberstoreobj->enrol = 'imsenterprise';
656 $memberstoreobj->course = $ship->courseid;
657 $memberstoreobj->time = time();
658 $memberstoreobj->timemodified = time();
26ba0912 659 if($memberstoreobj->userid){
2dcccb0f 660
26ba0912 661 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
662 // Zero means this roletype is supposed to be skipped.
663 $moodleroleid = $this->rolemappings[$member->roletype];
fa36ca3d 664 if(!$moodleroleid) {
26ba0912 665 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course");
666 continue;
667 }
2dcccb0f 668
fa36ca3d
AB
669 if(intval($member->status) == 1) {
670 // Enrol the member
26ba0912 671
fa36ca3d
AB
672 $einstance = $DB->get_record('enrol',
673 array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));
674 if (empty($einstance)) {
675 // Only add an enrol instance to the course if non-existent
676 $enrolid = $this->add_instance($courseobj);
677 $einstance = $DB->get_record('enrol', array('id' => $enrolid));
678 }
26ba0912 679
fa36ca3d
AB
680 $this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);
681
682 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
683 $memberstally++;
684
685 // At this point we can also ensure the group membership is recorded if present
686 if(isset($member->groupname)){
687 // Create the group if it doesn't exist - either way, make sure we know the group ID
688 if(isset($groupids[$member->groupname])) {
689 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available
690 } else {
fb57247e 691 if($groupid = $DB->get_field('groups', 'id', array('courseid'=>$ship->courseid, 'name'=>$member->groupname))){
fa36ca3d
AB
692 $member->groupid = $groupid;
693 $groupids[$member->groupname] = $groupid; // Store ID in cache
694 } else {
695 // Attempt to create the group
92701024 696 $group = new stdClass();
fa36ca3d
AB
697 $group->name = $member->groupname;
698 $group->courseid = $ship->courseid;
699 $group->timecreated = time();
700 $group->timemodified = time();
701 $groupid = $DB->insert_record('groups', $group);
702 $this->log_line('Added a new group for this course: '.$group->name);
703 $groupids[$member->groupname] = $groupid; // Store ID in cache
704 $member->groupid = $groupid;
e17dbeeb
SH
705 // Invalidate the course group data cache just in case.
706 cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($ship->courseid));
26ba0912 707 }
fa36ca3d
AB
708 }
709 // Add the user-to-group association if it doesn't already exist
710 if($member->groupid) {
1d1917ae 711 groups_add_member($member->groupid, $memberstoreobj->userid,
712 'enrol_imsenterprise', $einstance->id);
fa36ca3d
AB
713 }
714 } // End of group-enrolment (from member.role.extension.cohort tag)
715
716 } elseif ($this->get_config('imsunenrol')) {
717 // Unenrol member
718
719 $einstances = $DB->get_records('enrol',
720 array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));
721 foreach ($einstances as $einstance) {
722 // Unenrol the user from all imsenterprise enrolment instances
723 $this->unenrol_user($einstance, $memberstoreobj->userid);
8c25f6f2 724 }
26ba0912 725
df997f84
PS
726 $membersuntally++;
727 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
26ba0912 728 }
2dcccb0f 729
8c25f6f2 730 }
8c25f6f2 731 }
26ba0912 732 $this->log_line("Added $memberstally users to course $ship->coursecode");
733 if($membersuntally > 0){
734 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
8c25f6f2 735 }
736 }
737} // End process_membership_tag()
738
739/**
740* Process the properties tag. The only data from this element
741* that is relevant is whether a <target> is specified.
742* @param string $tagconents The raw contents of the XML element
743*/
744function process_properties_tag($tagcontents){
fa36ca3d 745 $imsrestricttarget = $this->get_config('imsrestricttarget');
2dcccb0f 746
fa36ca3d
AB
747 if ($imsrestricttarget) {
748 if(!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))){
749 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
8c25f6f2 750 $this->continueprocessing = false;
751 }
752 }
753}
754
755/**
756* Store logging information. This does two things: uses the {@link mtrace()}
2dcccb0f 757* function to print info to screen/STDOUT, and also writes log to a text file
8c25f6f2 758* if a path has been specified.
759* @param string $string Text to write (newline will be added automatically)
760*/
761function log_line($string){
2b8f692f
DM
762
763 if (!PHPUNIT_TEST) {
764 mtrace($string);
765 }
8c25f6f2 766 if($this->logfp) {
767 fwrite($this->logfp, $string . "\n");
768 }
769}
770
771/**
772* Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
773*/
774function 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 775 $ret = new stdClass();
8c25f6f2 776 $ret->begin = $ret->end = 0;
777 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
778 // because otherwise the time markers should be ignored (participation should be
779 // allowed outside the period)
780 if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){
781 $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
782 }
783 if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){
784 $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
785 }
786 return $ret;
787} // End decode_timeframe
788
26ba0912 789/**
2dcccb0f 790* Load the role mappings (from the config), so we can easily refer to
26ba0912 791* how an IMS-E role corresponds to a Moodle role
792*/
793function load_role_mappings() {
fa36ca3d 794 require_once('locallib.php');
50c5bef4 795 global $DB;
796
fa36ca3d
AB
797 $imsroles = new imsenterprise_roles();
798 $imsroles = $imsroles->get_imsroles();
799
26ba0912 800 $this->rolemappings = array();
fa36ca3d 801 foreach($imsroles as $imsrolenum=>$imsrolename) {
365bf676 802 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
26ba0912 803 }
804}
805
a65c11b0
AS
806 /**
807 * Load the name mappings (from the config), so we can easily refer to
66dad719 808 * how an IMS-E course properties corresponds to a Moodle course properties
a65c11b0
AS
809 */
810 function load_course_mappings() {
811 require_once('locallib.php');
812
66dad719
DM
813 $imsnames = new imsenterprise_courses();
814 $courseattrs = $imsnames->get_courseattrs();
a65c11b0
AS
815
816 $this->coursemappings = array();
66dad719
DM
817 foreach($courseattrs as $courseattr) {
818 $this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
a65c11b0
AS
819 }
820 }
821
1d1917ae 822 /**
823 * Called whenever anybody tries (from the normal interface) to remove a group
824 * member which is registered as being created by this component. (Not called
825 * when deleting an entire group or course at once.)
826 * @param int $itemid Item ID that was stored in the group_members entry
827 * @param int $groupid Group ID
828 * @param int $userid User ID being removed from group
829 * @return bool True if the remove is permitted, false to give an error
830 */
831 function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {
832 return false;
833 }
834
a65c11b0 835
8c25f6f2 836} // end of class
837
4317f92f 838