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