[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/enrol/imsenterprise/ -> lib.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1