1 |
efrain |
1 |
<?php
|
|
|
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/>.
|
|
|
16 |
|
|
|
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).
|
|
|
23 |
* @package enrol_imsenterprise
|
|
|
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
|
|
|
27 |
*/
|
|
|
28 |
|
|
|
29 |
defined('MOODLE_INTERNAL') || die();
|
|
|
30 |
|
|
|
31 |
require_once($CFG->dirroot.'/group/lib.php');
|
|
|
32 |
|
|
|
33 |
/**
|
|
|
34 |
* IMS Enterprise file enrolment plugin.
|
|
|
35 |
*
|
|
|
36 |
* @copyright 2010 Eugene Venter
|
|
|
37 |
* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
|
|
|
38 |
*/
|
|
|
39 |
class enrol_imsenterprise_plugin extends enrol_plugin {
|
|
|
40 |
|
|
|
41 |
/**
|
|
|
42 |
* @var IMSENTERPRISE_ADD imsenterprise add action.
|
|
|
43 |
*/
|
|
|
44 |
const IMSENTERPRISE_ADD = 1;
|
|
|
45 |
|
|
|
46 |
/**
|
|
|
47 |
* @var IMSENTERPRISE_UPDATE imsenterprise update action.
|
|
|
48 |
*/
|
|
|
49 |
const IMSENTERPRISE_UPDATE = 2;
|
|
|
50 |
|
|
|
51 |
/**
|
|
|
52 |
* @var IMSENTERPRISE_DELETE imsenterprise delete action.
|
|
|
53 |
*/
|
|
|
54 |
const IMSENTERPRISE_DELETE = 3;
|
|
|
55 |
|
|
|
56 |
/**
|
|
|
57 |
* @var $logfp resource file pointer for writing log data to.
|
|
|
58 |
*/
|
|
|
59 |
protected $logfp;
|
|
|
60 |
|
|
|
61 |
/**
|
|
|
62 |
* @var $continueprocessing bool flag to determine if processing should continue.
|
|
|
63 |
*/
|
|
|
64 |
protected $continueprocessing;
|
|
|
65 |
|
|
|
66 |
/**
|
|
|
67 |
* @var $xmlcache string cache of xml lines.
|
|
|
68 |
*/
|
|
|
69 |
protected $xmlcache;
|
|
|
70 |
|
|
|
71 |
/**
|
|
|
72 |
* @var $coursemappings array of mappings between IMS data fields and moodle course fields.
|
|
|
73 |
*/
|
|
|
74 |
protected $coursemappings;
|
|
|
75 |
|
|
|
76 |
/**
|
|
|
77 |
* @var $rolemappings array of mappings between IMS roles and moodle roles.
|
|
|
78 |
*/
|
|
|
79 |
protected $rolemappings;
|
|
|
80 |
|
|
|
81 |
/**
|
|
|
82 |
* @var $defaultcategoryid id of default category.
|
|
|
83 |
*/
|
|
|
84 |
protected $defaultcategoryid;
|
|
|
85 |
|
|
|
86 |
/**
|
|
|
87 |
* Read in an IMS Enterprise file.
|
|
|
88 |
* Originally designed to handle v1.1 files but should be able to handle
|
|
|
89 |
* earlier types as well, I believe.
|
|
|
90 |
* This cron feature has been converted to a scheduled task and it can now be scheduled
|
|
|
91 |
* from the UI.
|
|
|
92 |
*/
|
|
|
93 |
public function cron() {
|
|
|
94 |
global $CFG;
|
|
|
95 |
|
|
|
96 |
// Get configs.
|
|
|
97 |
$imsfilelocation = $this->get_config('imsfilelocation');
|
|
|
98 |
$logtolocation = $this->get_config('logtolocation');
|
|
|
99 |
$mailadmins = $this->get_config('mailadmins');
|
|
|
100 |
$prevtime = $this->get_config('prev_time');
|
|
|
101 |
$prevmd5 = $this->get_config('prev_md5');
|
|
|
102 |
$prevpath = $this->get_config('prev_path');
|
|
|
103 |
|
|
|
104 |
if (empty($imsfilelocation)) {
|
|
|
105 |
$filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location.
|
|
|
106 |
} else {
|
|
|
107 |
$filename = $imsfilelocation;
|
|
|
108 |
}
|
|
|
109 |
|
|
|
110 |
$this->logfp = false;
|
|
|
111 |
if (!empty($logtolocation)) {
|
|
|
112 |
$this->logfp = fopen($logtolocation, 'a');
|
|
|
113 |
}
|
|
|
114 |
|
|
|
115 |
$this->defaultcategoryid = null;
|
|
|
116 |
|
|
|
117 |
$fileisnew = false;
|
|
|
118 |
if ( file_exists($filename) ) {
|
|
|
119 |
core_php_time_limit::raise();
|
|
|
120 |
$starttime = time();
|
|
|
121 |
|
|
|
122 |
$this->log_line('----------------------------------------------------------------------');
|
|
|
123 |
$this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));
|
|
|
124 |
$this->log_line('Found file '.$filename);
|
|
|
125 |
$this->xmlcache = '';
|
|
|
126 |
|
|
|
127 |
$categoryseparator = trim($this->get_config('categoryseparator'));
|
|
|
128 |
$categoryidnumber = $this->get_config('categoryidnumber');
|
|
|
129 |
|
|
|
130 |
// Make sure we understand how to map the IMS-E roles to Moodle roles.
|
|
|
131 |
$this->load_role_mappings();
|
|
|
132 |
// Make sure we understand how to map the IMS-E course names to Moodle course names.
|
|
|
133 |
$this->load_course_mappings();
|
|
|
134 |
|
|
|
135 |
$md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron.
|
|
|
136 |
$filemtime = filemtime($filename);
|
|
|
137 |
|
|
|
138 |
// Decide if we want to process the file (based on filepath, modification time, and MD5 hash)
|
|
|
139 |
// This is so we avoid wasting the server's efforts processing a file unnecessarily.
|
|
|
140 |
if ($categoryidnumber && empty($categoryseparator)) {
|
|
|
141 |
$this->log_line('Category idnumber is enabled but category separator is not defined - skipping processing.');
|
|
|
142 |
} else if (empty($prevpath) || ($filename != $prevpath)) {
|
|
|
143 |
$fileisnew = true;
|
|
|
144 |
} else if (isset($prevtime) && ($filemtime <= $prevtime)) {
|
|
|
145 |
$this->log_line('File modification time is not more recent than last update - skipping processing.');
|
|
|
146 |
} else if (isset($prevmd5) && ($md5 == $prevmd5)) {
|
|
|
147 |
$this->log_line('File MD5 hash is same as on last update - skipping processing.');
|
|
|
148 |
} else {
|
|
|
149 |
$fileisnew = true; // Let's process it!
|
|
|
150 |
}
|
|
|
151 |
|
|
|
152 |
if ($fileisnew) {
|
|
|
153 |
|
|
|
154 |
// The <properties> tag is allowed to halt processing if we're demanding a matching target.
|
|
|
155 |
$this->continueprocessing = true;
|
|
|
156 |
|
|
|
157 |
// Run through the file and process the group/person entries.
|
|
|
158 |
if (($fh = fopen($filename, "r")) != false) {
|
|
|
159 |
|
|
|
160 |
$line = 0;
|
|
|
161 |
while ((!feof($fh)) && $this->continueprocessing) {
|
|
|
162 |
|
|
|
163 |
$line++;
|
|
|
164 |
$curline = fgets($fh);
|
|
|
165 |
$this->xmlcache .= $curline; // Add a line onto the XML cache.
|
|
|
166 |
|
|
|
167 |
while (true) {
|
|
|
168 |
// If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.
|
|
|
169 |
// Must always make sure to remove tags from cache so they don't clog up our memory.
|
|
|
170 |
if ($tagcontents = $this->full_tag_found_in_cache('group', $curline)) {
|
|
|
171 |
$this->process_group_tag($tagcontents);
|
|
|
172 |
$this->remove_tag_from_cache('group');
|
|
|
173 |
} else if ($tagcontents = $this->full_tag_found_in_cache('person', $curline)) {
|
|
|
174 |
$this->process_person_tag($tagcontents);
|
|
|
175 |
$this->remove_tag_from_cache('person');
|
|
|
176 |
} else if ($tagcontents = $this->full_tag_found_in_cache('membership', $curline)) {
|
|
|
177 |
$this->process_membership_tag($tagcontents);
|
|
|
178 |
$this->remove_tag_from_cache('membership');
|
|
|
179 |
} else if ($tagcontents = $this->full_tag_found_in_cache('comments', $curline)) {
|
|
|
180 |
$this->remove_tag_from_cache('comments');
|
|
|
181 |
} else if ($tagcontents = $this->full_tag_found_in_cache('properties', $curline)) {
|
|
|
182 |
$this->process_properties_tag($tagcontents);
|
|
|
183 |
$this->remove_tag_from_cache('properties');
|
|
|
184 |
} else {
|
|
|
185 |
break;
|
|
|
186 |
}
|
|
|
187 |
}
|
|
|
188 |
}
|
|
|
189 |
fclose($fh);
|
|
|
190 |
fix_course_sortorder();
|
|
|
191 |
}
|
|
|
192 |
|
|
|
193 |
$timeelapsed = time() - $starttime;
|
|
|
194 |
$this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');
|
|
|
195 |
|
|
|
196 |
}
|
|
|
197 |
|
|
|
198 |
// These variables are stored so we can compare them against the IMS file, next time round.
|
|
|
199 |
$this->set_config('prev_time', $filemtime);
|
|
|
200 |
$this->set_config('prev_md5', $md5);
|
|
|
201 |
$this->set_config('prev_path', $filename);
|
|
|
202 |
|
|
|
203 |
} else {
|
|
|
204 |
$this->log_line('File not found: '.$filename);
|
|
|
205 |
}
|
|
|
206 |
|
|
|
207 |
if (!empty($mailadmins) && $fileisnew) {
|
|
|
208 |
$timeelapsed = isset($timeelapsed) ? $timeelapsed : 0;
|
|
|
209 |
$msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";
|
|
|
210 |
if (!empty($logtolocation)) {
|
|
|
211 |
if ($this->logfp) {
|
|
|
212 |
$msg .= "Log data has been written to:\n";
|
|
|
213 |
$msg .= "$logtolocation\n";
|
|
|
214 |
$msg .= "(Log file size: ".ceil(filesize($logtolocation) / 1024)."Kb)\n\n";
|
|
|
215 |
} else {
|
|
|
216 |
$msg .= "The log file appears not to have been successfully written.\n";
|
|
|
217 |
$msg .= "Check that the file is writeable by the server:\n";
|
|
|
218 |
$msg .= "$logtolocation\n\n";
|
|
|
219 |
}
|
|
|
220 |
} else {
|
|
|
221 |
$msg .= "Logging is currently not active.";
|
|
|
222 |
}
|
|
|
223 |
|
|
|
224 |
$eventdata = new \core\message\message();
|
|
|
225 |
$eventdata->courseid = SITEID;
|
|
|
226 |
$eventdata->modulename = 'moodle';
|
|
|
227 |
$eventdata->component = 'enrol_imsenterprise';
|
|
|
228 |
$eventdata->name = 'imsenterprise_enrolment';
|
|
|
229 |
$eventdata->userfrom = get_admin();
|
|
|
230 |
$eventdata->userto = get_admin();
|
|
|
231 |
$eventdata->subject = "Moodle IMS Enterprise enrolment notification";
|
|
|
232 |
$eventdata->fullmessage = $msg;
|
|
|
233 |
$eventdata->fullmessageformat = FORMAT_PLAIN;
|
|
|
234 |
$eventdata->fullmessagehtml = '';
|
|
|
235 |
$eventdata->smallmessage = '';
|
|
|
236 |
message_send($eventdata);
|
|
|
237 |
|
|
|
238 |
$this->log_line('Notification email sent to administrator.');
|
|
|
239 |
|
|
|
240 |
}
|
|
|
241 |
|
|
|
242 |
if ($this->logfp) {
|
|
|
243 |
fclose($this->logfp);
|
|
|
244 |
}
|
|
|
245 |
|
|
|
246 |
}
|
|
|
247 |
|
|
|
248 |
/**
|
|
|
249 |
* Check if a complete tag is found in the cached data, which usually happens
|
|
|
250 |
* when the end of the tag has only just been loaded into the cache.
|
|
|
251 |
*
|
|
|
252 |
* @param string $tagname Name of tag to look for
|
|
|
253 |
* @param string $latestline The very last line in the cache (used for speeding up the match)
|
|
|
254 |
* @return bool|string false, or the contents of the tag (including start and end).
|
|
|
255 |
*/
|
|
|
256 |
protected function full_tag_found_in_cache($tagname, $latestline) {
|
|
|
257 |
// Return entire element if found. Otherwise return false.
|
|
|
258 |
if (strpos(strtolower($latestline), '</'.strtolower($tagname).'>') === false) {
|
|
|
259 |
return false;
|
|
|
260 |
} else if (preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)) {
|
|
|
261 |
return $matches[1];
|
|
|
262 |
} else {
|
|
|
263 |
return false;
|
|
|
264 |
}
|
|
|
265 |
}
|
|
|
266 |
|
|
|
267 |
/**
|
|
|
268 |
* Remove complete tag from the cached data (including all its contents) - so
|
|
|
269 |
* that the cache doesn't grow to unmanageable size
|
|
|
270 |
*
|
|
|
271 |
* @param string $tagname Name of tag to look for
|
|
|
272 |
*/
|
|
|
273 |
protected function remove_tag_from_cache($tagname) {
|
|
|
274 |
// Trim the cache so we're not in danger of running out of memory.
|
|
|
275 |
// "1" so that we replace only the FIRST instance.
|
|
|
276 |
$this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1));
|
|
|
277 |
}
|
|
|
278 |
|
|
|
279 |
/**
|
|
|
280 |
* Very simple convenience function to return the "recstatus" found in person/group/role tags.
|
|
|
281 |
* 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".
|
|
|
282 |
*
|
|
|
283 |
* @param string $tagdata the tag XML data
|
|
|
284 |
* @param string $tagname the name of the tag we're interested in
|
|
|
285 |
* @return int recstatus value
|
|
|
286 |
*/
|
|
|
287 |
protected static function get_recstatus($tagdata, $tagname) {
|
|
|
288 |
if (preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)) {
|
|
|
289 |
return intval($matches[1]);
|
|
|
290 |
} else {
|
|
|
291 |
return 0; // Unspecified.
|
|
|
292 |
}
|
|
|
293 |
}
|
|
|
294 |
|
|
|
295 |
/**
|
|
|
296 |
* Process the group tag. This defines a Moodle course.
|
|
|
297 |
*
|
|
|
298 |
* @param string $tagcontents The raw contents of the XML element
|
|
|
299 |
*/
|
|
|
300 |
protected function process_group_tag($tagcontents) {
|
|
|
301 |
global $DB, $CFG;
|
|
|
302 |
|
|
|
303 |
// Get configs.
|
|
|
304 |
$truncatecoursecodes = $this->get_config('truncatecoursecodes');
|
|
|
305 |
$createnewcourses = $this->get_config('createnewcourses');
|
|
|
306 |
$updatecourses = $this->get_config('updatecourses');
|
|
|
307 |
|
|
|
308 |
if ($createnewcourses) {
|
|
|
309 |
require_once("$CFG->dirroot/course/lib.php");
|
|
|
310 |
}
|
|
|
311 |
|
|
|
312 |
// Process tag contents.
|
|
|
313 |
$group = new stdClass();
|
|
|
314 |
if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
|
|
|
315 |
$group->coursecode = trim($matches[1]);
|
|
|
316 |
}
|
|
|
317 |
|
|
|
318 |
$matches = array();
|
|
|
319 |
if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {
|
|
|
320 |
$group->long = trim($matches[1]);
|
|
|
321 |
}
|
|
|
322 |
|
|
|
323 |
$matches = array();
|
|
|
324 |
if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {
|
|
|
325 |
$group->short = trim($matches[1]);
|
|
|
326 |
}
|
|
|
327 |
|
|
|
328 |
$matches = array();
|
|
|
329 |
if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {
|
|
|
330 |
$group->full = trim($matches[1]);
|
|
|
331 |
}
|
|
|
332 |
|
|
|
333 |
if (preg_match('{<org>(.*?)</org>}is', $tagcontents, $matchesorg)) {
|
|
|
334 |
if (preg_match_all('{<orgunit>(.*?)</orgunit>}is', $matchesorg[1], $matchesorgunit)) {
|
|
|
335 |
$group->categories = array_map('trim', $matchesorgunit[1]);
|
|
|
336 |
}
|
|
|
337 |
}
|
|
|
338 |
|
|
|
339 |
$recstatus = ($this->get_recstatus($tagcontents, 'group'));
|
|
|
340 |
|
|
|
341 |
if (empty($group->coursecode)) {
|
|
|
342 |
$this->log_line('Error: Unable to find course code in \'group\' element.');
|
|
|
343 |
} else {
|
|
|
344 |
// First, truncate the course code if desired.
|
|
|
345 |
if (intval($truncatecoursecodes) > 0) {
|
|
|
346 |
$group->coursecode = ($truncatecoursecodes > 0)
|
|
|
347 |
? substr($group->coursecode, 0, intval($truncatecoursecodes))
|
|
|
348 |
: $group->coursecode;
|
|
|
349 |
}
|
|
|
350 |
|
|
|
351 |
// For compatibility with the (currently inactive) course aliasing, we need this to be an array.
|
|
|
352 |
$group->coursecode = array($group->coursecode);
|
|
|
353 |
|
|
|
354 |
// Third, check if the course(s) exist.
|
|
|
355 |
foreach ($group->coursecode as $coursecode) {
|
|
|
356 |
$coursecode = trim($coursecode);
|
|
|
357 |
$dbcourse = $DB->get_record('course', array('idnumber' => $coursecode));
|
|
|
358 |
if (!$dbcourse) {
|
|
|
359 |
if (!$createnewcourses) {
|
|
|
360 |
$this->log_line("Course $coursecode not found in Moodle's course idnumbers.");
|
|
|
361 |
} else {
|
|
|
362 |
|
|
|
363 |
// Create the (hidden) course(s) if not found.
|
|
|
364 |
$courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.
|
|
|
365 |
|
|
|
366 |
// New course.
|
|
|
367 |
$course = new stdClass();
|
|
|
368 |
foreach ($this->coursemappings as $courseattr => $imsname) {
|
|
|
369 |
|
|
|
370 |
if ($imsname == 'ignore') {
|
|
|
371 |
continue;
|
|
|
372 |
}
|
|
|
373 |
|
|
|
374 |
// Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.
|
|
|
375 |
if ($imsname == 'coursecode') {
|
|
|
376 |
$course->{$courseattr} = $coursecode;
|
|
|
377 |
} else if (!empty($group->{$imsname})) {
|
|
|
378 |
$course->{$courseattr} = $group->{$imsname};
|
|
|
379 |
} else {
|
|
|
380 |
$this->log_line('No ' . $imsname . ' description tag found for '
|
|
|
381 |
.$coursecode . ' coursecode, using ' . $coursecode . ' instead');
|
|
|
382 |
$course->{$courseattr} = $coursecode;
|
|
|
383 |
}
|
|
|
384 |
}
|
|
|
385 |
|
|
|
386 |
$course->idnumber = $coursecode;
|
|
|
387 |
$course->format = $courseconfig->format;
|
|
|
388 |
$course->visible = $courseconfig->visible;
|
|
|
389 |
$course->newsitems = $courseconfig->newsitems;
|
|
|
390 |
$course->showgrades = $courseconfig->showgrades;
|
|
|
391 |
$course->showreports = $courseconfig->showreports;
|
|
|
392 |
$course->maxbytes = $courseconfig->maxbytes;
|
|
|
393 |
$course->groupmode = $courseconfig->groupmode;
|
|
|
394 |
$course->groupmodeforce = $courseconfig->groupmodeforce;
|
|
|
395 |
$course->enablecompletion = $courseconfig->enablecompletion;
|
|
|
396 |
// Insert default names for teachers/students, from the current language.
|
|
|
397 |
|
|
|
398 |
// Handle course categorisation (taken from the group.org.orgunit or group.org.id fields if present).
|
|
|
399 |
$course->category = $this->get_category_from_group($group->categories);
|
|
|
400 |
|
|
|
401 |
$course->startdate = time();
|
|
|
402 |
// Choose a sort order that puts us at the start of the list!
|
|
|
403 |
$course->sortorder = 0;
|
|
|
404 |
|
|
|
405 |
$course = create_course($course);
|
|
|
406 |
|
|
|
407 |
$this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");
|
|
|
408 |
}
|
|
|
409 |
} else if (($recstatus == self::IMSENTERPRISE_UPDATE) && $dbcourse) {
|
|
|
410 |
if ($updatecourses) {
|
|
|
411 |
// Update course. Allowed fields to be updated are:
|
|
|
412 |
// Short Name, and Full Name.
|
|
|
413 |
$hasupdates = false;
|
|
|
414 |
if (!empty($group->short)) {
|
|
|
415 |
if ($group->short != $dbcourse->shortname) {
|
|
|
416 |
$dbcourse->shortname = $group->short;
|
|
|
417 |
$hasupdates = true;
|
|
|
418 |
}
|
|
|
419 |
}
|
|
|
420 |
if (!empty($group->full)) {
|
|
|
421 |
if ($group->full != $dbcourse->fullname) {
|
|
|
422 |
$dbcourse->fullname = $group->full;
|
|
|
423 |
$hasupdates = true;
|
|
|
424 |
}
|
|
|
425 |
}
|
|
|
426 |
if ($hasupdates) {
|
|
|
427 |
update_course($dbcourse);
|
|
|
428 |
$courseid = $dbcourse->id;
|
|
|
429 |
$this->log_line("Updated course $coursecode in Moodle (Moodle ID is $courseid)");
|
|
|
430 |
}
|
|
|
431 |
} else {
|
|
|
432 |
// Update courses option is not enabled. Ignore.
|
|
|
433 |
$this->log_line("Ignoring update to course $coursecode");
|
|
|
434 |
}
|
|
|
435 |
} else if (($recstatus == self::IMSENTERPRISE_DELETE) && $dbcourse) {
|
|
|
436 |
// If course does exist, but recstatus==3 (delete), then set the course as hidden.
|
|
|
437 |
$courseid = $dbcourse->id;
|
|
|
438 |
$show = false;
|
|
|
439 |
course_change_visibility($courseid, $show);
|
|
|
440 |
$this->log_line("Updated (set to hidden) course $coursecode in Moodle (Moodle ID is $courseid)");
|
|
|
441 |
}
|
|
|
442 |
}
|
|
|
443 |
}
|
|
|
444 |
}
|
|
|
445 |
|
|
|
446 |
/**
|
|
|
447 |
* Process the person tag. This defines a Moodle user.
|
|
|
448 |
*
|
|
|
449 |
* @param string $tagcontents The raw contents of the XML element
|
|
|
450 |
*/
|
|
|
451 |
protected function process_person_tag($tagcontents) {
|
|
|
452 |
global $CFG, $DB;
|
|
|
453 |
|
|
|
454 |
// Get plugin configs.
|
|
|
455 |
$imssourcedidfallback = $this->get_config('imssourcedidfallback');
|
|
|
456 |
$fixcaseusernames = $this->get_config('fixcaseusernames');
|
|
|
457 |
$fixcasepersonalnames = $this->get_config('fixcasepersonalnames');
|
|
|
458 |
$imsdeleteusers = $this->get_config('imsdeleteusers');
|
|
|
459 |
$createnewusers = $this->get_config('createnewusers');
|
|
|
460 |
$imsupdateusers = $this->get_config('imsupdateusers');
|
|
|
461 |
|
|
|
462 |
$person = new stdClass();
|
|
|
463 |
if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
|
|
|
464 |
$person->idnumber = trim($matches[1]);
|
|
|
465 |
}
|
|
|
466 |
|
|
|
467 |
$matches = array();
|
|
|
468 |
if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
|
|
|
469 |
$person->firstname = trim($matches[1]);
|
|
|
470 |
}
|
|
|
471 |
|
|
|
472 |
$matches = array();
|
|
|
473 |
if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {
|
|
|
474 |
$person->lastname = trim($matches[1]);
|
|
|
475 |
}
|
|
|
476 |
|
|
|
477 |
$matches = array();
|
|
|
478 |
if (preg_match('{<userid.*?>(.*?)</userid>}is', $tagcontents, $matches)) {
|
|
|
479 |
$person->username = trim($matches[1]);
|
|
|
480 |
}
|
|
|
481 |
|
|
|
482 |
$matches = array();
|
|
|
483 |
if (preg_match('{<userid\s+authenticationtype\s*=\s*"*(.+?)"*>.*?</userid>}is', $tagcontents, $matches)) {
|
|
|
484 |
$person->auth = trim($matches[1]);
|
|
|
485 |
}
|
|
|
486 |
|
|
|
487 |
if ($imssourcedidfallback && trim($person->username) == '') {
|
|
|
488 |
// This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied.
|
|
|
489 |
// NB We don't use an "elseif" because the tag may be supplied-but-empty.
|
|
|
490 |
$person->username = $person->idnumber;
|
|
|
491 |
}
|
|
|
492 |
|
|
|
493 |
$matches = array();
|
|
|
494 |
if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {
|
|
|
495 |
$person->email = trim($matches[1]);
|
|
|
496 |
}
|
|
|
497 |
|
|
|
498 |
$matches = array();
|
|
|
499 |
if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {
|
|
|
500 |
$person->url = trim($matches[1]);
|
|
|
501 |
}
|
|
|
502 |
|
|
|
503 |
$matches = array();
|
|
|
504 |
if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {
|
|
|
505 |
$person->city = trim($matches[1]);
|
|
|
506 |
}
|
|
|
507 |
|
|
|
508 |
$matches = array();
|
|
|
509 |
if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {
|
|
|
510 |
$person->country = trim($matches[1]);
|
|
|
511 |
}
|
|
|
512 |
|
|
|
513 |
// Fix case of some of the fields if required.
|
|
|
514 |
if ($fixcaseusernames && isset($person->username)) {
|
|
|
515 |
$person->username = strtolower($person->username);
|
|
|
516 |
}
|
|
|
517 |
if ($fixcasepersonalnames) {
|
|
|
518 |
if (isset($person->firstname)) {
|
|
|
519 |
$person->firstname = ucwords(strtolower($person->firstname));
|
|
|
520 |
}
|
|
|
521 |
if (isset($person->lastname)) {
|
|
|
522 |
$person->lastname = ucwords(strtolower($person->lastname));
|
|
|
523 |
}
|
|
|
524 |
}
|
|
|
525 |
|
|
|
526 |
$recstatus = ($this->get_recstatus($tagcontents, 'person'));
|
|
|
527 |
|
|
|
528 |
// Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on.
|
|
|
529 |
if ($recstatus == self::IMSENTERPRISE_DELETE) {
|
|
|
530 |
|
|
|
531 |
if ($imsdeleteusers) { // If we're allowed to delete user records.
|
|
|
532 |
// Do not dare to hack the user.deleted field directly in database!!!
|
|
|
533 |
$params = array('username' => $person->username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 0);
|
|
|
534 |
if ($user = $DB->get_record('user', $params)) {
|
|
|
535 |
if (delete_user($user)) {
|
|
|
536 |
$this->log_line("Deleted user '$person->username' (ID number $person->idnumber).");
|
|
|
537 |
} else {
|
|
|
538 |
$this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");
|
|
|
539 |
}
|
|
|
540 |
} else {
|
|
|
541 |
$this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");
|
|
|
542 |
}
|
|
|
543 |
} else {
|
|
|
544 |
$this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");
|
|
|
545 |
}
|
|
|
546 |
} else if ($recstatus == self::IMSENTERPRISE_UPDATE) { // Update user.
|
|
|
547 |
if ($imsupdateusers) {
|
|
|
548 |
if ($id = $DB->get_field('user', 'id', array('idnumber' => $person->idnumber))) {
|
|
|
549 |
$person->id = $id;
|
|
|
550 |
$DB->update_record('user', $person);
|
|
|
551 |
$this->log_line("Updated user $person->username");
|
|
|
552 |
} else {
|
|
|
553 |
$this->log_line("Ignoring update request for non-existent user $person->username");
|
|
|
554 |
}
|
|
|
555 |
} else {
|
|
|
556 |
$this->log_line("Ignoring update request for user $person->username");
|
|
|
557 |
}
|
|
|
558 |
|
|
|
559 |
} else { // Add or update record.
|
|
|
560 |
|
|
|
561 |
// If the user exists (matching sourcedid) then we don't need to do anything.
|
|
|
562 |
if (!$DB->get_field('user', 'id', array('idnumber' => $person->idnumber)) && $createnewusers) {
|
|
|
563 |
// If they don't exist and haven't a defined username, we log this as a potential problem.
|
|
|
564 |
if ((!isset($person->username)) || (strlen($person->username) == 0)) {
|
|
|
565 |
$this->log_line("Cannot create new user for ID # $person->idnumber".
|
|
|
566 |
"- no username listed in IMS data for this person.");
|
|
|
567 |
} else if ($DB->get_field('user', 'id', array('username' => $person->username))) {
|
|
|
568 |
// If their idnumber is not registered but their user ID is, then add their idnumber to their record.
|
|
|
569 |
$DB->set_field('user', 'idnumber', $person->idnumber, array('username' => $person->username));
|
|
|
570 |
} else {
|
|
|
571 |
|
|
|
572 |
// If they don't exist and they have a defined username, and $createnewusers == true, we create them.
|
|
|
573 |
$person->lang = $CFG->lang;
|
|
|
574 |
// TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.
|
|
|
575 |
if (empty($person->auth)) {
|
|
|
576 |
$auth = explode(',', $CFG->auth);
|
|
|
577 |
$auth = reset($auth);
|
|
|
578 |
$person->auth = $auth;
|
|
|
579 |
}
|
|
|
580 |
$person->confirmed = 1;
|
|
|
581 |
$person->timemodified = time();
|
|
|
582 |
$person->mnethostid = $CFG->mnet_localhost_id;
|
|
|
583 |
$id = $DB->insert_record('user', $person);
|
|
|
584 |
$this->log_line("Created user record ('.$id.') for user '$person->username' (ID number $person->idnumber).");
|
|
|
585 |
}
|
|
|
586 |
} else if ($createnewusers) {
|
|
|
587 |
|
|
|
588 |
$username = $person->username ?? "[unknown username]";
|
|
|
589 |
$personnumber = $person->idnumber ?? "[unknown ID number]";
|
|
|
590 |
|
|
|
591 |
$this->log_line("User record already exists for user '" . $username . "' (ID number " . $personnumber . ").");
|
|
|
592 |
|
|
|
593 |
// It is totally wrong to mess with deleted users flag directly in database!!!
|
|
|
594 |
// There is no official way to undelete user, sorry..
|
|
|
595 |
} else {
|
|
|
596 |
$this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");
|
|
|
597 |
}
|
|
|
598 |
|
|
|
599 |
}
|
|
|
600 |
|
|
|
601 |
}
|
|
|
602 |
|
|
|
603 |
/**
|
|
|
604 |
* Process the membership tag. This defines whether the specified Moodle users
|
|
|
605 |
* should be added/removed as teachers/students.
|
|
|
606 |
*
|
|
|
607 |
* @param string $tagcontents The raw contents of the XML element
|
|
|
608 |
*/
|
|
|
609 |
protected function process_membership_tag($tagcontents) {
|
|
|
610 |
global $DB;
|
|
|
611 |
|
|
|
612 |
// Get plugin configs.
|
|
|
613 |
$truncatecoursecodes = $this->get_config('truncatecoursecodes');
|
|
|
614 |
$imscapitafix = $this->get_config('imscapitafix');
|
|
|
615 |
|
|
|
616 |
$memberstally = 0;
|
|
|
617 |
$membersuntally = 0;
|
|
|
618 |
|
|
|
619 |
// In order to reduce the number of db queries required, group name/id associations are cached in this array.
|
|
|
620 |
$groupids = array();
|
|
|
621 |
|
|
|
622 |
$ship = new stdClass();
|
|
|
623 |
|
|
|
624 |
if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {
|
|
|
625 |
$ship->coursecode = ($truncatecoursecodes > 0)
|
|
|
626 |
? substr(trim($matches[1]), 0, intval($truncatecoursecodes))
|
|
|
627 |
: trim($matches[1]);
|
|
|
628 |
$ship->courseid = $DB->get_field('course', 'id', array('idnumber' => $ship->coursecode));
|
|
|
629 |
}
|
|
|
630 |
if ($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)) {
|
|
|
631 |
$courseobj = new stdClass();
|
|
|
632 |
$courseobj->id = $ship->courseid;
|
|
|
633 |
|
|
|
634 |
foreach ($membermatches as $mmatch) {
|
|
|
635 |
$member = new stdClass();
|
|
|
636 |
$memberstoreobj = new stdClass();
|
|
|
637 |
$matches = array();
|
|
|
638 |
if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)) {
|
|
|
639 |
$member->idnumber = trim($matches[1]);
|
|
|
640 |
}
|
|
|
641 |
|
|
|
642 |
$matches = array();
|
|
|
643 |
if (preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)) {
|
|
|
644 |
// 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides.
|
|
|
645 |
$member->roletype = trim($matches[1]);
|
|
|
646 |
} else if ($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)) {
|
|
|
647 |
// The XML that comes out of Capita Student Records seems to contain a misinterpretation of
|
|
|
648 |
// the IMS specification! 01 means Student, 02 means Instructor, 3 means ContentDeveloper,
|
|
|
649 |
// and there are more besides.
|
|
|
650 |
$member->roletype = trim($matches[1]);
|
|
|
651 |
}
|
|
|
652 |
|
|
|
653 |
$matches = array();
|
|
|
654 |
if (preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)) {
|
|
|
655 |
// 1 means active, 0 means inactive - treat this as enrol vs unenrol.
|
|
|
656 |
$member->status = trim($matches[1]);
|
|
|
657 |
}
|
|
|
658 |
|
|
|
659 |
$recstatus = ($this->get_recstatus($mmatch[1], 'role'));
|
|
|
660 |
if ($recstatus == self::IMSENTERPRISE_DELETE) {
|
|
|
661 |
// See above - recstatus of 3 (==delete) is treated the same as status of 0.
|
|
|
662 |
$member->status = 0;
|
|
|
663 |
}
|
|
|
664 |
|
|
|
665 |
$timeframe = new stdClass();
|
|
|
666 |
$timeframe->begin = 0;
|
|
|
667 |
$timeframe->end = 0;
|
|
|
668 |
$matches = array();
|
|
|
669 |
if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {
|
|
|
670 |
$timeframe = $this->decode_timeframe($matches[1]);
|
|
|
671 |
}
|
|
|
672 |
|
|
|
673 |
$matches = array();
|
|
|
674 |
if (preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is',
|
|
|
675 |
$mmatch[1], $matches)) {
|
|
|
676 |
$member->groupname = trim($matches[1]);
|
|
|
677 |
// The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause.
|
|
|
678 |
}
|
|
|
679 |
|
|
|
680 |
// Add or remove this student or teacher to the course...
|
|
|
681 |
$memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber' => $member->idnumber));
|
|
|
682 |
$memberstoreobj->enrol = 'imsenterprise';
|
|
|
683 |
$memberstoreobj->course = $ship->courseid;
|
|
|
684 |
$memberstoreobj->time = time();
|
|
|
685 |
$memberstoreobj->timemodified = time();
|
|
|
686 |
if ($memberstoreobj->userid) {
|
|
|
687 |
|
|
|
688 |
// Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.
|
|
|
689 |
// Zero means this roletype is supposed to be skipped.
|
|
|
690 |
$moodleroleid = (isset($member->roletype) && isset($this->rolemappings[$member->roletype]))
|
|
|
691 |
? $this->rolemappings[$member->roletype] : null;
|
|
|
692 |
if (!$moodleroleid) {
|
|
|
693 |
$this->log_line("SKIPPING role " .
|
|
|
694 |
($member->roletype ?? "[]") . " for $memberstoreobj->userid " .
|
|
|
695 |
"($member->idnumber) in course $memberstoreobj->course");
|
|
|
696 |
continue;
|
|
|
697 |
}
|
|
|
698 |
|
|
|
699 |
if (intval($member->status) == 1) {
|
|
|
700 |
// Enrol the member.
|
|
|
701 |
|
|
|
702 |
$einstance = $DB->get_record('enrol',
|
|
|
703 |
array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));
|
|
|
704 |
if (empty($einstance)) {
|
|
|
705 |
// Only add an enrol instance to the course if non-existent.
|
|
|
706 |
$enrolid = $this->add_instance($courseobj);
|
|
|
707 |
$einstance = $DB->get_record('enrol', array('id' => $enrolid));
|
|
|
708 |
}
|
|
|
709 |
|
|
|
710 |
$this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);
|
|
|
711 |
|
|
|
712 |
$this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) "
|
|
|
713 |
."to role $member->roletype in course $memberstoreobj->course");
|
|
|
714 |
$memberstally++;
|
|
|
715 |
|
|
|
716 |
// At this point we can also ensure the group membership is recorded if present.
|
|
|
717 |
if (isset($member->groupname)) {
|
|
|
718 |
// Create the group if it doesn't exist - either way, make sure we know the group ID.
|
|
|
719 |
if (isset($groupids[$member->groupname])) {
|
|
|
720 |
$member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available.
|
|
|
721 |
} else {
|
|
|
722 |
$params = array('courseid' => $ship->courseid, 'name' => $member->groupname);
|
|
|
723 |
if ($groupid = $DB->get_field('groups', 'id', $params)) {
|
|
|
724 |
$member->groupid = $groupid;
|
|
|
725 |
$groupids[$member->groupname] = $groupid; // Store ID in cache.
|
|
|
726 |
} else {
|
|
|
727 |
// Attempt to create the group.
|
|
|
728 |
$group = new stdClass();
|
|
|
729 |
$group->name = $member->groupname;
|
|
|
730 |
$group->courseid = $ship->courseid;
|
|
|
731 |
$group->timecreated = time();
|
|
|
732 |
$group->timemodified = time();
|
|
|
733 |
$groupid = $DB->insert_record('groups', $group);
|
|
|
734 |
$this->log_line('Added a new group for this course: '.$group->name);
|
|
|
735 |
$groupids[$member->groupname] = $groupid; // Store ID in cache.
|
|
|
736 |
$member->groupid = $groupid;
|
|
|
737 |
// Invalidate the course group data cache just in case.
|
|
|
738 |
cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($ship->courseid));
|
|
|
739 |
}
|
|
|
740 |
}
|
|
|
741 |
// Add the user-to-group association if it doesn't already exist.
|
|
|
742 |
if ($member->groupid) {
|
|
|
743 |
groups_add_member($member->groupid, $memberstoreobj->userid,
|
|
|
744 |
'enrol_imsenterprise', $einstance->id);
|
|
|
745 |
}
|
|
|
746 |
}
|
|
|
747 |
|
|
|
748 |
} else if ($this->get_config('imsunenrol')) {
|
|
|
749 |
// Unenrol member.
|
|
|
750 |
$unenrolsetting = $this->get_config('unenrolaction');
|
|
|
751 |
|
|
|
752 |
$einstances = $DB->get_records('enrol',
|
|
|
753 |
array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));
|
|
|
754 |
|
|
|
755 |
switch ($unenrolsetting) {
|
|
|
756 |
case ENROL_EXT_REMOVED_SUSPEND:
|
|
|
757 |
case ENROL_EXT_REMOVED_SUSPENDNOROLES: {
|
|
|
758 |
foreach ($einstances as $einstance) {
|
|
|
759 |
$this->update_user_enrol($einstance, $memberstoreobj->userid,
|
|
|
760 |
ENROL_USER_SUSPENDED, $timeframe->begin, $timeframe->end);
|
|
|
761 |
|
|
|
762 |
$this->log_line("Suspending user enrolment for $member->idnumber in " .
|
|
|
763 |
" course $ship->coursecode ");
|
|
|
764 |
|
|
|
765 |
if (intval($unenrolsetting) === intval(ENROL_EXT_REMOVED_SUSPENDNOROLES)) {
|
|
|
766 |
if (!$context =
|
|
|
767 |
context_course::instance($courseobj->id, IGNORE_MISSING)) {
|
|
|
768 |
|
|
|
769 |
$this->log_line("Unable to process IMS unenrolment request " .
|
|
|
770 |
" because course context not found. User: " .
|
|
|
771 |
"#$memberstoreobj->userid ($member->idnumber) , " .
|
|
|
772 |
" course: $memberstoreobj->course");
|
|
|
773 |
} else {
|
|
|
774 |
|
|
|
775 |
role_unassign_all([
|
|
|
776 |
'contextid' => $context->id,
|
|
|
777 |
'userid' => $memberstoreobj->userid,
|
|
|
778 |
'component' => 'enrol_imsenterprise',
|
|
|
779 |
'itemid' => $einstance->id
|
|
|
780 |
]);
|
|
|
781 |
|
|
|
782 |
$this->log_line("Removing role assignments for user " .
|
|
|
783 |
"$member->idnumber from role $moodleroleid in course " .
|
|
|
784 |
"$ship->coursecode ");
|
|
|
785 |
}
|
|
|
786 |
}
|
|
|
787 |
}
|
|
|
788 |
}
|
|
|
789 |
break;
|
|
|
790 |
|
|
|
791 |
case ENROL_EXT_REMOVED_UNENROL: {
|
|
|
792 |
foreach ($einstances as $einstance) {
|
|
|
793 |
$this->unenrol_user($einstance, $memberstoreobj->userid);
|
|
|
794 |
$this->log_line("Removing user enrolment record for $member->idnumber " .
|
|
|
795 |
" in course $ship->coursecode ");
|
|
|
796 |
}
|
|
|
797 |
}
|
|
|
798 |
break;
|
|
|
799 |
|
|
|
800 |
case ENROL_EXT_REMOVED_KEEP: {
|
|
|
801 |
$this->log_line("Processed KEEP IMS unenrol instruction (i.e. do nothing)");
|
|
|
802 |
}
|
|
|
803 |
break;
|
|
|
804 |
|
|
|
805 |
default:
|
|
|
806 |
$this->log_line("Unable to process IMS unenrolment request because " .
|
|
|
807 |
" the value set for plugin parameter, unenrol action, is not recognised. " .
|
|
|
808 |
" User: #$memberstoreobj->userid ($member->idnumber) " .
|
|
|
809 |
" , course: $memberstoreobj->course");
|
|
|
810 |
break;
|
|
|
811 |
}
|
|
|
812 |
|
|
|
813 |
$membersuntally++;
|
|
|
814 |
}
|
|
|
815 |
|
|
|
816 |
}
|
|
|
817 |
}
|
|
|
818 |
$this->log_line("Added $memberstally users to course $ship->coursecode");
|
|
|
819 |
if ($membersuntally > 0) {
|
|
|
820 |
$this->log_line("Processed $membersuntally unenrol instructions for course $ship->coursecode");
|
|
|
821 |
}
|
|
|
822 |
}
|
|
|
823 |
|
|
|
824 |
} // End process_membership_tag().
|
|
|
825 |
|
|
|
826 |
/**
|
|
|
827 |
* Process the properties tag. The only data from this element
|
|
|
828 |
* that is relevant is whether a <target> is specified.
|
|
|
829 |
*
|
|
|
830 |
* @param string $tagcontents The raw contents of the XML element
|
|
|
831 |
*/
|
|
|
832 |
protected function process_properties_tag($tagcontents) {
|
|
|
833 |
$imsrestricttarget = $this->get_config('imsrestricttarget');
|
|
|
834 |
|
|
|
835 |
if ($imsrestricttarget) {
|
|
|
836 |
if (!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))) {
|
|
|
837 |
$this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");
|
|
|
838 |
$this->continueprocessing = false;
|
|
|
839 |
}
|
|
|
840 |
}
|
|
|
841 |
}
|
|
|
842 |
|
|
|
843 |
/**
|
|
|
844 |
* Store logging information. This does two things: uses the {@link mtrace()}
|
|
|
845 |
* function to print info to screen/STDOUT, and also writes log to a text file
|
|
|
846 |
* if a path has been specified.
|
|
|
847 |
* @param string $string Text to write (newline will be added automatically)
|
|
|
848 |
*/
|
|
|
849 |
protected function log_line($string) {
|
|
|
850 |
|
|
|
851 |
if (!PHPUNIT_TEST) {
|
|
|
852 |
mtrace($string);
|
|
|
853 |
}
|
|
|
854 |
if ($this->logfp) {
|
|
|
855 |
fwrite($this->logfp, $string . "\n");
|
|
|
856 |
}
|
|
|
857 |
}
|
|
|
858 |
|
|
|
859 |
/**
|
|
|
860 |
* Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.
|
|
|
861 |
*
|
|
|
862 |
* @param string $string tag to decode.
|
|
|
863 |
* @return stdClass beginning and/or ending is returned, in unix time, zero indicating not specified.
|
|
|
864 |
*/
|
|
|
865 |
protected static function decode_timeframe($string) {
|
|
|
866 |
$ret = new stdClass();
|
|
|
867 |
$ret->begin = $ret->end = 0;
|
|
|
868 |
// Explanatory note: The matching will ONLY match if the attribute restrict="1"
|
|
|
869 |
// because otherwise the time markers should be ignored (participation should be
|
|
|
870 |
// allowed outside the period).
|
|
|
871 |
if (preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)) {
|
|
|
872 |
$ret->begin = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
|
|
|
873 |
}
|
|
|
874 |
|
|
|
875 |
$matches = array();
|
|
|
876 |
if (preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)) {
|
|
|
877 |
$ret->end = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);
|
|
|
878 |
}
|
|
|
879 |
return $ret;
|
|
|
880 |
}
|
|
|
881 |
|
|
|
882 |
/**
|
|
|
883 |
* Load the role mappings (from the config), so we can easily refer to
|
|
|
884 |
* how an IMS-E role corresponds to a Moodle role
|
|
|
885 |
*/
|
|
|
886 |
protected function load_role_mappings() {
|
|
|
887 |
require_once('locallib.php');
|
|
|
888 |
|
|
|
889 |
$imsroles = new imsenterprise_roles();
|
|
|
890 |
$imsroles = $imsroles->get_imsroles();
|
|
|
891 |
|
|
|
892 |
$this->rolemappings = array();
|
|
|
893 |
foreach ($imsroles as $imsrolenum => $imsrolename) {
|
|
|
894 |
$this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);
|
|
|
895 |
}
|
|
|
896 |
}
|
|
|
897 |
|
|
|
898 |
/**
|
|
|
899 |
* Load the name mappings (from the config), so we can easily refer to
|
|
|
900 |
* how an IMS-E course properties corresponds to a Moodle course properties
|
|
|
901 |
*/
|
|
|
902 |
protected function load_course_mappings() {
|
|
|
903 |
require_once('locallib.php');
|
|
|
904 |
|
|
|
905 |
$imsnames = new imsenterprise_courses();
|
|
|
906 |
$courseattrs = $imsnames->get_courseattrs();
|
|
|
907 |
|
|
|
908 |
$this->coursemappings = array();
|
|
|
909 |
foreach ($courseattrs as $courseattr) {
|
|
|
910 |
$this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);
|
|
|
911 |
}
|
|
|
912 |
}
|
|
|
913 |
|
|
|
914 |
/**
|
|
|
915 |
* Get the default category id (often known as 'Miscellaneous'),
|
|
|
916 |
* statically cached to avoid multiple DB lookups on big imports.
|
|
|
917 |
*
|
|
|
918 |
* @return int id of default category.
|
|
|
919 |
*/
|
|
|
920 |
private function get_default_category_id() {
|
|
|
921 |
global $CFG;
|
|
|
922 |
|
|
|
923 |
if ($this->defaultcategoryid === null) {
|
|
|
924 |
$category = core_course_category::get_default();
|
|
|
925 |
$this->defaultcategoryid = $category->id;
|
|
|
926 |
}
|
|
|
927 |
|
|
|
928 |
return $this->defaultcategoryid;
|
|
|
929 |
}
|
|
|
930 |
|
|
|
931 |
/**
|
|
|
932 |
* Find the category using idnumber or name.
|
|
|
933 |
*
|
|
|
934 |
* @param array $categories List of categories
|
|
|
935 |
*
|
|
|
936 |
* @return int id of category found.
|
|
|
937 |
*/
|
|
|
938 |
private function get_category_from_group($categories) {
|
|
|
939 |
global $DB;
|
|
|
940 |
|
|
|
941 |
if (empty($categories)) {
|
|
|
942 |
$catid = $this->get_default_category_id();
|
|
|
943 |
} else {
|
|
|
944 |
$createnewcategories = $this->get_config('createnewcategories');
|
|
|
945 |
$categoryseparator = trim($this->get_config('categoryseparator'));
|
|
|
946 |
$nestedcategories = trim($this->get_config('nestedcategories'));
|
|
|
947 |
$searchbyidnumber = trim($this->get_config('categoryidnumber'));
|
|
|
948 |
|
|
|
949 |
if (!empty($categoryseparator)) {
|
|
|
950 |
$sep = '{\\'.$categoryseparator.'}';
|
|
|
951 |
}
|
|
|
952 |
|
|
|
953 |
$catid = 0;
|
|
|
954 |
$fullnestedcatname = '';
|
|
|
955 |
|
|
|
956 |
foreach ($categories as $categoryinfo) {
|
|
|
957 |
if ($searchbyidnumber) {
|
|
|
958 |
$values = preg_split($sep, $categoryinfo, -1, PREG_SPLIT_NO_EMPTY);
|
|
|
959 |
if (count($values) < 2) {
|
|
|
960 |
$this->log_line('Category ' . $categoryinfo . ' missing name or idnumber. Using default category instead.');
|
|
|
961 |
$catid = $this->get_default_category_id();
|
|
|
962 |
break;
|
|
|
963 |
}
|
|
|
964 |
$categoryname = $values[0];
|
|
|
965 |
$categoryidnumber = $values[1];
|
|
|
966 |
} else {
|
|
|
967 |
$categoryname = $categoryinfo;
|
|
|
968 |
$categoryidnumber = null;
|
|
|
969 |
if (empty($categoryname)) {
|
|
|
970 |
$this->log_line('Category ' . $categoryinfo . ' missing name. Using default category instead.');
|
|
|
971 |
$catid = $this->get_default_category_id();
|
|
|
972 |
break;
|
|
|
973 |
}
|
|
|
974 |
}
|
|
|
975 |
|
|
|
976 |
if (!empty($fullnestedcatname)) {
|
|
|
977 |
$fullnestedcatname .= ' / ';
|
|
|
978 |
}
|
|
|
979 |
|
|
|
980 |
$fullnestedcatname .= $categoryname;
|
|
|
981 |
$parentid = $catid;
|
|
|
982 |
|
|
|
983 |
// Check if category exist.
|
|
|
984 |
$params = array();
|
|
|
985 |
if ($searchbyidnumber) {
|
|
|
986 |
$params['idnumber'] = $categoryidnumber;
|
|
|
987 |
} else {
|
|
|
988 |
$params['name'] = $categoryname;
|
|
|
989 |
}
|
|
|
990 |
if ($nestedcategories) {
|
|
|
991 |
$params['parent'] = $parentid;
|
|
|
992 |
}
|
|
|
993 |
|
|
|
994 |
if ($catid = $DB->get_field('course_categories', 'id', $params)) {
|
|
|
995 |
continue; // This category already exists.
|
|
|
996 |
}
|
|
|
997 |
|
|
|
998 |
// If we're allowed to create new categories, let's create this one.
|
|
|
999 |
if ($createnewcategories) {
|
|
|
1000 |
$newcat = new stdClass();
|
|
|
1001 |
$newcat->name = $categoryname;
|
|
|
1002 |
$newcat->visible = 0;
|
|
|
1003 |
$newcat->parent = $parentid;
|
|
|
1004 |
$newcat->idnumber = $categoryidnumber;
|
|
|
1005 |
$newcat = core_course_category::create($newcat);
|
|
|
1006 |
$catid = $newcat->id;
|
|
|
1007 |
$this->log_line("Created new (hidden) category '$fullnestedcatname'");
|
|
|
1008 |
} else {
|
|
|
1009 |
// If not found and not allowed to create, stick with default.
|
|
|
1010 |
$this->log_line('Category ' . $categoryinfo . ' not found in Moodle database. Using default category instead.');
|
|
|
1011 |
$catid = $this->get_default_category_id();
|
|
|
1012 |
break;
|
|
|
1013 |
}
|
|
|
1014 |
}
|
|
|
1015 |
}
|
|
|
1016 |
|
|
|
1017 |
return $catid;
|
|
|
1018 |
}
|
|
|
1019 |
|
|
|
1020 |
/**
|
|
|
1021 |
* Is it possible to delete enrol instance via standard UI?
|
|
|
1022 |
*
|
|
|
1023 |
* @param object $instance
|
|
|
1024 |
* @return bool
|
|
|
1025 |
*/
|
|
|
1026 |
public function can_delete_instance($instance) {
|
|
|
1027 |
$context = context_course::instance($instance->courseid);
|
|
|
1028 |
return has_capability('enrol/imsenterprise:config', $context);
|
|
|
1029 |
}
|
|
|
1030 |
|
|
|
1031 |
/**
|
|
|
1032 |
* Is it possible to hide/show enrol instance via standard UI?
|
|
|
1033 |
*
|
|
|
1034 |
* @param stdClass $instance
|
|
|
1035 |
* @return bool
|
|
|
1036 |
*/
|
|
|
1037 |
public function can_hide_show_instance($instance) {
|
|
|
1038 |
$context = context_course::instance($instance->courseid);
|
|
|
1039 |
return has_capability('enrol/imsenterprise:config', $context);
|
|
|
1040 |
}
|
|
|
1041 |
}
|
|
|
1042 |
|
|
|
1043 |
/**
|
|
|
1044 |
* Called whenever anybody tries (from the normal interface) to remove a group
|
|
|
1045 |
* member which is registered as being created by this component. (Not called
|
|
|
1046 |
* when deleting an entire group or course at once.)
|
|
|
1047 |
* @param int $itemid Item ID that was stored in the group_members entry
|
|
|
1048 |
* @param int $groupid Group ID
|
|
|
1049 |
* @param int $userid User ID being removed from group
|
|
|
1050 |
* @return bool True if the remove is permitted, false to give an error
|
|
|
1051 |
*/
|
|
|
1052 |
function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {
|
|
|
1053 |
return false;
|
|
|
1054 |
}
|