AutorÃa | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** IMS Enterprise file enrolment plugin.** This plugin lets the user specify an IMS Enterprise file to be processed.* The IMS Enterprise file is mainly parsed on a regular cron,* but can also be imported via the UI (Admin Settings).* @package enrol_imsenterprise* @copyright 2010 Eugene Venter* @author Eugene Venter - based on code by Dan Stowell* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();require_once($CFG->dirroot.'/group/lib.php');/*** IMS Enterprise file enrolment plugin.** @copyright 2010 Eugene Venter* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class enrol_imsenterprise_plugin extends enrol_plugin {/*** @var IMSENTERPRISE_ADD imsenterprise add action.*/const IMSENTERPRISE_ADD = 1;/*** @var IMSENTERPRISE_UPDATE imsenterprise update action.*/const IMSENTERPRISE_UPDATE = 2;/*** @var IMSENTERPRISE_DELETE imsenterprise delete action.*/const IMSENTERPRISE_DELETE = 3;/*** @var $logfp resource file pointer for writing log data to.*/protected $logfp;/*** @var $continueprocessing bool flag to determine if processing should continue.*/protected $continueprocessing;/*** @var $xmlcache string cache of xml lines.*/protected $xmlcache;/*** @var $coursemappings array of mappings between IMS data fields and moodle course fields.*/protected $coursemappings;/*** @var $rolemappings array of mappings between IMS roles and moodle roles.*/protected $rolemappings;/*** @var $defaultcategoryid id of default category.*/protected $defaultcategoryid;/*** Read in an IMS Enterprise file.* Originally designed to handle v1.1 files but should be able to handle* earlier types as well, I believe.* This cron feature has been converted to a scheduled task and it can now be scheduled* from the UI.*/public function cron() {global $CFG;// Get configs.$imsfilelocation = $this->get_config('imsfilelocation');$logtolocation = $this->get_config('logtolocation');$mailadmins = $this->get_config('mailadmins');$prevtime = $this->get_config('prev_time');$prevmd5 = $this->get_config('prev_md5');$prevpath = $this->get_config('prev_path');if (empty($imsfilelocation)) {$filename = "$CFG->dataroot/1/imsenterprise-enrol.xml"; // Default location.} else {$filename = $imsfilelocation;}$this->logfp = false;if (!empty($logtolocation)) {$this->logfp = fopen($logtolocation, 'a');}$this->defaultcategoryid = null;$fileisnew = false;if ( file_exists($filename) ) {core_php_time_limit::raise();$starttime = time();$this->log_line('----------------------------------------------------------------------');$this->log_line("IMS Enterprise enrol cron process launched at " . userdate(time()));$this->log_line('Found file '.$filename);$this->xmlcache = '';$categoryseparator = trim($this->get_config('categoryseparator'));$categoryidnumber = $this->get_config('categoryidnumber');// Make sure we understand how to map the IMS-E roles to Moodle roles.$this->load_role_mappings();// Make sure we understand how to map the IMS-E course names to Moodle course names.$this->load_course_mappings();$md5 = md5_file($filename); // NB We'll write this value back to the database at the end of the cron.$filemtime = filemtime($filename);// Decide if we want to process the file (based on filepath, modification time, and MD5 hash)// This is so we avoid wasting the server's efforts processing a file unnecessarily.if ($categoryidnumber && empty($categoryseparator)) {$this->log_line('Category idnumber is enabled but category separator is not defined - skipping processing.');} else if (empty($prevpath) || ($filename != $prevpath)) {$fileisnew = true;} else if (isset($prevtime) && ($filemtime <= $prevtime)) {$this->log_line('File modification time is not more recent than last update - skipping processing.');} else if (isset($prevmd5) && ($md5 == $prevmd5)) {$this->log_line('File MD5 hash is same as on last update - skipping processing.');} else {$fileisnew = true; // Let's process it!}if ($fileisnew) {// The <properties> tag is allowed to halt processing if we're demanding a matching target.$this->continueprocessing = true;// Run through the file and process the group/person entries.if (($fh = fopen($filename, "r")) != false) {$line = 0;while ((!feof($fh)) && $this->continueprocessing) {$line++;$curline = fgets($fh);$this->xmlcache .= $curline; // Add a line onto the XML cache.while (true) {// If we've got a full tag (i.e. the most recent line has closed the tag) then process-it-and-forget-it.// Must always make sure to remove tags from cache so they don't clog up our memory.if ($tagcontents = $this->full_tag_found_in_cache('group', $curline)) {$this->process_group_tag($tagcontents);$this->remove_tag_from_cache('group');} else if ($tagcontents = $this->full_tag_found_in_cache('person', $curline)) {$this->process_person_tag($tagcontents);$this->remove_tag_from_cache('person');} else if ($tagcontents = $this->full_tag_found_in_cache('membership', $curline)) {$this->process_membership_tag($tagcontents);$this->remove_tag_from_cache('membership');} else if ($tagcontents = $this->full_tag_found_in_cache('comments', $curline)) {$this->remove_tag_from_cache('comments');} else if ($tagcontents = $this->full_tag_found_in_cache('properties', $curline)) {$this->process_properties_tag($tagcontents);$this->remove_tag_from_cache('properties');} else {break;}}}fclose($fh);fix_course_sortorder();}$timeelapsed = time() - $starttime;$this->log_line('Process has completed. Time taken: '.$timeelapsed.' seconds.');}// These variables are stored so we can compare them against the IMS file, next time round.$this->set_config('prev_time', $filemtime);$this->set_config('prev_md5', $md5);$this->set_config('prev_path', $filename);} else {$this->log_line('File not found: '.$filename);}if (!empty($mailadmins) && $fileisnew) {$timeelapsed = isset($timeelapsed) ? $timeelapsed : 0;$msg = "An IMS enrolment has been carried out within Moodle.\nTime taken: $timeelapsed seconds.\n\n";if (!empty($logtolocation)) {if ($this->logfp) {$msg .= "Log data has been written to:\n";$msg .= "$logtolocation\n";$msg .= "(Log file size: ".ceil(filesize($logtolocation) / 1024)."Kb)\n\n";} else {$msg .= "The log file appears not to have been successfully written.\n";$msg .= "Check that the file is writeable by the server:\n";$msg .= "$logtolocation\n\n";}} else {$msg .= "Logging is currently not active.";}$eventdata = new \core\message\message();$eventdata->courseid = SITEID;$eventdata->modulename = 'moodle';$eventdata->component = 'enrol_imsenterprise';$eventdata->name = 'imsenterprise_enrolment';$eventdata->userfrom = get_admin();$eventdata->userto = get_admin();$eventdata->subject = "Moodle IMS Enterprise enrolment notification";$eventdata->fullmessage = $msg;$eventdata->fullmessageformat = FORMAT_PLAIN;$eventdata->fullmessagehtml = '';$eventdata->smallmessage = '';message_send($eventdata);$this->log_line('Notification email sent to administrator.');}if ($this->logfp) {fclose($this->logfp);}}/*** Check if a complete tag is found in the cached data, which usually happens* when the end of the tag has only just been loaded into the cache.** @param string $tagname Name of tag to look for* @param string $latestline The very last line in the cache (used for speeding up the match)* @return bool|string false, or the contents of the tag (including start and end).*/protected function full_tag_found_in_cache($tagname, $latestline) {// Return entire element if found. Otherwise return false.if (strpos(strtolower($latestline), '</'.strtolower($tagname).'>') === false) {return false;} else if (preg_match('{(<'.$tagname.'\b.*?>.*?</'.$tagname.'>)}is', $this->xmlcache, $matches)) {return $matches[1];} else {return false;}}/*** Remove complete tag from the cached data (including all its contents) - so* that the cache doesn't grow to unmanageable size** @param string $tagname Name of tag to look for*/protected function remove_tag_from_cache($tagname) {// Trim the cache so we're not in danger of running out of memory.// "1" so that we replace only the FIRST instance.$this->xmlcache = trim(preg_replace('{<'.$tagname.'\b.*?>.*?</'.$tagname.'>}is', '', $this->xmlcache, 1));}/*** Very simple convenience function to return the "recstatus" found in person/group/role tags.* 1=Add, 2=Update, 3=Delete, as specified by IMS, and we also use 0 to indicate "unspecified".** @param string $tagdata the tag XML data* @param string $tagname the name of the tag we're interested in* @return int recstatus value*/protected static function get_recstatus($tagdata, $tagname) {if (preg_match('{<'.$tagname.'\b[^>]*recstatus\s*=\s*["\'](\d)["\']}is', $tagdata, $matches)) {return intval($matches[1]);} else {return 0; // Unspecified.}}/*** Process the group tag. This defines a Moodle course.** @param string $tagcontents The raw contents of the XML element*/protected function process_group_tag($tagcontents) {global $DB, $CFG;// Get configs.$truncatecoursecodes = $this->get_config('truncatecoursecodes');$createnewcourses = $this->get_config('createnewcourses');$updatecourses = $this->get_config('updatecourses');if ($createnewcourses) {require_once("$CFG->dirroot/course/lib.php");}// Process tag contents.$group = new stdClass();if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {$group->coursecode = trim($matches[1]);}$matches = array();if (preg_match('{<description>.*?<long>(.*?)</long>.*?</description>}is', $tagcontents, $matches)) {$group->long = trim($matches[1]);}$matches = array();if (preg_match('{<description>.*?<short>(.*?)</short>.*?</description>}is', $tagcontents, $matches)) {$group->short = trim($matches[1]);}$matches = array();if (preg_match('{<description>.*?<full>(.*?)</full>.*?</description>}is', $tagcontents, $matches)) {$group->full = trim($matches[1]);}if (preg_match('{<org>(.*?)</org>}is', $tagcontents, $matchesorg)) {if (preg_match_all('{<orgunit>(.*?)</orgunit>}is', $matchesorg[1], $matchesorgunit)) {$group->categories = array_map('trim', $matchesorgunit[1]);}}$recstatus = ($this->get_recstatus($tagcontents, 'group'));if (empty($group->coursecode)) {$this->log_line('Error: Unable to find course code in \'group\' element.');} else {// First, truncate the course code if desired.if (intval($truncatecoursecodes) > 0) {$group->coursecode = ($truncatecoursecodes > 0)? substr($group->coursecode, 0, intval($truncatecoursecodes)): $group->coursecode;}// For compatibility with the (currently inactive) course aliasing, we need this to be an array.$group->coursecode = array($group->coursecode);// Third, check if the course(s) exist.foreach ($group->coursecode as $coursecode) {$coursecode = trim($coursecode);$dbcourse = $DB->get_record('course', array('idnumber' => $coursecode));if (!$dbcourse) {if (!$createnewcourses) {$this->log_line("Course $coursecode not found in Moodle's course idnumbers.");} else {// Create the (hidden) course(s) if not found.$courseconfig = get_config('moodlecourse'); // Load Moodle Course shell defaults.// New course.$course = new stdClass();foreach ($this->coursemappings as $courseattr => $imsname) {if ($imsname == 'ignore') {continue;}// Check if the IMS file contains the mapped tag, otherwise fallback on coursecode.if ($imsname == 'coursecode') {$course->{$courseattr} = $coursecode;} else if (!empty($group->{$imsname})) {$course->{$courseattr} = $group->{$imsname};} else {$this->log_line('No ' . $imsname . ' description tag found for '.$coursecode . ' coursecode, using ' . $coursecode . ' instead');$course->{$courseattr} = $coursecode;}}$course->idnumber = $coursecode;$course->format = $courseconfig->format;$course->visible = $courseconfig->visible;$course->newsitems = $courseconfig->newsitems;$course->showgrades = $courseconfig->showgrades;$course->showreports = $courseconfig->showreports;$course->maxbytes = $courseconfig->maxbytes;$course->groupmode = $courseconfig->groupmode;$course->groupmodeforce = $courseconfig->groupmodeforce;$course->enablecompletion = $courseconfig->enablecompletion;// Insert default names for teachers/students, from the current language.// Handle course categorisation (taken from the group.org.orgunit or group.org.id fields if present).$course->category = $this->get_category_from_group($group->categories);$course->startdate = time();// Choose a sort order that puts us at the start of the list!$course->sortorder = 0;$course = create_course($course);$this->log_line("Created course $coursecode in Moodle (Moodle ID is $course->id)");}} else if (($recstatus == self::IMSENTERPRISE_UPDATE) && $dbcourse) {if ($updatecourses) {// Update course. Allowed fields to be updated are:// Short Name, and Full Name.$hasupdates = false;if (!empty($group->short)) {if ($group->short != $dbcourse->shortname) {$dbcourse->shortname = $group->short;$hasupdates = true;}}if (!empty($group->full)) {if ($group->full != $dbcourse->fullname) {$dbcourse->fullname = $group->full;$hasupdates = true;}}if ($hasupdates) {update_course($dbcourse);$courseid = $dbcourse->id;$this->log_line("Updated course $coursecode in Moodle (Moodle ID is $courseid)");}} else {// Update courses option is not enabled. Ignore.$this->log_line("Ignoring update to course $coursecode");}} else if (($recstatus == self::IMSENTERPRISE_DELETE) && $dbcourse) {// If course does exist, but recstatus==3 (delete), then set the course as hidden.$courseid = $dbcourse->id;$show = false;course_change_visibility($courseid, $show);$this->log_line("Updated (set to hidden) course $coursecode in Moodle (Moodle ID is $courseid)");}}}}/*** Process the person tag. This defines a Moodle user.** @param string $tagcontents The raw contents of the XML element*/protected function process_person_tag($tagcontents) {global $CFG, $DB;// Get plugin configs.$imssourcedidfallback = $this->get_config('imssourcedidfallback');$fixcaseusernames = $this->get_config('fixcaseusernames');$fixcasepersonalnames = $this->get_config('fixcasepersonalnames');$imsdeleteusers = $this->get_config('imsdeleteusers');$createnewusers = $this->get_config('createnewusers');$imsupdateusers = $this->get_config('imsupdateusers');$person = new stdClass();if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {$person->idnumber = trim($matches[1]);}$matches = array();if (preg_match('{<name>.*?<n>.*?<given>(.+?)</given>.*?</n>.*?</name>}is', $tagcontents, $matches)) {$person->firstname = trim($matches[1]);}$matches = array();if (preg_match('{<name>.*?<n>.*?<family>(.+?)</family>.*?</n>.*?</name>}is', $tagcontents, $matches)) {$person->lastname = trim($matches[1]);}$matches = array();if (preg_match('{<userid.*?>(.*?)</userid>}is', $tagcontents, $matches)) {$person->username = trim($matches[1]);}$matches = array();if (preg_match('{<userid\s+authenticationtype\s*=\s*"*(.+?)"*>.*?</userid>}is', $tagcontents, $matches)) {$person->auth = trim($matches[1]);}if ($imssourcedidfallback && trim($person->username) == '') {// This is the point where we can fall back to useing the "sourcedid" if "userid" is not supplied.// NB We don't use an "elseif" because the tag may be supplied-but-empty.$person->username = $person->idnumber;}$matches = array();if (preg_match('{<email>(.*?)</email>}is', $tagcontents, $matches)) {$person->email = trim($matches[1]);}$matches = array();if (preg_match('{<url>(.*?)</url>}is', $tagcontents, $matches)) {$person->url = trim($matches[1]);}$matches = array();if (preg_match('{<adr>.*?<locality>(.+?)</locality>.*?</adr>}is', $tagcontents, $matches)) {$person->city = trim($matches[1]);}$matches = array();if (preg_match('{<adr>.*?<country>(.+?)</country>.*?</adr>}is', $tagcontents, $matches)) {$person->country = trim($matches[1]);}// Fix case of some of the fields if required.if ($fixcaseusernames && isset($person->username)) {$person->username = strtolower($person->username);}if ($fixcasepersonalnames) {if (isset($person->firstname)) {$person->firstname = ucwords(strtolower($person->firstname));}if (isset($person->lastname)) {$person->lastname = ucwords(strtolower($person->lastname));}}$recstatus = ($this->get_recstatus($tagcontents, 'person'));// Now if the recstatus is 3, we should delete the user if-and-only-if the setting for delete users is turned on.if ($recstatus == self::IMSENTERPRISE_DELETE) {if ($imsdeleteusers) { // If we're allowed to delete user records.// Do not dare to hack the user.deleted field directly in database!!!$params = array('username' => $person->username, 'mnethostid' => $CFG->mnet_localhost_id, 'deleted' => 0);if ($user = $DB->get_record('user', $params)) {if (delete_user($user)) {$this->log_line("Deleted user '$person->username' (ID number $person->idnumber).");} else {$this->log_line("Error deleting '$person->username' (ID number $person->idnumber).");}} else {$this->log_line("Can not delete user '$person->username' (ID number $person->idnumber) - user does not exist.");}} else {$this->log_line("Ignoring deletion request for user '$person->username' (ID number $person->idnumber).");}} else if ($recstatus == self::IMSENTERPRISE_UPDATE) { // Update user.if ($imsupdateusers) {if ($id = $DB->get_field('user', 'id', array('idnumber' => $person->idnumber))) {$person->id = $id;$DB->update_record('user', $person);$this->log_line("Updated user $person->username");} else {$this->log_line("Ignoring update request for non-existent user $person->username");}} else {$this->log_line("Ignoring update request for user $person->username");}} else { // Add or update record.// If the user exists (matching sourcedid) then we don't need to do anything.if (!$DB->get_field('user', 'id', array('idnumber' => $person->idnumber)) && $createnewusers) {// If they don't exist and haven't a defined username, we log this as a potential problem.if ((!isset($person->username)) || (strlen($person->username) == 0)) {$this->log_line("Cannot create new user for ID # $person->idnumber"."- no username listed in IMS data for this person.");} else if ($DB->get_field('user', 'id', array('username' => $person->username))) {// If their idnumber is not registered but their user ID is, then add their idnumber to their record.$DB->set_field('user', 'idnumber', $person->idnumber, array('username' => $person->username));} else {// If they don't exist and they have a defined username, and $createnewusers == true, we create them.$person->lang = $CFG->lang;// TODO: MDL-15863 this needs more work due to multiauth changes, use first auth for now.if (empty($person->auth)) {$auth = explode(',', $CFG->auth);$auth = reset($auth);$person->auth = $auth;}$person->confirmed = 1;$person->timemodified = time();$person->mnethostid = $CFG->mnet_localhost_id;$id = $DB->insert_record('user', $person);$this->log_line("Created user record ('.$id.') for user '$person->username' (ID number $person->idnumber).");}} else if ($createnewusers) {$username = $person->username ?? "[unknown username]";$personnumber = $person->idnumber ?? "[unknown ID number]";$this->log_line("User record already exists for user '" . $username . "' (ID number " . $personnumber . ").");// It is totally wrong to mess with deleted users flag directly in database!!!// There is no official way to undelete user, sorry..} else {$this->log_line("No user record found for '$person->username' (ID number $person->idnumber).");}}}/*** Process the membership tag. This defines whether the specified Moodle users* should be added/removed as teachers/students.** @param string $tagcontents The raw contents of the XML element*/protected function process_membership_tag($tagcontents) {global $DB;// Get plugin configs.$truncatecoursecodes = $this->get_config('truncatecoursecodes');$imscapitafix = $this->get_config('imscapitafix');$memberstally = 0;$membersuntally = 0;// In order to reduce the number of db queries required, group name/id associations are cached in this array.$groupids = array();$ship = new stdClass();if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $tagcontents, $matches)) {$ship->coursecode = ($truncatecoursecodes > 0)? substr(trim($matches[1]), 0, intval($truncatecoursecodes)): trim($matches[1]);$ship->courseid = $DB->get_field('course', 'id', array('idnumber' => $ship->coursecode));}if ($ship->courseid && preg_match_all('{<member>(.*?)</member>}is', $tagcontents, $membermatches, PREG_SET_ORDER)) {$courseobj = new stdClass();$courseobj->id = $ship->courseid;foreach ($membermatches as $mmatch) {$member = new stdClass();$memberstoreobj = new stdClass();$matches = array();if (preg_match('{<sourcedid>.*?<id>(.+?)</id>.*?</sourcedid>}is', $mmatch[1], $matches)) {$member->idnumber = trim($matches[1]);}$matches = array();if (preg_match('{<role\s+roletype=["\'](.+?)["\'].*?>}is', $mmatch[1], $matches)) {// 01 means Student, 02 means Instructor, 3 means ContentDeveloper, and there are more besides.$member->roletype = trim($matches[1]);} else if ($imscapitafix && preg_match('{<roletype>(.+?)</roletype>}is', $mmatch[1], $matches)) {// The XML that comes out of Capita Student Records seems to contain a misinterpretation of// the IMS specification! 01 means Student, 02 means Instructor, 3 means ContentDeveloper,// and there are more besides.$member->roletype = trim($matches[1]);}$matches = array();if (preg_match('{<role\b.*?<status>(.+?)</status>.*?</role>}is', $mmatch[1], $matches)) {// 1 means active, 0 means inactive - treat this as enrol vs unenrol.$member->status = trim($matches[1]);}$recstatus = ($this->get_recstatus($mmatch[1], 'role'));if ($recstatus == self::IMSENTERPRISE_DELETE) {// See above - recstatus of 3 (==delete) is treated the same as status of 0.$member->status = 0;}$timeframe = new stdClass();$timeframe->begin = 0;$timeframe->end = 0;$matches = array();if (preg_match('{<role\b.*?<timeframe>(.+?)</timeframe>.*?</role>}is', $mmatch[1], $matches)) {$timeframe = $this->decode_timeframe($matches[1]);}$matches = array();if (preg_match('{<role\b.*?<extension>.*?<cohort>(.+?)</cohort>.*?</extension>.*?</role>}is',$mmatch[1], $matches)) {$member->groupname = trim($matches[1]);// The actual processing (ensuring a group record exists, etc) occurs below, in the enrol-a-student clause.}// Add or remove this student or teacher to the course...$memberstoreobj->userid = $DB->get_field('user', 'id', array('idnumber' => $member->idnumber));$memberstoreobj->enrol = 'imsenterprise';$memberstoreobj->course = $ship->courseid;$memberstoreobj->time = time();$memberstoreobj->timemodified = time();if ($memberstoreobj->userid) {// Decide the "real" role (i.e. the Moodle role) that this user should be assigned to.// Zero means this roletype is supposed to be skipped.$moodleroleid = (isset($member->roletype) && isset($this->rolemappings[$member->roletype]))? $this->rolemappings[$member->roletype] : null;if (!$moodleroleid) {$this->log_line("SKIPPING role " .($member->roletype ?? "[]") . " for $memberstoreobj->userid " ."($member->idnumber) in course $memberstoreobj->course");continue;}if (intval($member->status) == 1) {// Enrol the member.$einstance = $DB->get_record('enrol',array('courseid' => $courseobj->id, 'enrol' => $memberstoreobj->enrol));if (empty($einstance)) {// Only add an enrol instance to the course if non-existent.$enrolid = $this->add_instance($courseobj);$einstance = $DB->get_record('enrol', array('id' => $enrolid));}$this->enrol_user($einstance, $memberstoreobj->userid, $moodleroleid, $timeframe->begin, $timeframe->end);$this->log_line("Enrolled user #$memberstoreobj->userid ($member->idnumber) "."to role $member->roletype in course $memberstoreobj->course");$memberstally++;// At this point we can also ensure the group membership is recorded if present.if (isset($member->groupname)) {// Create the group if it doesn't exist - either way, make sure we know the group ID.if (isset($groupids[$member->groupname])) {$member->groupid = $groupids[$member->groupname]; // Recall the group ID from cache if available.} else {$params = array('courseid' => $ship->courseid, 'name' => $member->groupname);if ($groupid = $DB->get_field('groups', 'id', $params)) {$member->groupid = $groupid;$groupids[$member->groupname] = $groupid; // Store ID in cache.} else {// Attempt to create the group.$group = new stdClass();$group->name = $member->groupname;$group->courseid = $ship->courseid;$group->timecreated = time();$group->timemodified = time();$groupid = $DB->insert_record('groups', $group);$this->log_line('Added a new group for this course: '.$group->name);$groupids[$member->groupname] = $groupid; // Store ID in cache.$member->groupid = $groupid;// Invalidate the course group data cache just in case.cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($ship->courseid));}}// Add the user-to-group association if it doesn't already exist.if ($member->groupid) {groups_add_member($member->groupid, $memberstoreobj->userid,'enrol_imsenterprise', $einstance->id);}}} else if ($this->get_config('imsunenrol')) {// Unenrol member.$unenrolsetting = $this->get_config('unenrolaction');$einstances = $DB->get_records('enrol',array('enrol' => $memberstoreobj->enrol, 'courseid' => $courseobj->id));switch ($unenrolsetting) {case ENROL_EXT_REMOVED_SUSPEND:case ENROL_EXT_REMOVED_SUSPENDNOROLES: {foreach ($einstances as $einstance) {$this->update_user_enrol($einstance, $memberstoreobj->userid,ENROL_USER_SUSPENDED, $timeframe->begin, $timeframe->end);$this->log_line("Suspending user enrolment for $member->idnumber in " ." course $ship->coursecode ");if (intval($unenrolsetting) === intval(ENROL_EXT_REMOVED_SUSPENDNOROLES)) {if (!$context =context_course::instance($courseobj->id, IGNORE_MISSING)) {$this->log_line("Unable to process IMS unenrolment request " ." because course context not found. User: " ."#$memberstoreobj->userid ($member->idnumber) , " ." course: $memberstoreobj->course");} else {role_unassign_all(['contextid' => $context->id,'userid' => $memberstoreobj->userid,'component' => 'enrol_imsenterprise','itemid' => $einstance->id]);$this->log_line("Removing role assignments for user " ."$member->idnumber from role $moodleroleid in course " ."$ship->coursecode ");}}}}break;case ENROL_EXT_REMOVED_UNENROL: {foreach ($einstances as $einstance) {$this->unenrol_user($einstance, $memberstoreobj->userid);$this->log_line("Removing user enrolment record for $member->idnumber " ." in course $ship->coursecode ");}}break;case ENROL_EXT_REMOVED_KEEP: {$this->log_line("Processed KEEP IMS unenrol instruction (i.e. do nothing)");}break;default:$this->log_line("Unable to process IMS unenrolment request because " ." the value set for plugin parameter, unenrol action, is not recognised. " ." User: #$memberstoreobj->userid ($member->idnumber) " ." , course: $memberstoreobj->course");break;}$membersuntally++;}}}$this->log_line("Added $memberstally users to course $ship->coursecode");if ($membersuntally > 0) {$this->log_line("Processed $membersuntally unenrol instructions for course $ship->coursecode");}}} // End process_membership_tag()./*** Process the properties tag. The only data from this element* that is relevant is whether a <target> is specified.** @param string $tagcontents The raw contents of the XML element*/protected function process_properties_tag($tagcontents) {$imsrestricttarget = $this->get_config('imsrestricttarget');if ($imsrestricttarget) {if (!(preg_match('{<target>'.preg_quote($imsrestricttarget).'</target>}is', $tagcontents, $matches))) {$this->log_line("Skipping processing: required target \"$imsrestricttarget\" not specified in this data.");$this->continueprocessing = false;}}}/*** Store logging information. This does two things: uses the {@link mtrace()}* function to print info to screen/STDOUT, and also writes log to a text file* if a path has been specified.* @param string $string Text to write (newline will be added automatically)*/protected function log_line($string) {if (!PHPUNIT_TEST) {mtrace($string);}if ($this->logfp) {fwrite($this->logfp, $string . "\n");}}/*** Process the INNER contents of a <timeframe> tag, to return beginning/ending dates.** @param string $string tag to decode.* @return stdClass beginning and/or ending is returned, in unix time, zero indicating not specified.*/protected static function decode_timeframe($string) {$ret = new stdClass();$ret->begin = $ret->end = 0;// Explanatory note: The matching will ONLY match if the attribute restrict="1"// because otherwise the time markers should be ignored (participation should be// allowed outside the period).if (preg_match('{<begin\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</begin>}is', $string, $matches)) {$ret->begin = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);}$matches = array();if (preg_match('{<end\s+restrict="1">(\d\d\d\d)-(\d\d)-(\d\d)</end>}is', $string, $matches)) {$ret->end = mktime(0, 0, 0, $matches[2], $matches[3], $matches[1]);}return $ret;}/*** Load the role mappings (from the config), so we can easily refer to* how an IMS-E role corresponds to a Moodle role*/protected function load_role_mappings() {require_once('locallib.php');$imsroles = new imsenterprise_roles();$imsroles = $imsroles->get_imsroles();$this->rolemappings = array();foreach ($imsroles as $imsrolenum => $imsrolename) {$this->rolemappings[$imsrolenum] = $this->rolemappings[$imsrolename] = $this->get_config('imsrolemap' . $imsrolenum);}}/*** Load the name mappings (from the config), so we can easily refer to* how an IMS-E course properties corresponds to a Moodle course properties*/protected function load_course_mappings() {require_once('locallib.php');$imsnames = new imsenterprise_courses();$courseattrs = $imsnames->get_courseattrs();$this->coursemappings = array();foreach ($courseattrs as $courseattr) {$this->coursemappings[$courseattr] = $this->get_config('imscoursemap' . $courseattr);}}/*** Get the default category id (often known as 'Miscellaneous'),* statically cached to avoid multiple DB lookups on big imports.** @return int id of default category.*/private function get_default_category_id() {global $CFG;if ($this->defaultcategoryid === null) {$category = core_course_category::get_default();$this->defaultcategoryid = $category->id;}return $this->defaultcategoryid;}/*** Find the category using idnumber or name.** @param array $categories List of categories** @return int id of category found.*/private function get_category_from_group($categories) {global $DB;if (empty($categories)) {$catid = $this->get_default_category_id();} else {$createnewcategories = $this->get_config('createnewcategories');$categoryseparator = trim($this->get_config('categoryseparator'));$nestedcategories = trim($this->get_config('nestedcategories'));$searchbyidnumber = trim($this->get_config('categoryidnumber'));if (!empty($categoryseparator)) {$sep = '{\\'.$categoryseparator.'}';}$catid = 0;$fullnestedcatname = '';foreach ($categories as $categoryinfo) {if ($searchbyidnumber) {$values = preg_split($sep, $categoryinfo, -1, PREG_SPLIT_NO_EMPTY);if (count($values) < 2) {$this->log_line('Category ' . $categoryinfo . ' missing name or idnumber. Using default category instead.');$catid = $this->get_default_category_id();break;}$categoryname = $values[0];$categoryidnumber = $values[1];} else {$categoryname = $categoryinfo;$categoryidnumber = null;if (empty($categoryname)) {$this->log_line('Category ' . $categoryinfo . ' missing name. Using default category instead.');$catid = $this->get_default_category_id();break;}}if (!empty($fullnestedcatname)) {$fullnestedcatname .= ' / ';}$fullnestedcatname .= $categoryname;$parentid = $catid;// Check if category exist.$params = array();if ($searchbyidnumber) {$params['idnumber'] = $categoryidnumber;} else {$params['name'] = $categoryname;}if ($nestedcategories) {$params['parent'] = $parentid;}if ($catid = $DB->get_field('course_categories', 'id', $params)) {continue; // This category already exists.}// If we're allowed to create new categories, let's create this one.if ($createnewcategories) {$newcat = new stdClass();$newcat->name = $categoryname;$newcat->visible = 0;$newcat->parent = $parentid;$newcat->idnumber = $categoryidnumber;$newcat = core_course_category::create($newcat);$catid = $newcat->id;$this->log_line("Created new (hidden) category '$fullnestedcatname'");} else {// If not found and not allowed to create, stick with default.$this->log_line('Category ' . $categoryinfo . ' not found in Moodle database. Using default category instead.');$catid = $this->get_default_category_id();break;}}}return $catid;}/*** Is it possible to delete enrol instance via standard UI?** @param object $instance* @return bool*/public function can_delete_instance($instance) {$context = context_course::instance($instance->courseid);return has_capability('enrol/imsenterprise:config', $context);}/*** Is it possible to hide/show enrol instance via standard UI?** @param stdClass $instance* @return bool*/public function can_hide_show_instance($instance) {$context = context_course::instance($instance->courseid);return has_capability('enrol/imsenterprise:config', $context);}}/*** Called whenever anybody tries (from the normal interface) to remove a group* member which is registered as being created by this component. (Not called* when deleting an entire group or course at once.)* @param int $itemid Item ID that was stored in the group_members entry* @param int $groupid Group ID* @param int $userid User ID being removed from group* @return bool True if the remove is permitted, false to give an error*/function enrol_imsenterprise_allow_group_member_remove($itemid, $groupid, $userid) {return false;}