Commit | Line | Data |
---|---|---|
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 |
30 | defined('MOODLE_INTERNAL') || die(); |
31 | ||
8c25f6f2 | 32 | /* |
33 | ||
34 | Note for programmers: | |
35 | ||
2dcccb0f | 36 | This class uses regular expressions to mine the data file. The main reason is |
8c25f6f2 | 37 | that XML handling changes from PHP 4 to PHP 5, so this should work on both. |
38 | ||
2dcccb0f | 39 | One drawback is that the pattern-matching doesn't (currently) handle XML |
8c25f6f2 | 40 | namespaces - it only copes with a <group> tag if it says <group>, and not |
41 | (for example) <ims:group>. | |
42 | ||
2dcccb0f | 43 | This should also be able to handle VERY LARGE FILES - so the entire IMS file is |
44 | NOT loaded into memory at once. It's handled line-by-line, 'forgetting' tags as | |
8c25f6f2 | 45 | soon as they are processed. |
46 | ||
2dcccb0f | 47 | N.B. The "sourcedid" ID code is translated to Moodle's "idnumber" field, both |
8c25f6f2 | 48 | for users and for courses. |
49 | ||
50 | */ | |
51 | ||
fa36ca3d | 52 | require_once($CFG->dirroot.'/group/lib.php'); |
26ba0912 | 53 | |
54 | ||
fa36ca3d | 55 | class enrol_imsenterprise_plugin extends enrol_plugin { |
8c25f6f2 | 56 | |
fa36ca3d | 57 | var $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 | */ | |
66 | function 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 | */ | |
265 | function 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 | */ | |
278 | function 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 | */ | |
291 | function 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 | */ | |
305 | function 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 | */ | |
423 | function 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 | */ | |
550 | function 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 | */ | |
697 | function 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 | */ | |
714 | function 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 | */ | |
724 | function 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 | */ | |
742 | function 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 |