MDL-25983 enrol_imsenterprise Importing the course summary when adding new courses
[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 }
b47316cd 318 if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)){
8c25f6f2 319 $group->description = trim($matches[1]);
320 }
b47316cd
DM
321 if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
322 $group->shortName = trim($matches[1]);
323 }
a4b1ba24
DM
324 if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
325 $group->fulldescription = trim($matches[1]);
326 }
801fdcfd 327 if (preg_match('{<org>.*?<orgunit>(.*?)</orgunit>.*?</org>}is', $tagcontents, $matches)) {
8c25f6f2 328 $group->category = trim($matches[1]);
329 }
2dcccb0f 330
8c25f6f2 331 $recstatus = ($this->get_recstatus($tagcontents, 'group'));
332 //echo "<p>get_recstatus for this group returned $recstatus</p>";
2dcccb0f 333
801fdcfd 334 if (!(strlen($group->coursecode)>0)) {
8c25f6f2 335 $this->log_line('Error at line '.$line.': Unable to find course code in \'group\' element.');
801fdcfd 336 } else {
8c25f6f2 337 // First, truncate the course code if desired
801fdcfd 338 if (intval($truncatecoursecodes)>0) {
fa36ca3d
AB
339 $group->coursecode = ($truncatecoursecodes > 0)
340 ? substr($group->coursecode, 0, intval($truncatecoursecodes))
8c25f6f2 341 : $group->coursecode;
342 }
2dcccb0f 343
8c25f6f2 344 /* -----------Course aliasing is DEACTIVATED until a more general method is in place---------------
2dcccb0f 345
8c25f6f2 346 // Second, look in the course alias table to see if the code should be translated to something else
a5d424df 347 if($aliases = $DB->get_field('enrol_coursealias', 'toids', array('fromid'=>$group->coursecode))){
8c25f6f2 348 $this->log_line("Found alias of course code: Translated $group->coursecode to $aliases");
349 // Alias is allowed to be a comma-separated list, so let's split it
350 $group->coursecode = explode(',', $aliases);
351 }
352 */
2dcccb0f 353
8c25f6f2 354 // For compatibility with the (currently inactive) course aliasing, we need this to be an array
355 $group->coursecode = array($group->coursecode);
2dcccb0f 356
8c25f6f2 357 // Third, check if the course(s) exist
801fdcfd 358 foreach ($group->coursecode as $coursecode) {
8c25f6f2 359 $coursecode = trim($coursecode);
801fdcfd
DM
360 if (!$DB->get_field('course', 'id', array('idnumber'=>$coursecode))) {
361 if (!$createnewcourses) {
20d694ac
DM
362 $this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
363 } else {
b47316cd 364 // Set shortname to description or description to shortname if one is set but not the other.
952a77d1 365 $nodescription = !isset($group->description);
d2861a7b 366 $noshortname = !isset($group->shortName);
952a77d1 367 if ( $nodescription && $noshortname) {
b47316cd
DM
368 // If neither short nor long description are set let if fail
369 $this->log_line("Neither long nor short name are set for $coursecode");
952a77d1 370 } else if ($nodescription) {
b47316cd
DM
371 // If short and ID exist, then give the long short's value, then give short the ID's value
372 $group->description = $group->shortName;
373 $group->shortName = $coursecode;
952a77d1 374 } else if ($noshortname) {
b47316cd
DM
375 // If long and ID exist, then map long to long, then give short the ID's value.
376 $group->shortName = $coursecode;
377 }
20d694ac
DM
378 // Create the (hidden) course(s) if not found
379 $courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults
380 $course = new stdClass();
381 $course->fullname = $group->description;
a4b1ba24
DM
382 $course->shortname = $group->shortName;
383 if (!empty($group->fulldescription)) {
384 $course->summary = format_text($group->fulldescription, FORMAT_HTML);
385 }
20d694ac
DM
386 $course->idnumber = $coursecode;
387 $course->format = $courseconfig->format;
388 $course->visible = $courseconfig->visible;
389 $course->numsections = $courseconfig->numsections;
390 $course->hiddensections = $courseconfig->hiddensections;
391 $course->newsitems = $courseconfig->newsitems;
392 $course->showgrades = $courseconfig->showgrades;
393 $course->showreports = $courseconfig->showreports;
394 $course->maxbytes = $courseconfig->maxbytes;
395 $course->groupmode = $courseconfig->groupmode;
396 $course->groupmodeforce = $courseconfig->groupmodeforce;
397 $course->enablecompletion = $courseconfig->enablecompletion;
398 $course->completionstartonenrol = $courseconfig->completionstartonenrol;
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;
0cbe8111 426
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
20d694ac
DM
437 $section = new stdClass();
438 $section->course = $course->id; // Create a default section.
439 $section->section = 0;
440 $section->summaryformat = FORMAT_HTML;
441 $section->id = $DB->insert_record("course_sections", $section);
2dcccb0f 442
20d694ac 443 add_to_log(SITEID, "course", "new", "view.php?id=$course->id", "$course->fullname (ID $course->id)");
2dcccb0f 444
20d694ac
DM
445 $this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
446 }
801fdcfd 447 } else if ($recstatus==3 && ($courseid = $DB->get_field('course', 'id', array('idnumber'=>$coursecode)))) {
8c25f6f2 448 // If course does exist, but recstatus==3 (delete), then set the course as hidden
50c5bef4 449 $DB->set_field('course', 'visible', '0', array('id'=>$courseid));
8c25f6f2 450 }
451 } // End of foreach(coursecode)
452 }
453} // End process_group_tag()
454
455/**
456* Process the person tag. This defines a Moodle user.
457* @param string $tagconents The raw contents of the XML element
458*/
459function process_person_tag($tagcontents){
50c5bef4 460 global $CFG, $DB;
8c25f6f2 461
fa36ca3d
AB
462 // Get plugin configs
463 $imssourcedidfallback = $this->get_config('imssourcedidfallback');
464 $fixcaseusernames = $this->get_config('fixcaseusernames');
465 $fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
466 $imsdeleteusers = $this->get_config('imsdeleteusers');
467 $createnewusers = $this->get_config('createnewusers');
468
94b9c2e8 469 $person = new stdClass();
8c25f6f2 470 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
471 $person->idnumber = trim($matches[1]);
472 }
473 if(preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)){
474 $person->firstname = trim($matches[1]);
475 }
476 if(preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)){
477 $person->lastname = trim($matches[1]);
478 }
479 if(preg_match('{<userid>(.*?)</userid>}is', $tagcontents, $matches)){
480 $person->username = trim($matches[1]);
22016388 481 }
fa36ca3d 482 if($imssourcedidfallback && trim($person->username)==''){
8c25f6f2 483 // This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied
450a0a7d 484 // NB We don't use an "elseif" because the tag may be supplied-but-empty
8c25f6f2 485 $person->username = $person->idnumber;
486 }
487 if(preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)){
488 $person->email = trim($matches[1]);
489 }
490 if(preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)){
491 $person->url = trim($matches[1]);
492 }
493 if(preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)){
494 $person->city = trim($matches[1]);
495 }
496 if(preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)){
497 $person->country = trim($matches[1]);
498 }
499
500 // Fix case of some of the fields if required
fa36ca3d 501 if($fixcaseusernames && isset($person->username)){
8c25f6f2 502 $person->username = strtolower($person->username);
503 }
fa36ca3d 504 if($fixcasepersonalnames){
8c25f6f2 505 if(isset($person->firstname)){
506 $person->firstname = ucwords(strtolower($person->firstname));
507 }
508 if(isset($person->lastname)){
509 $person->lastname = ucwords(strtolower($person->lastname));
510 }
511 }
512
513 $recstatus = ($this->get_recstatus($tagcontents, 'person'));
514
515
516 // Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on
517 // In the "users" table we can do this by setting deleted=1
518 if($recstatus==3){
2dcccb0f 519
fa36ca3d 520 if($imsdeleteusers){ // If we're allowed to delete user records
8c25f6f2 521 // Make sure their "deleted" field is set to one
50c5bef4 522 $DB->set_field('user', 'deleted', 1, array('username'=>$person->username));
8c25f6f2 523 $this->log_line("Marked user record for user '$person->username' (ID number $person->idnumber) as deleted.");
524 }else{
525 $this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
526 }
2dcccb0f 527
8c25f6f2 528 }else{ // Add or update record
2dcccb0f 529
8c25f6f2 530
531 // If the user exists (matching sourcedid) then we don't need to do anything.
fa36ca3d 532 if(!$DB->get_field('user', 'id', array('idnumber'=>$person->idnumber)) && $createnewusers){
8c25f6f2 533 // If they don't exist and haven't a defined username, we log this as a potential problem.
534 if((!isset($person->username)) || (strlen($person->username)==0)){
535 $this->log_line("Cannot create new user for ID # $person->idnumber - no username listed in IMS data for this person.");
50c5bef4 536 } else if ($DB->get_field('user', 'id', array('username'=>$person->username))){
8c25f6f2 537 // If their idnumber is not registered but their user ID is, then add their idnumber to their record
50c5bef4 538 $DB->set_field('user', 'idnumber', $person->idnumber, array('username'=>$person->username));
539 } else {
2dcccb0f 540
fa36ca3d 541 // If they don't exist and they have a defined username, and $createnewusers == true, we create them.
2dcccb0f 542 $person->lang = 'manual'; //TODO: this needs more work due tu multiauth changes
67e3f5b5 543 $person->auth = $CFG->auth;
8c25f6f2 544 $person->confirmed = 1;
545 $person->timemodified = time();
2dcccb0f 546 $person->mnethostid = $CFG->mnet_localhost_id;
9d97f08e 547 $id = $DB->insert_record('user', $person);
8c25f6f2 548 /*
549 Photo processing is deactivated until we hear from Moodle dev forum about modification to gdlib.
2dcccb0f 550
551 //Antoni Mas. 07/12/2005. If a photo URL is specified then we might want to load
8c25f6f2 552 // it into the user's profile. Beware that this may cause a heavy overhead on the server.
553 if($CFG->enrol_processphoto){
554 if(preg_match('{<photo>.*?<extref>(.*?)</extref>.*?</photo>}is', $tagcontents, $matches)){
555 $person->urlphoto = trim($matches[1]);
556 }
557 //Habilitam el flag que ens indica que el personatge t foto prpia.
2dcccb0f 558 $person->picture = 1;
8c25f6f2 559 //Llibreria creada per nosaltres mateixos.
560 require_once($CFG->dirroot.'/lib/gdlib.php');
edfd6a5e 561 if ($usernew->picture = save_profile_image($id, $person->urlphoto,'user')) { TODO: use process_new_icon() instead
50c5bef4 562 $DB->set_field('user', 'picture', $usernew->picture, array('id'=>$id)); /// Note picture in DB
8c25f6f2 563 }
564 }
565 */
9d97f08e 566 $this->log_line("Created user record for user '$person->username' (ID number $person->idnumber).");
8c25f6f2 567 }
fa36ca3d 568 } elseif ($createnewusers) {
8c25f6f2 569 $this->log_line("User record already exists for user '$person->username' (ID number $person->idnumber).");
2dcccb0f 570
8c25f6f2 571 // Make sure their "deleted" field is set to zero.
50c5bef4 572 $DB->set_field('user', 'deleted', 0, array('idnumber'=>$person->idnumber));
8c25f6f2 573 }else{
574 $this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
575 }
2dcccb0f 576
8c25f6f2 577 } // End of are-we-deleting-or-adding
578
579} // End process_person_tag()
580
581/**
2dcccb0f 582* Process the membership tag. This defines whether the specified Moodle users
8c25f6f2 583* should be added/removed as teachers/students.
584* @param string $tagconents The raw contents of the XML element
585*/
586function process_membership_tag($tagcontents){
fa36ca3d
AB
587 global $DB;
588
589 // Get plugin configs
590 $truncatecoursecodes = $this->get_config('truncatecoursecodes');
591 $imscapitafix = $this->get_config('imscapitafix');
592
26ba0912 593 $memberstally = 0;
594 $membersuntally = 0;
2dcccb0f 595
8c25f6f2 596 // In order to reduce the number of db queries required, group name/id associations are cached in this array:
597 $groupids = array();
2dcccb0f 598
92701024
PS
599 $ship = new stdClass();
600
8c25f6f2 601 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)){
fa36ca3d
AB
602 $ship->coursecode = ($truncatecoursecodes > 0)
603 ? substr(trim($matches[1]), 0, intval($truncatecoursecodes))
8c25f6f2 604 : trim($matches[1]);
50c5bef4 605 $ship->courseid = $DB->get_field('course', 'id', array('idnumber'=>$ship->coursecode));
8c25f6f2 606 }
607 if($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)){
fa36ca3d
AB
608 $courseobj = new stdClass();
609 $courseobj->id = $ship->courseid;
610
8c25f6f2 611 foreach($membermatches as $mmatch){
92701024
PS
612 $member = new stdClass();
613 $memberstoreobj = new stdClass();
8c25f6f2 614 if(preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)){
615 $member->idnumber = trim($matches[1]);
616 }
617 if(preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)){
618 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
fa36ca3d 619 } elseif($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)){
8c25f6f2 620 // The XML that comes out of Capita Student Records seems to contain a misinterpretation of the IMS specification!
621 $member->roletype = trim($matches[1]); // 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides
622 }
623 if(preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)){
624 $member->status = trim($matches[1]); // 1 means active, 0 means inactive - treat this as enrol vs unenrol
625 }
626
627 $recstatus = ($this->get_recstatus($mmatch[1], 'role'));
628 if($recstatus==3){
629 $member->status = 0; // See above - recstatus of 3 (==delete) is treated the same as status of 0
630 //echo "<p>process_membership_tag: unenrolling member due to recstatus of 3</p>";
631 }
632
92701024 633 $timeframe = new stdClass();
8c25f6f2 634 $timeframe->begin = 0;
635 $timeframe->end = 0;
636 if(preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)){
637 $timeframe = $this->decode_timeframe($matches[1]);
638 }
639 if(preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is', $mmatch[1], $matches)){
640 $member->groupname = trim($matches[1]);
641 // The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause
642 }
2dcccb0f 643
55bcef29 644 $rolecontext = context_course::instance($ship->courseid);
26ba0912 645 $rolecontext = $rolecontext->id; // All we really want is the ID
646//$this->log_line("Context instance for course $ship->courseid is...");
647//print_r($rolecontext);
2dcccb0f 648
8c25f6f2 649 // Add or remove this student or teacher to the course...
50c5bef4 650 $memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber'=>$member->idnumber));
8c25f6f2 651 $memberstoreobj->enrol = 'imsenterprise';
652 $memberstoreobj->course = $ship->courseid;
653 $memberstoreobj->time = time();
654 $memberstoreobj->timemodified = time();
26ba0912 655 if($memberstoreobj->userid){
2dcccb0f 656
26ba0912 657 // Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
658 // Zero means this roletype is supposed to be skipped.
659 $moodleroleid = $this->rolemappings[$member->roletype];
fa36ca3d 660 if(!$moodleroleid) {
26ba0912 661 $this->log_line("SKIPPING role $member->roletype for $memberstoreobj->userid ($member->idnumber) in course $memberstoreobj->course");
662 continue;
663 }
2dcccb0f 664
fa36ca3d
AB
665 if(intval($member->status) == 1) {
666 // Enrol the member
26ba0912 667
fa36ca3d
AB
668 $einstance = $DB->get_record('enrol',
669 array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));
670 if (empty($einstance)) {
671 // Only add an enrol instance to the course if non-existent
672 $enrolid = $this->add_instance($courseobj);
673 $einstance = $DB->get_record('enrol', array('id' => $enrolid));
674 }
26ba0912 675
fa36ca3d
AB
676 $this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);
677
678 $this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) to role $member->roletype in course $memberstoreobj->course");
679 $memberstally++;
680
681 // At this point we can also ensure the group membership is recorded if present
682 if(isset($member->groupname)){
683 // Create the group if it doesn't exist - either way, make sure we know the group ID
684 if(isset($groupids[$member->groupname])) {
685 $member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available
686 } else {
fb57247e 687 if($groupid = $DB->get_field('groups', 'id', array('courseid'=>$ship->courseid, 'name'=>$member->groupname))){
fa36ca3d
AB
688 $member->groupid = $groupid;
689 $groupids[$member->groupname] = $groupid; // Store ID in cache
690 } else {
691 // Attempt to create the group
92701024 692 $group = new stdClass();
fa36ca3d
AB
693 $group->name = $member->groupname;
694 $group->courseid = $ship->courseid;
695 $group->timecreated = time();
696 $group->timemodified = time();
697 $groupid = $DB->insert_record('groups', $group);
698 $this->log_line('Added a new group for this course: '.$group->name);
699 $groupids[$member->groupname] = $groupid; // Store ID in cache
700 $member->groupid = $groupid;
26ba0912 701 }
fa36ca3d
AB
702 }
703 // Add the user-to-group association if it doesn't already exist
704 if($member->groupid) {
705 groups_add_member($member->groupid, $memberstoreobj->userid);
706 }
707 } // End of group-enrolment (from member.role.extension.cohort tag)
708
709 } elseif ($this->get_config('imsunenrol')) {
710 // Unenrol member
711
712 $einstances = $DB->get_records('enrol',
713 array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));
714 foreach ($einstances as $einstance) {
715 // Unenrol the user from all imsenterprise enrolment instances
716 $this->unenrol_user($einstance, $memberstoreobj->userid);
8c25f6f2 717 }
26ba0912 718
df997f84
PS
719 $membersuntally++;
720 $this->log_line("Unenrolled $member->idnumber from role $moodleroleid in course");
26ba0912 721 }
2dcccb0f 722
8c25f6f2 723 }
8c25f6f2 724 }
26ba0912 725 $this->log_line("Added $memberstally users to course $ship->coursecode");
726 if($membersuntally > 0){
727 $this->log_line("Removed $membersuntally users from course $ship->coursecode");
8c25f6f2 728 }
729 }
730} // End process_membership_tag()
731
732/**
733* Process the properties tag. The only data from this element
734* that is relevant is whether a <target> is specified.
735* @param string $tagconents The raw contents of the XML element
736*/
737function process_properties_tag($tagcontents){
fa36ca3d 738 $imsrestricttarget = $this->get_config('imsrestricttarget');
2dcccb0f 739
fa36ca3d
AB
740 if ($imsrestricttarget) {
741 if(!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))){
742 $this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
8c25f6f2 743 $this->continueprocessing = false;
744 }
745 }
746}
747
748/**
749* Store logging information. This does two things: uses the {@link mtrace()}
2dcccb0f 750* function to print info to screen/STDOUT, and also writes log to a text file
8c25f6f2 751* if a path has been specified.
752* @param string $string Text to write (newline will be added automatically)
753*/
754function log_line($string){
755 mtrace($string);
756 if($this->logfp) {
757 fwrite($this->logfp, $string . "\n");
758 }
759}
760
761/**
762* Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
763*/
764function 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 765 $ret = new stdClass();
8c25f6f2 766 $ret->begin = $ret->end = 0;
767 // Explanatory note: The matching will ONLY match if the attribute restrict="1"
768 // because otherwise the time markers should be ignored (participation should be
769 // allowed outside the period)
770 if(preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)){
771 $ret->begin = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
772 }
773 if(preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)){
774 $ret->end = mktime(0,0,0, $matches[2], $matches[3], $matches[1]);
775 }
776 return $ret;
777} // End decode_timeframe
778
26ba0912 779/**
2dcccb0f 780* Load the role mappings (from the config), so we can easily refer to
26ba0912 781* how an IMS-E role corresponds to a Moodle role
782*/
783function load_role_mappings() {
fa36ca3d 784 require_once('locallib.php');
50c5bef4 785 global $DB;
786
fa36ca3d
AB
787 $imsroles = new imsenterprise_roles();
788 $imsroles = $imsroles->get_imsroles();
789
26ba0912 790 $this->rolemappings = array();
fa36ca3d 791 foreach($imsroles as $imsrolenum=>$imsrolename) {
365bf676 792 $this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
26ba0912 793 }
794}
795
8c25f6f2 796} // end of class
797
4317f92f 798