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