Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401]

   1  <?php
   2  
   3  // This file is part of Moodle - http://moodle.org/
   4  //
   5  // Moodle is free software: you can redistribute it and/or modify
   6  // it under the terms of the GNU General Public License as published by
   7  // the Free Software Foundation, either version 3 of the License, or
   8  // (at your option) any later version.
   9  //
  10  // Moodle is distributed in the hope that it will be useful,
  11  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  12  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  13  // GNU General Public License for more details.
  14  //
  15  // You should have received a copy of the GNU General Public License
  16  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  17  
  18  /**
  19   * Defines Moodle 1.9 backup conversion handlers
  20   *
  21   * Handlers are classes responsible for the actual conversion work. Their logic
  22   * is similar to the functionality provided by steps in plan based restore process.
  23   *
  24   * @package    backup-convert
  25   * @subpackage moodle1
  26   * @copyright  2011 David Mudrak <david@moodle.com>
  27   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  
  30  defined('MOODLE_INTERNAL') || die();
  31  
  32  require_once($CFG->dirroot . '/backup/util/xml/xml_writer.class.php');
  33  require_once($CFG->dirroot . '/backup/util/xml/output/xml_output.class.php');
  34  require_once($CFG->dirroot . '/backup/util/xml/output/file_xml_output.class.php');
  35  
  36  /**
  37   * Handlers factory class
  38   */
  39  abstract class moodle1_handlers_factory {
  40  
  41      /**
  42       * @param moodle1_converter the converter requesting the converters
  43       * @return list of all available conversion handlers
  44       */
  45      public static function get_handlers(moodle1_converter $converter) {
  46  
  47          $handlers = array(
  48              new moodle1_root_handler($converter),
  49              new moodle1_info_handler($converter),
  50              new moodle1_course_header_handler($converter),
  51              new moodle1_course_outline_handler($converter),
  52              new moodle1_roles_definition_handler($converter),
  53              new moodle1_question_bank_handler($converter),
  54              new moodle1_scales_handler($converter),
  55              new moodle1_outcomes_handler($converter),
  56              new moodle1_gradebook_handler($converter),
  57          );
  58  
  59          $handlers = array_merge($handlers, self::get_plugin_handlers('mod', $converter));
  60          $handlers = array_merge($handlers, self::get_plugin_handlers('block', $converter));
  61  
  62          // make sure that all handlers have expected class
  63          foreach ($handlers as $handler) {
  64              if (!$handler instanceof moodle1_handler) {
  65                  throw new moodle1_convert_exception('wrong_handler_class', get_class($handler));
  66              }
  67          }
  68  
  69          return $handlers;
  70      }
  71  
  72      /// public API ends here ///////////////////////////////////////////////////
  73  
  74      /**
  75       * Runs through all plugins of a specific type and instantiates their handlers
  76       *
  77       * @todo ask mod's subplugins
  78       * @param string $type the plugin type
  79       * @param moodle1_converter $converter the converter requesting the handler
  80       * @throws moodle1_convert_exception
  81       * @return array of {@link moodle1_handler} instances
  82       */
  83      protected static function get_plugin_handlers($type, moodle1_converter $converter) {
  84          global $CFG;
  85  
  86          $handlers = array();
  87          $plugins = core_component::get_plugin_list($type);
  88          foreach ($plugins as $name => $dir) {
  89              $handlerfile  = $dir . '/backup/moodle1/lib.php';
  90              $handlerclass = "moodle1_{$type}_{$name}_handler";
  91              if (file_exists($handlerfile)) {
  92                  require_once($handlerfile);
  93              } elseif ($type == 'block') {
  94                  $handlerclass = "moodle1_block_generic_handler";
  95              } else {
  96                  continue;
  97              }
  98  
  99              if (!class_exists($handlerclass)) {
 100                  throw new moodle1_convert_exception('missing_handler_class', $handlerclass);
 101              }
 102              $handlers[] = new $handlerclass($converter, $type, $name);
 103          }
 104          return $handlers;
 105      }
 106  }
 107  
 108  
 109  /**
 110   * Base backup conversion handler
 111   */
 112  abstract class moodle1_handler implements loggable {
 113  
 114      /** @var moodle1_converter */
 115      protected $converter;
 116  
 117      /**
 118       * @param moodle1_converter $converter the converter that requires us
 119       */
 120      public function __construct(moodle1_converter $converter) {
 121          $this->converter = $converter;
 122      }
 123  
 124      /**
 125       * @return moodle1_converter the converter that required this handler
 126       */
 127      public function get_converter() {
 128          return $this->converter;
 129      }
 130  
 131      /**
 132       * Log a message using the converter's logging mechanism
 133       *
 134       * @param string $message message text
 135       * @param int $level message level {@example backup::LOG_WARNING}
 136       * @param null|mixed $a additional information
 137       * @param null|int $depth the message depth
 138       * @param bool $display whether the message should be sent to the output, too
 139       */
 140      public function log($message, $level, $a = null, $depth = null, $display = false) {
 141          $this->converter->log($message, $level, $a, $depth, $display);
 142      }
 143  }
 144  
 145  
 146  /**
 147   * Base backup conversion handler that generates an XML file
 148   */
 149  abstract class moodle1_xml_handler extends moodle1_handler {
 150  
 151      /** @var null|string the name of file we are writing to */
 152      protected $xmlfilename;
 153  
 154      /** @var null|xml_writer */
 155      protected $xmlwriter;
 156  
 157      /**
 158       * Opens the XML writer - after calling, one is free to use $xmlwriter
 159       *
 160       * @param string $filename XML file name to write into
 161       * @return void
 162       */
 163      protected function open_xml_writer($filename) {
 164  
 165          if (!is_null($this->xmlfilename) and $filename !== $this->xmlfilename) {
 166              throw new moodle1_convert_exception('xml_writer_already_opened_for_other_file', $this->xmlfilename);
 167          }
 168  
 169          if (!$this->xmlwriter instanceof xml_writer) {
 170              $this->xmlfilename = $filename;
 171              $fullpath  = $this->converter->get_workdir_path() . '/' . $this->xmlfilename;
 172              $directory = pathinfo($fullpath, PATHINFO_DIRNAME);
 173  
 174              if (!check_dir_exists($directory)) {
 175                  throw new moodle1_convert_exception('unable_create_target_directory', $directory);
 176              }
 177              $this->xmlwriter = new xml_writer(new file_xml_output($fullpath), new moodle1_xml_transformer());
 178              $this->xmlwriter->start();
 179          }
 180      }
 181  
 182      /**
 183       * Close the XML writer
 184       *
 185       * At the moment, the caller must close all tags before calling
 186       *
 187       * @return void
 188       */
 189      protected function close_xml_writer() {
 190          if ($this->xmlwriter instanceof xml_writer) {
 191              $this->xmlwriter->stop();
 192          }
 193          unset($this->xmlwriter);
 194          $this->xmlwriter = null;
 195          $this->xmlfilename = null;
 196      }
 197  
 198      /**
 199       * Checks if the XML writer has been opened by {@link self::open_xml_writer()}
 200       *
 201       * @return bool
 202       */
 203      protected function has_xml_writer() {
 204  
 205          if ($this->xmlwriter instanceof xml_writer) {
 206              return true;
 207          } else {
 208              return false;
 209          }
 210      }
 211  
 212      /**
 213       * Writes the given XML tree data into the currently opened file
 214       *
 215       * @param string $element the name of the root element of the tree
 216       * @param array $data the associative array of data to write
 217       * @param array $attribs list of additional fields written as attributes instead of nested elements
 218       * @param string $parent used internally during the recursion, do not set yourself
 219       */
 220      protected function write_xml($element, array $data, array $attribs = array(), $parent = '/') {
 221  
 222          if (!$this->has_xml_writer()) {
 223              throw new moodle1_convert_exception('write_xml_without_writer');
 224          }
 225  
 226          $mypath    = $parent . $element;
 227          $myattribs = array();
 228  
 229          // detect properties that should be rendered as element's attributes instead of children
 230          foreach ($data as $name => $value) {
 231              if (!is_array($value)) {
 232                  if (in_array($mypath . '/' . $name, $attribs)) {
 233                      $myattribs[$name] = $value;
 234                      unset($data[$name]);
 235                  }
 236              }
 237          }
 238  
 239          // reorder the $data so that all sub-branches are at the end (needed by our parser)
 240          $leaves   = array();
 241          $branches = array();
 242          foreach ($data as $name => $value) {
 243              if (is_array($value)) {
 244                  $branches[$name] = $value;
 245              } else {
 246                  $leaves[$name] = $value;
 247              }
 248          }
 249          $data = array_merge($leaves, $branches);
 250  
 251          $this->xmlwriter->begin_tag($element, $myattribs);
 252  
 253          foreach ($data as $name => $value) {
 254              if (is_array($value)) {
 255                  // recursively call self
 256                  $this->write_xml($name, $value, $attribs, $mypath.'/');
 257              } else {
 258                  $this->xmlwriter->full_tag($name, $value);
 259              }
 260          }
 261  
 262          $this->xmlwriter->end_tag($element);
 263      }
 264  
 265      /**
 266       * Makes sure that a new XML file exists, or creates it itself
 267       *
 268       * This is here so we can check that all XML files that the restore process relies on have
 269       * been created by an executed handler. If the file is not found, this method can create it
 270       * using the given $rootelement as an empty root container in the file.
 271       *
 272       * @param string $filename relative file name like 'course/course.xml'
 273       * @param string|bool $rootelement root element to use, false to not create the file
 274       * @param array $content content of the root element
 275       * @return bool true is the file existed, false if it did not
 276       */
 277      protected function make_sure_xml_exists($filename, $rootelement = false, $content = array()) {
 278  
 279          $existed = file_exists($this->converter->get_workdir_path().'/'.$filename);
 280  
 281          if ($existed) {
 282              return true;
 283          }
 284  
 285          if ($rootelement !== false) {
 286              $this->open_xml_writer($filename);
 287              $this->write_xml($rootelement, $content);
 288              $this->close_xml_writer();
 289          }
 290  
 291          return false;
 292      }
 293  }
 294  
 295  
 296  /**
 297   * Process the root element of the backup file
 298   */
 299  class moodle1_root_handler extends moodle1_xml_handler {
 300  
 301      public function get_paths() {
 302          return array(new convert_path('root_element', '/MOODLE_BACKUP'));
 303      }
 304  
 305      /**
 306       * Converts course_files and site_files
 307       */
 308      public function on_root_element_start() {
 309  
 310          // convert course files
 311          $fileshandler = new moodle1_files_handler($this->converter);
 312          $fileshandler->process();
 313      }
 314  
 315      /**
 316       * This is executed at the end of the moodle.xml parsing
 317       */
 318      public function on_root_element_end() {
 319          global $CFG;
 320  
 321          // restore the stashes prepared by other handlers for us
 322          $backupinfo         = $this->converter->get_stash('backup_info');
 323          $originalcourseinfo = $this->converter->get_stash('original_course_info');
 324  
 325          ////////////////////////////////////////////////////////////////////////
 326          // write moodle_backup.xml
 327          ////////////////////////////////////////////////////////////////////////
 328          $this->open_xml_writer('moodle_backup.xml');
 329  
 330          $this->xmlwriter->begin_tag('moodle_backup');
 331          $this->xmlwriter->begin_tag('information');
 332  
 333          // moodle_backup/information
 334          $this->xmlwriter->full_tag('name', $backupinfo['name']);
 335          $this->xmlwriter->full_tag('moodle_version', $backupinfo['moodle_version']);
 336          $this->xmlwriter->full_tag('moodle_release', $backupinfo['moodle_release']);
 337          $this->xmlwriter->full_tag('backup_version', $CFG->backup_version); // {@see restore_prechecks_helper::execute_prechecks}
 338          $this->xmlwriter->full_tag('backup_release', $CFG->backup_release);
 339          $this->xmlwriter->full_tag('backup_date', $backupinfo['date']);
 340          // see the commit c0543b - all backups created in 1.9 and later declare the
 341          // information or it is considered as false
 342          if (isset($backupinfo['mnet_remoteusers'])) {
 343              $this->xmlwriter->full_tag('mnet_remoteusers', $backupinfo['mnet_remoteusers']);
 344          } else {
 345              $this->xmlwriter->full_tag('mnet_remoteusers', false);
 346          }
 347          $this->xmlwriter->full_tag('original_wwwroot', $backupinfo['original_wwwroot']);
 348          // {@see backup_general_helper::backup_is_samesite()}
 349          if (isset($backupinfo['original_site_identifier_hash'])) {
 350              $this->xmlwriter->full_tag('original_site_identifier_hash', $backupinfo['original_site_identifier_hash']);
 351          } else {
 352              $this->xmlwriter->full_tag('original_site_identifier_hash', null);
 353          }
 354          $this->xmlwriter->full_tag('original_course_id', $originalcourseinfo['original_course_id']);
 355          $this->xmlwriter->full_tag('original_course_fullname', $originalcourseinfo['original_course_fullname']);
 356          $this->xmlwriter->full_tag('original_course_shortname', $originalcourseinfo['original_course_shortname']);
 357          $this->xmlwriter->full_tag('original_course_startdate', $originalcourseinfo['original_course_startdate']);
 358          $this->xmlwriter->full_tag('original_system_contextid', $this->converter->get_contextid(CONTEXT_SYSTEM));
 359          // note that even though we have original_course_contextid available, we regenerate the
 360          // original course contextid using our helper method to be sure that the data are consistent
 361          // within the MBZ file
 362          $this->xmlwriter->full_tag('original_course_contextid', $this->converter->get_contextid(CONTEXT_COURSE));
 363  
 364          // moodle_backup/information/details
 365          $this->xmlwriter->begin_tag('details');
 366          $this->write_xml('detail', array(
 367              'backup_id'     => $this->converter->get_id(),
 368              'type'          => backup::TYPE_1COURSE,
 369              'format'        => backup::FORMAT_MOODLE,
 370              'interactive'   => backup::INTERACTIVE_YES,
 371              'mode'          => backup::MODE_CONVERTED,
 372              'execution'     => backup::EXECUTION_INMEDIATE,
 373              'executiontime' => 0,
 374          ), array('/detail/backup_id'));
 375          $this->xmlwriter->end_tag('details');
 376  
 377          // moodle_backup/information/contents
 378          $this->xmlwriter->begin_tag('contents');
 379  
 380          // moodle_backup/information/contents/activities
 381          $this->xmlwriter->begin_tag('activities');
 382          $activitysettings = array();
 383          foreach ($this->converter->get_stash('coursecontents') as $activity) {
 384              $modinfo = $this->converter->get_stash('modinfo_'.$activity['modulename']);
 385              $modinstance = $modinfo['instances'][$activity['instanceid']];
 386              $this->write_xml('activity', array(
 387                  'moduleid'      => $activity['cmid'],
 388                  'sectionid'     => $activity['sectionid'],
 389                  'modulename'    => $activity['modulename'],
 390                  'title'         => $modinstance['name'],
 391                  'directory'     => 'activities/'.$activity['modulename'].'_'.$activity['cmid']
 392              ));
 393              $activitysettings[] = array(
 394                  'level'     => 'activity',
 395                  'activity'  => $activity['modulename'].'_'.$activity['cmid'],
 396                  'name'      => $activity['modulename'].'_'.$activity['cmid'].'_included',
 397                  'value'     => (($modinfo['included'] === 'true' and $modinstance['included'] === 'true') ? 1 : 0));
 398              $activitysettings[] = array(
 399                  'level'     => 'activity',
 400                  'activity'  => $activity['modulename'].'_'.$activity['cmid'],
 401                  'name'      => $activity['modulename'].'_'.$activity['cmid'].'_userinfo',
 402                  //'value'     => (($modinfo['userinfo'] === 'true' and $modinstance['userinfo'] === 'true') ? 1 : 0));
 403                  'value'     => 0); // todo hardcoded non-userinfo for now
 404          }
 405          $this->xmlwriter->end_tag('activities');
 406  
 407          // moodle_backup/information/contents/sections
 408          $this->xmlwriter->begin_tag('sections');
 409          $sectionsettings = array();
 410          foreach ($this->converter->get_stash_itemids('sectioninfo') as $sectionid) {
 411              $sectioninfo = $this->converter->get_stash('sectioninfo', $sectionid);
 412              $sectionsettings[] = array(
 413                  'level'     => 'section',
 414                  'section'   => 'section_'.$sectionid,
 415                  'name'      => 'section_'.$sectionid.'_included',
 416                  'value'     => 1);
 417              $sectionsettings[] = array(
 418                  'level'     => 'section',
 419                  'section'   => 'section_'.$sectionid,
 420                  'name'      => 'section_'.$sectionid.'_userinfo',
 421                  'value'     => 0); // @todo how to detect this from moodle.xml?
 422              $this->write_xml('section', array(
 423                  'sectionid' => $sectionid,
 424                  'title'     => $sectioninfo['number'], // because the title is not available
 425                  'directory' => 'sections/section_'.$sectionid));
 426          }
 427          $this->xmlwriter->end_tag('sections');
 428  
 429          // moodle_backup/information/contents/course
 430          $this->write_xml('course', array(
 431              'courseid'  => $originalcourseinfo['original_course_id'],
 432              'title'     => $originalcourseinfo['original_course_shortname'],
 433              'directory' => 'course'));
 434          unset($originalcourseinfo);
 435  
 436          $this->xmlwriter->end_tag('contents');
 437  
 438          // moodle_backup/information/settings
 439          $this->xmlwriter->begin_tag('settings');
 440  
 441          // fake backup root seetings
 442          $rootsettings = array(
 443              'filename'         => $backupinfo['name'],
 444              'users'            => 0, // @todo how to detect this from moodle.xml?
 445              'anonymize'        => 0,
 446              'role_assignments' => 0,
 447              'activities'       => 1,
 448              'blocks'           => 1,
 449              'filters'          => 0,
 450              'comments'         => 0,
 451              'userscompletion'  => 0,
 452              'logs'             => 0,
 453              'grade_histories'  => 0,
 454          );
 455          unset($backupinfo);
 456          foreach ($rootsettings as $name => $value) {
 457              $this->write_xml('setting', array(
 458                  'level' => 'root',
 459                  'name'  => $name,
 460                  'value' => $value));
 461          }
 462          unset($rootsettings);
 463  
 464          // activity settings populated above
 465          foreach ($activitysettings as $activitysetting) {
 466              $this->write_xml('setting', $activitysetting);
 467          }
 468          unset($activitysettings);
 469  
 470          // section settings populated above
 471          foreach ($sectionsettings as $sectionsetting) {
 472              $this->write_xml('setting', $sectionsetting);
 473          }
 474          unset($sectionsettings);
 475  
 476          $this->xmlwriter->end_tag('settings');
 477  
 478          $this->xmlwriter->end_tag('information');
 479          $this->xmlwriter->end_tag('moodle_backup');
 480  
 481          $this->close_xml_writer();
 482  
 483          ////////////////////////////////////////////////////////////////////////
 484          // write files.xml
 485          ////////////////////////////////////////////////////////////////////////
 486          $this->open_xml_writer('files.xml');
 487          $this->xmlwriter->begin_tag('files');
 488          foreach ($this->converter->get_stash_itemids('files') as $fileid) {
 489              $this->write_xml('file', $this->converter->get_stash('files', $fileid), array('/file/id'));
 490          }
 491          $this->xmlwriter->end_tag('files');
 492          $this->close_xml_writer('files.xml');
 493  
 494          ////////////////////////////////////////////////////////////////////////
 495          // write scales.xml
 496          ////////////////////////////////////////////////////////////////////////
 497          $this->open_xml_writer('scales.xml');
 498          $this->xmlwriter->begin_tag('scales_definition');
 499          foreach ($this->converter->get_stash_itemids('scales') as $scaleid) {
 500              $this->write_xml('scale', $this->converter->get_stash('scales', $scaleid), array('/scale/id'));
 501          }
 502          $this->xmlwriter->end_tag('scales_definition');
 503          $this->close_xml_writer('scales.xml');
 504  
 505          ////////////////////////////////////////////////////////////////////////
 506          // write course/inforef.xml
 507          ////////////////////////////////////////////////////////////////////////
 508          $this->open_xml_writer('course/inforef.xml');
 509          $this->xmlwriter->begin_tag('inforef');
 510  
 511          $this->xmlwriter->begin_tag('fileref');
 512          // legacy course files
 513          $fileids = $this->converter->get_stash('course_files_ids');
 514          if (is_array($fileids)) {
 515              foreach ($fileids as $fileid) {
 516                  $this->write_xml('file', array('id' => $fileid));
 517              }
 518          }
 519          // todo site files
 520          // course summary files
 521          $fileids = $this->converter->get_stash('course_summary_files_ids');
 522          if (is_array($fileids)) {
 523              foreach ($fileids as $fileid) {
 524                  $this->write_xml('file', array('id' => $fileid));
 525              }
 526          }
 527          $this->xmlwriter->end_tag('fileref');
 528  
 529          $this->xmlwriter->begin_tag('question_categoryref');
 530          foreach ($this->converter->get_stash_itemids('question_categories') as $questioncategoryid) {
 531              $this->write_xml('question_category', array('id' => $questioncategoryid));
 532          }
 533          $this->xmlwriter->end_tag('question_categoryref');
 534  
 535          $this->xmlwriter->end_tag('inforef');
 536          $this->close_xml_writer();
 537  
 538          // make sure that the files required by the restore process have been generated.
 539          // missing file may happen if the watched tag is not present in moodle.xml (for example
 540          // QUESTION_CATEGORIES is optional in moodle.xml but questions.xml must exist in
 541          // moodle2 format) or the handler has not been implemented yet.
 542          // apparently this must be called after the handler had a chance to create the file.
 543          $this->make_sure_xml_exists('questions.xml', 'question_categories');
 544          $this->make_sure_xml_exists('groups.xml', 'groups');
 545          $this->make_sure_xml_exists('outcomes.xml', 'outcomes_definition');
 546          $this->make_sure_xml_exists('users.xml', 'users');
 547          $this->make_sure_xml_exists('course/roles.xml', 'roles',
 548              array('role_assignments' => array(), 'role_overrides' => array()));
 549          $this->make_sure_xml_exists('course/enrolments.xml', 'enrolments',
 550              array('enrols' => array()));
 551      }
 552  }
 553  
 554  
 555  /**
 556   * The class responsible for course and site files migration
 557   *
 558   * @todo migrate site_files
 559   */
 560  class moodle1_files_handler extends moodle1_xml_handler {
 561  
 562      /**
 563       * Migrates course_files and site_files in the converter workdir
 564       */
 565      public function process() {
 566          $this->migrate_course_files();
 567          // todo $this->migrate_site_files();
 568      }
 569  
 570      /**
 571       * Migrates course_files in the converter workdir
 572       */
 573      protected function migrate_course_files() {
 574          $ids  = array();
 575          $fileman = $this->converter->get_file_manager($this->converter->get_contextid(CONTEXT_COURSE), 'course', 'legacy');
 576          $this->converter->set_stash('course_files_ids', array());
 577          if (file_exists($this->converter->get_tempdir_path().'/course_files')) {
 578              $ids = $fileman->migrate_directory('course_files');
 579              $this->converter->set_stash('course_files_ids', $ids);
 580          }
 581          $this->log('course files migrated', backup::LOG_INFO, count($ids));
 582      }
 583  }
 584  
 585  
 586  /**
 587   * Handles the conversion of /MOODLE_BACKUP/INFO paths
 588   *
 589   * We do not produce any XML file here, just storing the data in the temp
 590   * table so thay can be used by a later handler.
 591   */
 592  class moodle1_info_handler extends moodle1_handler {
 593  
 594      /** @var array list of mod names included in info_details */
 595      protected $modnames = array();
 596  
 597      /** @var array the in-memory cache of the currently parsed info_details_mod element */
 598      protected $currentmod;
 599  
 600      public function get_paths() {
 601          return array(
 602              new convert_path('info', '/MOODLE_BACKUP/INFO'),
 603              new convert_path('info_details', '/MOODLE_BACKUP/INFO/DETAILS'),
 604              new convert_path('info_details_mod', '/MOODLE_BACKUP/INFO/DETAILS/MOD'),
 605              new convert_path('info_details_mod_instance', '/MOODLE_BACKUP/INFO/DETAILS/MOD/INSTANCES/INSTANCE'),
 606          );
 607      }
 608  
 609      /**
 610       * Stashes the backup info for later processing by {@link moodle1_root_handler}
 611       */
 612      public function process_info($data) {
 613          $this->converter->set_stash('backup_info', $data);
 614      }
 615  
 616      /**
 617       * Initializes the in-memory cache for the current mod
 618       */
 619      public function process_info_details_mod($data) {
 620          $this->currentmod = $data;
 621          $this->currentmod['instances'] = array();
 622      }
 623  
 624      /**
 625       * Appends the current instance data to the temporary in-memory cache
 626       */
 627      public function process_info_details_mod_instance($data) {
 628          $this->currentmod['instances'][$data['id']] = $data;
 629      }
 630  
 631      /**
 632       * Stashes the backup info for later processing by {@link moodle1_root_handler}
 633       */
 634      public function on_info_details_mod_end($data) {
 635          global $CFG;
 636  
 637          // keep only such modules that seem to have the support for moodle1 implemented
 638          $modname = $this->currentmod['name'];
 639          if (file_exists($CFG->dirroot.'/mod/'.$modname.'/backup/moodle1/lib.php')) {
 640              $this->converter->set_stash('modinfo_'.$modname, $this->currentmod);
 641              $this->modnames[] = $modname;
 642          } else {
 643              $this->log('unsupported activity module', backup::LOG_WARNING, $modname);
 644          }
 645  
 646          $this->currentmod = array();
 647      }
 648  
 649      /**
 650       * Stashes the list of activity module types for later processing by {@link moodle1_root_handler}
 651       */
 652      public function on_info_details_end() {
 653          $this->converter->set_stash('modnameslist', $this->modnames);
 654      }
 655  }
 656  
 657  
 658  /**
 659   * Handles the conversion of /MOODLE_BACKUP/COURSE/HEADER paths
 660   */
 661  class moodle1_course_header_handler extends moodle1_xml_handler {
 662  
 663      /** @var array we need to merge course information because it is dispatched twice */
 664      protected $course = array();
 665  
 666      /** @var array we need to merge course information because it is dispatched twice */
 667      protected $courseraw = array();
 668  
 669      /** @var array */
 670      protected $category;
 671  
 672      public function get_paths() {
 673          return array(
 674              new convert_path(
 675                  'course_header', '/MOODLE_BACKUP/COURSE/HEADER',
 676                  array(
 677                      'newfields' => array(
 678                          'summaryformat'          => 1,
 679                          'legacyfiles'            => 2,
 680                          'requested'              => 0, // @todo not really new, but maybe never backed up?
 681                          'restrictmodules'        => 0,
 682                          'enablecompletion'       => 0,
 683                          'completionstartonenrol' => 0,
 684                          'completionnotify'       => 0,
 685                          'tags'                   => array(),
 686                          'allowed_modules'        => array(),
 687                      ),
 688                      'dropfields' => array(
 689                          'roles_overrides',
 690                          'roles_assignments',
 691                          'cost',
 692                          'currancy',
 693                          'defaultrole',
 694                          'enrol',
 695                          'enrolenddate',
 696                          'enrollable',
 697                          'enrolperiod',
 698                          'enrolstartdate',
 699                          'expirynotify',
 700                          'expirythreshold',
 701                          'guest',
 702                          'notifystudents',
 703                          'password',
 704                          'student',
 705                          'students',
 706                          'teacher',
 707                          'teachers',
 708                          'metacourse',
 709                      )
 710                  )
 711              ),
 712              new convert_path(
 713                  'course_header_category', '/MOODLE_BACKUP/COURSE/HEADER/CATEGORY',
 714                  array(
 715                      'newfields' => array(
 716                          'description' => null,
 717                      )
 718                  )
 719              ),
 720          );
 721      }
 722  
 723      /**
 724       * Because there is the CATEGORY branch in the middle of the COURSE/HEADER
 725       * branch, this is dispatched twice. We use $this->coursecooked to merge
 726       * the result. Once the parser is fixed, it can be refactored.
 727       */
 728      public function process_course_header($data, $raw) {
 729         $this->course    = array_merge($this->course, $data);
 730         $this->courseraw = array_merge($this->courseraw, $raw);
 731      }
 732  
 733      public function process_course_header_category($data) {
 734          $this->category = $data;
 735      }
 736  
 737      public function on_course_header_end() {
 738  
 739          $contextid = $this->converter->get_contextid(CONTEXT_COURSE);
 740  
 741          // stash the information needed by other handlers
 742          $info = array(
 743              'original_course_id'        => $this->course['id'],
 744              'original_course_fullname'  => $this->course['fullname'],
 745              'original_course_shortname' => $this->course['shortname'],
 746              'original_course_startdate' => $this->course['startdate'],
 747              'original_course_contextid' => $contextid
 748          );
 749          $this->converter->set_stash('original_course_info', $info);
 750  
 751          $this->course['contextid'] = $contextid;
 752          $this->course['category'] = $this->category;
 753  
 754          // migrate files embedded into the course summary and stash their ids
 755          $fileman = $this->converter->get_file_manager($contextid, 'course', 'summary');
 756          $this->course['summary'] = moodle1_converter::migrate_referenced_files($this->course['summary'], $fileman);
 757          $this->converter->set_stash('course_summary_files_ids', $fileman->get_fileids());
 758  
 759          // write course.xml
 760          $this->open_xml_writer('course/course.xml');
 761          $this->write_xml('course', $this->course, array('/course/id', '/course/contextid'));
 762          $this->close_xml_writer();
 763      }
 764  }
 765  
 766  
 767  /**
 768   * Handles the conversion of course sections and course modules
 769   */
 770  class moodle1_course_outline_handler extends moodle1_xml_handler {
 771  
 772      /** @var array ordered list of the course contents */
 773      protected $coursecontents = array();
 774  
 775      /** @var array current section data */
 776      protected $currentsection;
 777  
 778      /**
 779       * This handler is interested in course sections and course modules within them
 780       */
 781      public function get_paths() {
 782          return array(
 783              new convert_path('course_sections', '/MOODLE_BACKUP/COURSE/SECTIONS'),
 784              new convert_path(
 785                  'course_section', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION',
 786                  array(
 787                      'newfields' => array(
 788                          'name'          => null,
 789                          'summaryformat' => 1,
 790                          'sequence'      => null,
 791                      ),
 792                  )
 793              ),
 794              new convert_path(
 795                  'course_module', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD',
 796                  array(
 797                      'newfields' => array(
 798                          'completion'                => 0,
 799                          'completiongradeitemnumber' => null,
 800                          'completionview'            => 0,
 801                          'completionexpected'        => 0,
 802                          'availability'              => null,
 803                          'visibleold'                => 1,
 804                          'showdescription'           => 0,
 805                      ),
 806                      'dropfields' => array(
 807                          'instance',
 808                          'roles_overrides',
 809                          'roles_assignments',
 810                      ),
 811                      'renamefields' => array(
 812                          'type' => 'modulename',
 813                      ),
 814                  )
 815              ),
 816              new convert_path('course_modules', '/MOODLE_BACKUP/COURSE/MODULES'),
 817              // todo new convert_path('course_module_roles_overrides', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_OVERRIDES'),
 818              // todo new convert_path('course_module_roles_assignments', '/MOODLE_BACKUP/COURSE/SECTIONS/SECTION/MODS/MOD/ROLES_ASSIGNMENTS'),
 819          );
 820      }
 821  
 822      public function process_course_section($data) {
 823          $this->currentsection = $data;
 824      }
 825  
 826      /**
 827       * Populates the section sequence field (order of course modules) and stashes the
 828       * course module info so that is can be dumped to activities/xxxx_x/module.xml later
 829       */
 830      public function process_course_module($data, $raw) {
 831          global $CFG;
 832  
 833          // check that this type of module should be included in the mbz
 834          $modinfo = $this->converter->get_stash_itemids('modinfo_'.$data['modulename']);
 835          if (empty($modinfo)) {
 836              return;
 837          }
 838  
 839          // add the course module into the course contents list
 840          $this->coursecontents[$data['id']] = array(
 841              'cmid'       => $data['id'],
 842              'instanceid' => $raw['INSTANCE'],
 843              'sectionid'  => $this->currentsection['id'],
 844              'modulename' => $data['modulename'],
 845              'title'      => null
 846          );
 847  
 848          // add the course module id into the section's sequence
 849          if (is_null($this->currentsection['sequence'])) {
 850              $this->currentsection['sequence'] = $data['id'];
 851          } else {
 852              $this->currentsection['sequence'] .= ',' . $data['id'];
 853          }
 854  
 855          // add the sectionid and sectionnumber
 856          $data['sectionid']      = $this->currentsection['id'];
 857          $data['sectionnumber']  = $this->currentsection['number'];
 858  
 859          // generate the module version - this is a bit tricky as this information
 860          // is not present in 1.9 backups. we will use the currently installed version
 861          // whenever we can but that might not be accurate for some modules.
 862          // also there might be problem with modules that are not present at the target
 863          // host...
 864          $versionfile = $CFG->dirroot.'/mod/'.$data['modulename'].'/version.php';
 865          if (file_exists($versionfile)) {
 866              $plugin = new stdClass();
 867              $plugin->version = null;
 868              $module = $plugin;
 869              include($versionfile);
 870              // Have to hardcode - since quiz uses some hardcoded version numbers when restoring.
 871              // This is the lowest number used minus one.
 872              $data['version'] = 2011010099;
 873          } else {
 874              $data['version'] = null;
 875          }
 876  
 877          // stash the course module info in stashes like 'cminfo_forum' with
 878          // itemid set to the instance id. this is needed so that module handlers
 879          // can later obtain information about the course module and dump it into
 880          // the module.xml file
 881          $this->converter->set_stash('cminfo_'.$data['modulename'], $data, $raw['INSTANCE']);
 882      }
 883  
 884      /**
 885       * Writes sections/section_xxx/section.xml file and stashes it, too
 886       */
 887      public function on_course_section_end() {
 888  
 889          // migrate files embedded into the section summary field
 890          $contextid = $this->converter->get_contextid(CONTEXT_COURSE);
 891          $fileman = $this->converter->get_file_manager($contextid, 'course', 'section', $this->currentsection['id']);
 892          $this->currentsection['summary'] = moodle1_converter::migrate_referenced_files($this->currentsection['summary'], $fileman);
 893  
 894          // write section's inforef.xml with the file references
 895          $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/inforef.xml');
 896          $this->xmlwriter->begin_tag('inforef');
 897          $this->xmlwriter->begin_tag('fileref');
 898          $fileids = $fileman->get_fileids();
 899          if (is_array($fileids)) {
 900              foreach ($fileids as $fileid) {
 901                  $this->write_xml('file', array('id' => $fileid));
 902              }
 903          }
 904          $this->xmlwriter->end_tag('fileref');
 905          $this->xmlwriter->end_tag('inforef');
 906          $this->close_xml_writer();
 907  
 908          // stash the section info and write section.xml
 909          $this->converter->set_stash('sectioninfo', $this->currentsection, $this->currentsection['id']);
 910          $this->open_xml_writer('sections/section_' . $this->currentsection['id'] . '/section.xml');
 911          $this->write_xml('section', $this->currentsection);
 912          $this->close_xml_writer();
 913          unset($this->currentsection);
 914      }
 915  
 916      /**
 917       * Stashes the course contents
 918       */
 919      public function on_course_sections_end() {
 920          $this->converter->set_stash('coursecontents', $this->coursecontents);
 921      }
 922  
 923      /**
 924       * Writes the information collected by mod handlers
 925       */
 926      public function on_course_modules_end() {
 927  
 928          foreach ($this->converter->get_stash('modnameslist') as $modname) {
 929              $modinfo = $this->converter->get_stash('modinfo_'.$modname);
 930              foreach ($modinfo['instances'] as $modinstanceid => $modinstance) {
 931                  $cminfo    = $this->converter->get_stash('cminfo_'.$modname, $modinstanceid);
 932                  $directory = 'activities/'.$modname.'_'.$cminfo['id'];
 933  
 934                  // write module.xml
 935                  $this->open_xml_writer($directory.'/module.xml');
 936                  $this->write_xml('module', $cminfo, array('/module/id', '/module/version'));
 937                  $this->close_xml_writer();
 938  
 939                  // write grades.xml
 940                  $this->open_xml_writer($directory.'/grades.xml');
 941                  $this->xmlwriter->begin_tag('activity_gradebook');
 942                  $gradeitems = $this->converter->get_stash_or_default('gradebook_modgradeitem_'.$modname, $modinstanceid, array());
 943                  if (!empty($gradeitems)) {
 944                      $this->xmlwriter->begin_tag('grade_items');
 945                      foreach ($gradeitems as $gradeitem) {
 946                          $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
 947                      }
 948                      $this->xmlwriter->end_tag('grade_items');
 949                  }
 950                  $this->write_xml('grade_letters', array()); // no grade_letters in module context in Moodle 1.9
 951                  $this->xmlwriter->end_tag('activity_gradebook');
 952                  $this->close_xml_writer();
 953  
 954                  // todo: write proper roles.xml, for now we just make sure the file is present
 955                  $this->make_sure_xml_exists($directory.'/roles.xml', 'roles');
 956              }
 957          }
 958      }
 959  }
 960  
 961  
 962  /**
 963   * Handles the conversion of the defined roles
 964   */
 965  class moodle1_roles_definition_handler extends moodle1_xml_handler {
 966  
 967      /**
 968       * Where the roles are defined in the source moodle.xml
 969       */
 970      public function get_paths() {
 971          return array(
 972              new convert_path('roles', '/MOODLE_BACKUP/ROLES'),
 973              new convert_path(
 974                  'roles_role', '/MOODLE_BACKUP/ROLES/ROLE',
 975                  array(
 976                      'newfields' => array(
 977                          'description'   => '',
 978                          'sortorder'     => 0,
 979                          'archetype'     => ''
 980                      )
 981                  )
 982              )
 983          );
 984      }
 985  
 986      /**
 987       * If there are any roles defined in moodle.xml, convert them to roles.xml
 988       */
 989      public function process_roles_role($data) {
 990  
 991          if (!$this->has_xml_writer()) {
 992              $this->open_xml_writer('roles.xml');
 993              $this->xmlwriter->begin_tag('roles_definition');
 994          }
 995          if (!isset($data['nameincourse'])) {
 996              $data['nameincourse'] = null;
 997          }
 998          $this->write_xml('role', $data, array('role/id'));
 999      }
1000  
1001      /**
1002       * Finishes writing roles.xml
1003       */
1004      public function on_roles_end() {
1005  
1006          if (!$this->has_xml_writer()) {
1007              // no roles defined in moodle.xml so {link self::process_roles_role()}
1008              // was never executed
1009              $this->open_xml_writer('roles.xml');
1010              $this->write_xml('roles_definition', array());
1011  
1012          } else {
1013              // some roles were dumped into the file, let us close their wrapper now
1014              $this->xmlwriter->end_tag('roles_definition');
1015          }
1016          $this->close_xml_writer();
1017      }
1018  }
1019  
1020  
1021  /**
1022   * Handles the conversion of the question bank included in the moodle.xml file
1023   */
1024  class moodle1_question_bank_handler extends moodle1_xml_handler {
1025  
1026      /** @var array the current question category being parsed */
1027      protected $currentcategory = null;
1028  
1029      /** @var array of the raw data for the current category */
1030      protected $currentcategoryraw = null;
1031  
1032      /** @var moodle1_file_manager instance used to convert question images */
1033      protected $fileman = null;
1034  
1035      /** @var bool are the currentcategory data already written (this is a work around MDL-27693) */
1036      private $currentcategorywritten = false;
1037  
1038      /** @var bool was the <questions> tag already written (work around MDL-27693) */
1039      private $questionswrapperwritten = false;
1040  
1041      /** @var array holds the instances of qtype specific conversion handlers */
1042      private $qtypehandlers = null;
1043  
1044      /**
1045       * Return the file manager instance used.
1046       *
1047       * @return moodle1_file_manager
1048       */
1049      public function get_file_manager() {
1050          return $this->fileman;
1051      }
1052  
1053      /**
1054       * Returns the information about the question category context being currently parsed
1055       *
1056       * @return array with keys contextid, contextlevel and contextinstanceid
1057       */
1058      public function get_current_category_context() {
1059          return $this->currentcategory;
1060      }
1061  
1062      /**
1063       * Registers path that are not qtype-specific
1064       */
1065      public function get_paths() {
1066  
1067          $paths = array(
1068              new convert_path('question_categories', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES'),
1069              new convert_path(
1070                  'question_category', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY',
1071                  array(
1072                      'newfields' => array(
1073                          'infoformat' => 0
1074                      )
1075                  )),
1076              new convert_path('question_category_context', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/CONTEXT'),
1077              new convert_path('questions', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS'),
1078              // the question element must be grouped so we can re-dispatch it to the qtype handler as a whole
1079              new convert_path('question', '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION', array(), true),
1080          );
1081  
1082          // annotate all question subpaths required by the qtypes subplugins
1083          $subpaths = array();
1084          foreach ($this->get_qtype_handler('*') as $qtypehandler) {
1085              foreach ($qtypehandler->get_question_subpaths() as $subpath) {
1086                  $subpaths[$subpath] = true;
1087              }
1088          }
1089          foreach (array_keys($subpaths) as $subpath) {
1090              $name = 'subquestion_'.strtolower(str_replace('/', '_', $subpath));
1091              $path = '/MOODLE_BACKUP/COURSE/QUESTION_CATEGORIES/QUESTION_CATEGORY/QUESTIONS/QUESTION/'.$subpath;
1092              $paths[] = new convert_path($name, $path);
1093          }
1094  
1095          return $paths;
1096      }
1097  
1098      /**
1099       * Starts writing questions.xml and prepares the file manager instance
1100       */
1101      public function on_question_categories_start() {
1102          $this->open_xml_writer('questions.xml');
1103          $this->xmlwriter->begin_tag('question_categories');
1104          if (is_null($this->fileman)) {
1105              $this->fileman = $this->converter->get_file_manager();
1106          }
1107      }
1108  
1109      /**
1110       * Initializes the current category cache
1111       */
1112      public function on_question_category_start() {
1113          $this->currentcategory         = array();
1114          $this->currentcategoryraw      = array();
1115          $this->currentcategorywritten  = false;
1116          $this->questionswrapperwritten = false;
1117      }
1118  
1119      /**
1120       * Populates the current question category data
1121       *
1122       * Bacuse of the known subpath-in-the-middle problem (CONTEXT in this case), this is actually
1123       * called twice for both halves of the data. We merge them here into the currentcategory array.
1124       */
1125      public function process_question_category($data, $raw) {
1126          $this->currentcategory    = array_merge($this->currentcategory, $data);
1127          $this->currentcategoryraw = array_merge($this->currentcategoryraw, $raw);
1128      }
1129  
1130      /**
1131       * Inject the context related information into the current category
1132       */
1133      public function process_question_category_context($data) {
1134  
1135          switch ($data['level']) {
1136          case 'module':
1137              $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_MODULE, $data['instance']);
1138              $this->currentcategory['contextlevel'] = CONTEXT_MODULE;
1139              $this->currentcategory['contextinstanceid'] = $data['instance'];
1140              break;
1141          case 'course':
1142              $originalcourseinfo = $this->converter->get_stash('original_course_info');
1143              $originalcourseid   = $originalcourseinfo['original_course_id'];
1144              $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSE);
1145              $this->currentcategory['contextlevel'] = CONTEXT_COURSE;
1146              $this->currentcategory['contextinstanceid'] = $originalcourseid;
1147              break;
1148          case 'coursecategory':
1149              // this is a bit hacky. the source moodle.xml defines COURSECATEGORYLEVEL as a distance
1150              // of the course category (1 = parent category, 2 = grand-parent category etc). We pretend
1151              // that this level*10 is the id of that category and create an artifical contextid for it
1152              $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_COURSECAT, $data['coursecategorylevel'] * 10);
1153              $this->currentcategory['contextlevel'] = CONTEXT_COURSECAT;
1154              $this->currentcategory['contextinstanceid'] = $data['coursecategorylevel'] * 10;
1155              break;
1156          case 'system':
1157              $this->currentcategory['contextid'] = $this->converter->get_contextid(CONTEXT_SYSTEM);
1158              $this->currentcategory['contextlevel'] = CONTEXT_SYSTEM;
1159              $this->currentcategory['contextinstanceid'] = 0;
1160              break;
1161          }
1162      }
1163  
1164      /**
1165       * Writes the common <question> data and re-dispateches the whole grouped
1166       * <QUESTION> data to the qtype for appending its qtype specific data processing
1167       *
1168       * @param array $data
1169       * @param array $raw
1170       * @return array
1171       */
1172      public function process_question(array $data, array $raw) {
1173          global $CFG;
1174  
1175          // firstly make sure that the category data and the <questions> wrapper are written
1176          // note that because of MDL-27693 we can't use {@link self::process_question_category()}
1177          // and {@link self::on_questions_start()} to do so
1178  
1179          if (empty($this->currentcategorywritten)) {
1180              $this->xmlwriter->begin_tag('question_category', array('id' => $this->currentcategory['id']));
1181              foreach ($this->currentcategory as $name => $value) {
1182                  if ($name === 'id') {
1183                      continue;
1184                  }
1185                  $this->xmlwriter->full_tag($name, $value);
1186              }
1187              $this->currentcategorywritten = true;
1188          }
1189  
1190          if (empty($this->questionswrapperwritten)) {
1191              $this->xmlwriter->begin_tag('questions');
1192              $this->questionswrapperwritten = true;
1193          }
1194  
1195          $qtype = $data['qtype'];
1196  
1197          // replay the upgrade step 2008050700 {@see question_fix_random_question_parents()}
1198          if ($qtype == 'random' and $data['parent'] <> $data['id']) {
1199              $data['parent'] = $data['id'];
1200          }
1201  
1202          // replay the upgrade step 2010080900 and part of 2010080901
1203          $data['generalfeedbackformat'] = $data['questiontextformat'];
1204          $data['oldquestiontextformat'] = $data['questiontextformat'];
1205  
1206          if ($CFG->texteditors !== 'textarea') {
1207              $data['questiontext'] = text_to_html($data['questiontext'], false, false, true);
1208              $data['questiontextformat'] = FORMAT_HTML;
1209              $data['generalfeedback'] = text_to_html($data['generalfeedback'], false, false, true);
1210              $data['generalfeedbackformat'] = FORMAT_HTML;
1211          }
1212  
1213          // Migrate files in questiontext.
1214          $this->fileman->contextid = $this->currentcategory['contextid'];
1215          $this->fileman->component = 'question';
1216          $this->fileman->filearea  = 'questiontext';
1217          $this->fileman->itemid    = $data['id'];
1218          $data['questiontext'] = moodle1_converter::migrate_referenced_files($data['questiontext'], $this->fileman);
1219  
1220          // Migrate files in generalfeedback.
1221          $this->fileman->filearea  = 'generalfeedback';
1222          $data['generalfeedback'] = moodle1_converter::migrate_referenced_files($data['generalfeedback'], $this->fileman);
1223  
1224          // replay the upgrade step 2010080901 - updating question image
1225          if (!empty($data['image'])) {
1226              if (core_text::substr(core_text::strtolower($data['image']), 0, 7) == 'http://') {
1227                  // it is a link, appending to existing question text
1228                  $data['questiontext'] .= ' <img src="' . $data['image'] . '" />';
1229  
1230              } else {
1231                  // it is a file in course_files
1232                  $filename = basename($data['image']);
1233                  $filepath = dirname($data['image']);
1234                  if (empty($filepath) or $filepath == '.' or $filepath == '/') {
1235                      $filepath = '/';
1236                  } else {
1237                      // append /
1238                      $filepath = '/'.trim($filepath, './@#$ ').'/';
1239                  }
1240  
1241                  if (file_exists($this->converter->get_tempdir_path().'/course_files'.$filepath.$filename)) {
1242                      $this->fileman->contextid = $this->currentcategory['contextid'];
1243                      $this->fileman->component = 'question';
1244                      $this->fileman->filearea  = 'questiontext';
1245                      $this->fileman->itemid    = $data['id'];
1246                      $this->fileman->migrate_file('course_files'.$filepath.$filename, '/', $filename);
1247                      // note this is slightly different from the upgrade code as we put the file into the
1248                      // root folder here. this makes our life easier as we do not need to create all the
1249                      // directories within the specified filearea/itemid
1250                      $data['questiontext'] .= ' <img src="@@PLUGINFILE@@/' . $filename . '" />';
1251  
1252                  } else {
1253                      $this->log('question file not found', backup::LOG_WARNING, array($data['id'], $filepath.$filename));
1254                  }
1255              }
1256          }
1257          unset($data['image']);
1258  
1259          // replay the upgrade step 2011060301 - Rename field defaultgrade on table question to defaultmark
1260          $data['defaultmark'] = $data['defaultgrade'];
1261  
1262          // write the common question data
1263          $this->xmlwriter->begin_tag('question', array('id' => $data['id']));
1264          foreach (array(
1265              'parent', 'name', 'questiontext', 'questiontextformat',
1266              'generalfeedback', 'generalfeedbackformat', 'defaultmark',
1267              'penalty', 'qtype', 'length', 'stamp', 'version', 'hidden',
1268              'timecreated', 'timemodified', 'createdby', 'modifiedby'
1269          ) as $fieldname) {
1270              if (!array_key_exists($fieldname, $data)) {
1271                  throw new moodle1_convert_exception('missing_common_question_field', $fieldname);
1272              }
1273              $this->xmlwriter->full_tag($fieldname, $data[$fieldname]);
1274          }
1275          // unless we know that the given qtype does not append any own structures,
1276          // give the handler a chance to do so now
1277          if (!in_array($qtype, array('description', 'random'))) {
1278              $handler = $this->get_qtype_handler($qtype);
1279              if ($handler === false) {
1280                  $this->log('question type converter not found', backup::LOG_ERROR, $qtype);
1281  
1282              } else {
1283                  $this->xmlwriter->begin_tag('plugin_qtype_'.$qtype.'_question');
1284                  $handler->use_xml_writer($this->xmlwriter);
1285                  $handler->process_question($data, $raw);
1286                  $this->xmlwriter->end_tag('plugin_qtype_'.$qtype.'_question');
1287              }
1288          }
1289  
1290          $this->xmlwriter->end_tag('question');
1291      }
1292  
1293      /**
1294       * Closes the questions wrapper
1295       */
1296      public function on_questions_end() {
1297          if ($this->questionswrapperwritten) {
1298              $this->xmlwriter->end_tag('questions');
1299          }
1300      }
1301  
1302      /**
1303       * Closes the question_category and annotates the category id
1304       * so that it can be dumped into course/inforef.xml
1305       */
1306      public function on_question_category_end() {
1307          // make sure that the category data were written by {@link self::process_question()}
1308          // if not, write it now. this may happen when the current category does not contain any
1309          // questions so the subpaths is missing completely
1310          if (empty($this->currentcategorywritten)) {
1311              $this->write_xml('question_category', $this->currentcategory, array('/question_category/id'));
1312          } else {
1313              $this->xmlwriter->end_tag('question_category');
1314          }
1315          $this->converter->set_stash('question_categories', $this->currentcategory, $this->currentcategory['id']);
1316      }
1317  
1318      /**
1319       * Stops writing questions.xml
1320       */
1321      public function on_question_categories_end() {
1322          $this->xmlwriter->end_tag('question_categories');
1323          $this->close_xml_writer();
1324      }
1325  
1326      /**
1327       * Provides access to the qtype handlers
1328       *
1329       * Returns either list of all qtype handler instances (if passed '*') or a particular handler
1330       * for the given qtype or false if the qtype is not supported.
1331       *
1332       * @throws moodle1_convert_exception
1333       * @param string $qtype the name of the question type or '*' for returning all
1334       * @return array|moodle1_qtype_handler|bool
1335       */
1336      protected function get_qtype_handler($qtype) {
1337  
1338          if (is_null($this->qtypehandlers)) {
1339              // initialize the list of qtype handler instances
1340              $this->qtypehandlers = array();
1341              foreach (core_component::get_plugin_list('qtype') as $qtypename => $qtypelocation) {
1342                  $filename = $qtypelocation.'/backup/moodle1/lib.php';
1343                  if (file_exists($filename)) {
1344                      $classname = 'moodle1_qtype_'.$qtypename.'_handler';
1345                      require_once($filename);
1346                      if (!class_exists($classname)) {
1347                          throw new moodle1_convert_exception('missing_handler_class', $classname);
1348                      }
1349                      $this->log('registering handler', backup::LOG_DEBUG, $classname, 2);
1350                      $this->qtypehandlers[$qtypename] = new $classname($this, $qtypename);
1351                  }
1352              }
1353          }
1354  
1355          if ($qtype === '*') {
1356              return $this->qtypehandlers;
1357  
1358          } else if (isset($this->qtypehandlers[$qtype])) {
1359              return $this->qtypehandlers[$qtype];
1360  
1361          } else {
1362              return false;
1363          }
1364      }
1365  }
1366  
1367  
1368  /**
1369   * Handles the conversion of the scales included in the moodle.xml file
1370   */
1371  class moodle1_scales_handler extends moodle1_handler {
1372  
1373      /** @var moodle1_file_manager instance used to convert question images */
1374      protected $fileman = null;
1375  
1376      /**
1377       * Registers paths
1378       */
1379      public function get_paths() {
1380          return array(
1381              new convert_path('scales', '/MOODLE_BACKUP/COURSE/SCALES'),
1382              new convert_path(
1383                  'scale', '/MOODLE_BACKUP/COURSE/SCALES/SCALE',
1384                  array(
1385                      'renamefields' => array(
1386                          'scaletext' => 'scale',
1387                      ),
1388                      'addfields' => array(
1389                          'descriptionformat' => 0,
1390                      )
1391                  )
1392              ),
1393          );
1394      }
1395  
1396      /**
1397       * Prepare the file manager for the files embedded in the scale description field
1398       */
1399      public function on_scales_start() {
1400          $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1401          $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'scale');
1402      }
1403  
1404      /**
1405       * This is executed every time we have one <SCALE> data available
1406       *
1407       * @param array $data
1408       * @param array $raw
1409       * @return array
1410       */
1411      public function process_scale(array $data, array $raw) {
1412          global $CFG;
1413  
1414          // replay upgrade step 2009110400
1415          if ($CFG->texteditors !== 'textarea') {
1416              $data['description'] = text_to_html($data['description'], false, false, true);
1417              $data['descriptionformat'] = FORMAT_HTML;
1418          }
1419  
1420          // convert course files embedded into the scale description field
1421          $this->fileman->itemid = $data['id'];
1422          $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
1423  
1424          // stash the scale
1425          $this->converter->set_stash('scales', $data, $data['id']);
1426      }
1427  }
1428  
1429  
1430  /**
1431   * Handles the conversion of the outcomes
1432   */
1433  class moodle1_outcomes_handler extends moodle1_xml_handler {
1434  
1435      /** @var moodle1_file_manager instance used to convert images embedded into outcome descriptions */
1436      protected $fileman = null;
1437  
1438      /**
1439       * Registers paths
1440       */
1441      public function get_paths() {
1442          return array(
1443              new convert_path('gradebook_grade_outcomes', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES'),
1444              new convert_path(
1445                  'gradebook_grade_outcome', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_OUTCOMES/GRADE_OUTCOME',
1446                  array(
1447                      'addfields' => array(
1448                          'descriptionformat' => FORMAT_MOODLE,
1449                      ),
1450                  )
1451              ),
1452          );
1453      }
1454  
1455      /**
1456       * Prepares the file manager and starts writing outcomes.xml
1457       */
1458      public function on_gradebook_grade_outcomes_start() {
1459  
1460          $syscontextid  = $this->converter->get_contextid(CONTEXT_SYSTEM);
1461          $this->fileman = $this->converter->get_file_manager($syscontextid, 'grade', 'outcome');
1462  
1463          $this->open_xml_writer('outcomes.xml');
1464          $this->xmlwriter->begin_tag('outcomes_definition');
1465      }
1466  
1467      /**
1468       * Processes GRADE_OUTCOME tags progressively
1469       */
1470      public function process_gradebook_grade_outcome(array $data, array $raw) {
1471          global $CFG;
1472  
1473          // replay the upgrade step 2009110400
1474          if ($CFG->texteditors !== 'textarea') {
1475              $data['description']       = text_to_html($data['description'], false, false, true);
1476              $data['descriptionformat'] = FORMAT_HTML;
1477          }
1478  
1479          // convert course files embedded into the outcome description field
1480          $this->fileman->itemid = $data['id'];
1481          $data['description'] = moodle1_converter::migrate_referenced_files($data['description'], $this->fileman);
1482  
1483          // write the outcome data
1484          $this->write_xml('outcome', $data, array('/outcome/id'));
1485  
1486          return $data;
1487      }
1488  
1489      /**
1490       * Closes outcomes.xml
1491       */
1492      public function on_gradebook_grade_outcomes_end() {
1493          $this->xmlwriter->end_tag('outcomes_definition');
1494          $this->close_xml_writer();
1495      }
1496  }
1497  
1498  
1499  /**
1500   * Handles the conversion of the gradebook structures in the moodle.xml file
1501   */
1502  class moodle1_gradebook_handler extends moodle1_xml_handler {
1503  
1504      /** @var array of (int)gradecategoryid => (int|null)parentcategoryid */
1505      protected $categoryparent = array();
1506  
1507      /**
1508       * Registers paths
1509       */
1510      public function get_paths() {
1511          return array(
1512              new convert_path('gradebook', '/MOODLE_BACKUP/COURSE/GRADEBOOK'),
1513              new convert_path('gradebook_grade_letter', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_LETTERS/GRADE_LETTER'),
1514              new convert_path(
1515                  'gradebook_grade_category', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_CATEGORIES/GRADE_CATEGORY',
1516                  array(
1517                      'addfields' => array(
1518                          'hidden' => 0,  // upgrade step 2010011200
1519                      ),
1520                  )
1521              ),
1522              new convert_path('gradebook_grade_item', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM'),
1523              new convert_path('gradebook_grade_item_grades', '/MOODLE_BACKUP/COURSE/GRADEBOOK/GRADE_ITEMS/GRADE_ITEM/GRADE_GRADES'),
1524          );
1525      }
1526  
1527      /**
1528       * Initializes the in-memory structures
1529       *
1530       * This should not be needed actually as the moodle.xml contains just one GRADEBOOK
1531       * element. But who knows - maybe someone will want to write a mass conversion
1532       * tool in the future (not me definitely ;-)
1533       */
1534      public function on_gradebook_start() {
1535          $this->categoryparent = array();
1536      }
1537  
1538      /**
1539       * Processes one GRADE_LETTER data
1540       *
1541       * In Moodle 1.9, all grade_letters are from course context only. Therefore
1542       * we put them here.
1543       */
1544      public function process_gradebook_grade_letter(array $data, array $raw) {
1545          $this->converter->set_stash('gradebook_gradeletter', $data, $data['id']);
1546      }
1547  
1548      /**
1549       * Processes one GRADE_CATEGORY data
1550       */
1551      public function process_gradebook_grade_category(array $data, array $raw) {
1552          $this->categoryparent[$data['id']] = $data['parent'];
1553          $this->converter->set_stash('gradebook_gradecategory', $data, $data['id']);
1554      }
1555  
1556      /**
1557       * Processes one GRADE_ITEM data
1558       */
1559      public function process_gradebook_grade_item(array $data, array $raw) {
1560  
1561          // here we use get_nextid() to get a nondecreasing sequence
1562          $data['sortorder'] = $this->converter->get_nextid();
1563  
1564          if ($data['itemtype'] === 'mod') {
1565              return $this->process_mod_grade_item($data, $raw);
1566  
1567          } else if (in_array($data['itemtype'], array('manual', 'course', 'category'))) {
1568              return $this->process_nonmod_grade_item($data, $raw);
1569  
1570          } else {
1571              $this->log('unsupported grade_item type', backup::LOG_ERROR, $data['itemtype']);
1572          }
1573      }
1574  
1575      /**
1576       * Processes one GRADE_ITEM of the type 'mod'
1577       */
1578      protected function process_mod_grade_item(array $data, array $raw) {
1579  
1580          $stashname   = 'gradebook_modgradeitem_'.$data['itemmodule'];
1581          $stashitemid = $data['iteminstance'];
1582          $gradeitems  = $this->converter->get_stash_or_default($stashname, $stashitemid, array());
1583  
1584          // typically there will be single item with itemnumber 0
1585          $gradeitems[$data['itemnumber']] = $data;
1586  
1587          $this->converter->set_stash($stashname, $gradeitems, $stashitemid);
1588  
1589          return $data;
1590      }
1591  
1592      /**
1593       * Processes one GRADE_ITEM of te type 'manual' or 'course' or 'category'
1594       */
1595      protected function process_nonmod_grade_item(array $data, array $raw) {
1596  
1597          $stashname   = 'gradebook_nonmodgradeitem';
1598          $stashitemid = $data['id'];
1599          $this->converter->set_stash($stashname, $data, $stashitemid);
1600  
1601          return $data;
1602      }
1603  
1604      /**
1605       * @todo
1606       */
1607      public function on_gradebook_grade_item_grades_start() {
1608      }
1609  
1610      /**
1611       * Writes the collected information into gradebook.xml
1612       */
1613      public function on_gradebook_end() {
1614  
1615          $this->open_xml_writer('gradebook.xml');
1616          $this->xmlwriter->begin_tag('gradebook');
1617          $this->write_grade_categories();
1618          $this->write_grade_items();
1619          $this->write_grade_letters();
1620          $this->xmlwriter->end_tag('gradebook');
1621          $this->close_xml_writer();
1622      }
1623  
1624      /**
1625       * Writes grade_categories
1626       */
1627      protected function write_grade_categories() {
1628  
1629          $this->xmlwriter->begin_tag('grade_categories');
1630          foreach ($this->converter->get_stash_itemids('gradebook_gradecategory') as $gradecategoryid) {
1631              $gradecategory = $this->converter->get_stash('gradebook_gradecategory', $gradecategoryid);
1632              $path = $this->calculate_category_path($gradecategoryid);
1633              $gradecategory['depth'] = count($path);
1634              $gradecategory['path']  = '/'.implode('/', $path).'/';
1635              $this->write_xml('grade_category', $gradecategory, array('/grade_category/id'));
1636          }
1637          $this->xmlwriter->end_tag('grade_categories');
1638      }
1639  
1640      /**
1641       * Calculates the path to the grade_category
1642       *
1643       * Moodle 1.9 backup does not store the grade_category's depth and path. This method is used
1644       * to repopulate this information using the $this->categoryparent values.
1645       *
1646       * @param int $categoryid
1647       * @return array of ids including the categoryid
1648       */
1649      protected function calculate_category_path($categoryid) {
1650  
1651          if (!array_key_exists($categoryid, $this->categoryparent)) {
1652              throw new moodle1_convert_exception('gradebook_unknown_categoryid', null, $categoryid);
1653          }
1654  
1655          $path = array($categoryid);
1656          $parent = $this->categoryparent[$categoryid];
1657          while (!is_null($parent)) {
1658              array_unshift($path, $parent);
1659              $parent = $this->categoryparent[$parent];
1660              if (in_array($parent, $path)) {
1661                  throw new moodle1_convert_exception('circular_reference_in_categories_tree');
1662              }
1663          }
1664  
1665          return $path;
1666      }
1667  
1668      /**
1669       * Writes grade_items
1670       */
1671      protected function write_grade_items() {
1672  
1673          $this->xmlwriter->begin_tag('grade_items');
1674          foreach ($this->converter->get_stash_itemids('gradebook_nonmodgradeitem') as $gradeitemid) {
1675              $gradeitem = $this->converter->get_stash('gradebook_nonmodgradeitem', $gradeitemid);
1676              $this->write_xml('grade_item', $gradeitem, array('/grade_item/id'));
1677          }
1678          $this->xmlwriter->end_tag('grade_items');
1679      }
1680  
1681      /**
1682       * Writes grade_letters
1683       */
1684      protected function write_grade_letters() {
1685  
1686          $this->xmlwriter->begin_tag('grade_letters');
1687          foreach ($this->converter->get_stash_itemids('gradebook_gradeletter') as $gradeletterid) {
1688              $gradeletter = $this->converter->get_stash('gradebook_gradeletter', $gradeletterid);
1689              $this->write_xml('grade_letter', $gradeletter, array('/grade_letter/id'));
1690          }
1691          $this->xmlwriter->end_tag('grade_letters');
1692      }
1693  }
1694  
1695  
1696  /**
1697   * Shared base class for activity modules, blocks and qtype handlers
1698   */
1699  abstract class moodle1_plugin_handler extends moodle1_xml_handler {
1700  
1701      /** @var string */
1702      protected $plugintype;
1703  
1704      /** @var string */
1705      protected $pluginname;
1706  
1707      /**
1708       * @param moodle1_converter $converter the converter that requires us
1709       * @param string $plugintype
1710       * @param string $pluginname
1711       */
1712      public function __construct(moodle1_converter $converter, $plugintype, $pluginname) {
1713  
1714          parent::__construct($converter);
1715          $this->plugintype = $plugintype;
1716          $this->pluginname = $pluginname;
1717      }
1718  
1719      /**
1720       * Returns the normalized name of the plugin, eg mod_workshop
1721       *
1722       * @return string
1723       */
1724      public function get_component_name() {
1725          return $this->plugintype.'_'.$this->pluginname;
1726      }
1727  }
1728  
1729  
1730  /**
1731   * Base class for all question type handlers
1732   */
1733  abstract class moodle1_qtype_handler extends moodle1_plugin_handler {
1734  
1735      /** @var moodle1_question_bank_handler */
1736      protected $qbankhandler;
1737  
1738      /**
1739       * Returns the list of paths within one <QUESTION> that this qtype needs to have included
1740       * in the grouped question structure
1741       *
1742       * @return array of strings
1743       */
1744      public function get_question_subpaths() {
1745          return array();
1746      }
1747  
1748      /**
1749       * Gives the qtype handler a chance to write converted data into questions.xml
1750       *
1751       * @param array $data grouped question data
1752       * @param array $raw grouped raw QUESTION data
1753       */
1754      public function process_question(array $data, array $raw) {
1755      }
1756  
1757      /**
1758       * Converts the answers and writes them into the questions.xml
1759       *
1760       * The structure "answers" is used by several qtypes. It contains data from {question_answers} table.
1761       *
1762       * @param array $answers as parsed by the grouped parser in moodle.xml
1763       * @param string $qtype containing the answers
1764       */
1765      protected function write_answers(array $answers, $qtype) {
1766  
1767          $this->xmlwriter->begin_tag('answers');
1768          foreach ($answers as $elementname => $elements) {
1769              foreach ($elements as $element) {
1770                  $answer = $this->convert_answer($element, $qtype);
1771                  // Migrate images in answertext.
1772                  if ($answer['answerformat'] == FORMAT_HTML) {
1773                      $answer['answertext'] = $this->migrate_files($answer['answertext'], 'question', 'answer', $answer['id']);
1774                  }
1775                  // Migrate images in feedback.
1776                  if ($answer['feedbackformat'] == FORMAT_HTML) {
1777                      $answer['feedback'] = $this->migrate_files($answer['feedback'], 'question', 'answerfeedback', $answer['id']);
1778                  }
1779                  $this->write_xml('answer', $answer, array('/answer/id'));
1780              }
1781          }
1782          $this->xmlwriter->end_tag('answers');
1783      }
1784  
1785      /**
1786       * Migrate files belonging to one qtype plugin text field.
1787       *
1788       * @param array $text the html fragment containing references to files
1789       * @param string $component the component for restored files
1790       * @param string $filearea the file area for restored files
1791       * @param int $itemid the itemid for restored files
1792       *
1793       * @return string the text for this field, after files references have been processed
1794       */
1795      protected function migrate_files($text, $component, $filearea, $itemid) {
1796          $context = $this->qbankhandler->get_current_category_context();
1797          $fileman = $this->qbankhandler->get_file_manager();
1798          $fileman->contextid = $context['contextid'];
1799          $fileman->component = $component;
1800          $fileman->filearea  = $filearea;
1801          $fileman->itemid    = $itemid;
1802          $text = moodle1_converter::migrate_referenced_files($text, $fileman);
1803          return $text;
1804      }
1805  
1806      /**
1807       * Writes the grouped numerical_units structure
1808       *
1809       * @param array $numericalunits
1810       */
1811      protected function write_numerical_units(array $numericalunits) {
1812  
1813          $this->xmlwriter->begin_tag('numerical_units');
1814          foreach ($numericalunits as $elementname => $elements) {
1815              foreach ($elements as $element) {
1816                  $element['id'] = $this->converter->get_nextid();
1817                  $this->write_xml('numerical_unit', $element, array('/numerical_unit/id'));
1818              }
1819          }
1820          $this->xmlwriter->end_tag('numerical_units');
1821      }
1822  
1823      /**
1824       * Writes the numerical_options structure
1825       *
1826       * @see get_default_numerical_options()
1827       * @param array $numericaloption
1828       */
1829      protected function write_numerical_options(array $numericaloption) {
1830  
1831          $this->xmlwriter->begin_tag('numerical_options');
1832          if (!empty($numericaloption)) {
1833              $this->write_xml('numerical_option', $numericaloption, array('/numerical_option/id'));
1834          }
1835          $this->xmlwriter->end_tag('numerical_options');
1836      }
1837  
1838      /**
1839       * Returns default numerical_option structure
1840       *
1841       * This structure is not present in moodle.xml, we create a new artificial one here.
1842       *
1843       * @see write_numerical_options()
1844       * @param int $oldquestiontextformat
1845       * @return array
1846       */
1847      protected function get_default_numerical_options($oldquestiontextformat, $units) {
1848          global $CFG;
1849  
1850          // replay the upgrade step 2009100100 - new table
1851          $options = array(
1852              'id'                 => $this->converter->get_nextid(),
1853              'instructions'       => null,
1854              'instructionsformat' => 0,
1855              'showunits'          => 0,
1856              'unitsleft'          => 0,
1857              'unitgradingtype'    => 0,
1858              'unitpenalty'        => 0.1
1859          );
1860  
1861          // replay the upgrade step 2009100101
1862          if ($CFG->texteditors !== 'textarea' and $oldquestiontextformat == FORMAT_MOODLE) {
1863              $options['instructionsformat'] = FORMAT_HTML;
1864          } else {
1865              $options['instructionsformat'] = $oldquestiontextformat;
1866          }
1867  
1868          // Set a good default, depending on whether there are any units defined.
1869          if (empty($units)) {
1870              $options['showunits'] = 3;
1871          }
1872  
1873          return $options;
1874      }
1875  
1876      /**
1877       * Writes the dataset_definitions structure
1878       *
1879       * @param array $datasetdefinitions array of dataset_definition structures
1880       */
1881      protected function write_dataset_definitions(array $datasetdefinitions) {
1882  
1883          $this->xmlwriter->begin_tag('dataset_definitions');
1884          foreach ($datasetdefinitions as $datasetdefinition) {
1885              $this->xmlwriter->begin_tag('dataset_definition', array('id' => $this->converter->get_nextid()));
1886              foreach (array('category', 'name', 'type', 'options', 'itemcount') as $element) {
1887                  $this->xmlwriter->full_tag($element, $datasetdefinition[$element]);
1888              }
1889              $this->xmlwriter->begin_tag('dataset_items');
1890              if (!empty($datasetdefinition['dataset_items']['dataset_item'])) {
1891                  foreach ($datasetdefinition['dataset_items']['dataset_item'] as $datasetitem) {
1892                      $datasetitem['id'] = $this->converter->get_nextid();
1893                      $this->write_xml('dataset_item', $datasetitem, array('/dataset_item/id'));
1894                  }
1895              }
1896              $this->xmlwriter->end_tag('dataset_items');
1897              $this->xmlwriter->end_tag('dataset_definition');
1898          }
1899          $this->xmlwriter->end_tag('dataset_definitions');
1900      }
1901  
1902      /// implementation details follow //////////////////////////////////////////
1903  
1904      public function __construct(moodle1_question_bank_handler $qbankhandler, $qtype) {
1905  
1906          parent::__construct($qbankhandler->get_converter(), 'qtype', $qtype);
1907          $this->qbankhandler = $qbankhandler;
1908      }
1909  
1910      /**
1911       * @see self::get_question_subpaths()
1912       */
1913      final public function get_paths() {
1914          throw new moodle1_convert_exception('qtype_handler_get_paths');
1915      }
1916  
1917      /**
1918       * Question type handlers cannot open the xml_writer
1919       */
1920      final protected function open_xml_writer($filename) {
1921          throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1922      }
1923  
1924      /**
1925       * Question type handlers cannot close the xml_writer
1926       */
1927      final protected function close_xml_writer() {
1928          throw new moodle1_convert_exception('opening_xml_writer_forbidden');
1929      }
1930  
1931      /**
1932       * Provides a xml_writer instance to this qtype converter
1933       *
1934       * @param xml_writer $xmlwriter
1935       */
1936      public function use_xml_writer(xml_writer $xmlwriter) {
1937          $this->xmlwriter = $xmlwriter;
1938      }
1939  
1940      /**
1941       * Converts <ANSWER> structure into the new <answer> one
1942       *
1943       * See question_backup_answers() in 1.9 and add_question_question_answers() in 2.0
1944       *
1945       * @param array $old the parsed answer array in moodle.xml
1946       * @param string $qtype the question type the answer is part of
1947       * @return array
1948       */
1949      private function convert_answer(array $old, $qtype) {
1950          global $CFG;
1951  
1952          $new                    = array();
1953          $new['id']              = $old['id'];
1954          $new['answertext']      = $old['answer_text'];
1955          $new['answerformat']    = 0;   // upgrade step 2010080900
1956          $new['fraction']        = $old['fraction'];
1957          $new['feedback']        = $old['feedback'];
1958          $new['feedbackformat']  = 0;   // upgrade step 2010080900
1959  
1960          // replay upgrade step 2010080901
1961          if ($qtype !== 'multichoice') {
1962              $new['answerformat'] = FORMAT_PLAIN;
1963          } else {
1964              $new['answertext'] = text_to_html($new['answertext'], false, false, true);
1965              $new['answerformat'] = FORMAT_HTML;
1966          }
1967  
1968          if ($CFG->texteditors !== 'textarea') {
1969              if ($qtype == 'essay') {
1970                  $new['feedback'] = text_to_html($new['feedback'], false, false, true);
1971              }
1972              $new['feedbackformat'] = FORMAT_HTML;
1973  
1974          } else {
1975              $new['feedbackformat'] = FORMAT_MOODLE;
1976          }
1977  
1978          return $new;
1979      }
1980  }
1981  
1982  
1983  /**
1984   * Base class for activity module handlers
1985   */
1986  abstract class moodle1_mod_handler extends moodle1_plugin_handler {
1987  
1988      /**
1989       * Returns the name of the module, eg. 'forum'
1990       *
1991       * @return string
1992       */
1993      public function get_modname() {
1994          return $this->pluginname;
1995      }
1996  
1997      /**
1998       * Returns course module information for the given instance id
1999       *
2000       * The information for this instance id has been stashed by
2001       * {@link moodle1_course_outline_handler::process_course_module()}
2002       *
2003       * @param int $instance the module instance id
2004       * @param string $modname the module type, defaults to $this->pluginname
2005       * @return int
2006       */
2007      protected function get_cminfo($instance, $modname = null) {
2008  
2009          if (is_null($modname)) {
2010              $modname = $this->pluginname;
2011          }
2012          return $this->converter->get_stash('cminfo_'.$modname, $instance);
2013      }
2014  }
2015  
2016  
2017  /**
2018   * Base class for all modules that are successors of the 1.9 resource module
2019   */
2020  abstract class moodle1_resource_successor_handler extends moodle1_mod_handler {
2021  
2022      /**
2023       * Resource successors do not attach to paths themselves, they are called explicitely
2024       * by moodle1_mod_resource_handler
2025       *
2026       * @return array
2027       */
2028      final public function get_paths() {
2029          return array();
2030      }
2031  
2032      /**
2033       * Converts /MOODLE_BACKUP/COURSE/MODULES/MOD/RESOURCE data
2034       *
2035       * Called by {@link moodle1_mod_resource_handler::process_resource()}
2036       *
2037       * @param array $data pre-cooked legacy resource data
2038       * @param array $raw raw legacy resource data
2039       */
2040      public function process_legacy_resource(array $data, array $raw = null) {
2041      }
2042  
2043      /**
2044       * Called when the parses reaches the end </MOD> resource tag
2045       *
2046       * @param array $data the data returned by {@link self::process_resource} or just pre-cooked
2047       */
2048      public function on_legacy_resource_end(array $data) {
2049      }
2050  }
2051  
2052  /**
2053   * Base class for block handlers
2054   */
2055  abstract class moodle1_block_handler extends moodle1_plugin_handler {
2056  
2057      public function get_paths() {
2058          $blockname = strtoupper($this->pluginname);
2059          return array(
2060              new convert_path('block', "/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/{$blockname}"),
2061          );
2062      }
2063  
2064      public function process_block(array $data) {
2065          $newdata = $this->convert_common_block_data($data);
2066  
2067          $this->write_block_xml($newdata, $data);
2068          $this->write_inforef_xml($newdata, $data);
2069          $this->write_roles_xml($newdata, $data);
2070  
2071          return $data;
2072      }
2073  
2074      protected function convert_common_block_data(array $olddata) {
2075          $newdata = array();
2076  
2077          $newdata['blockname'] = $olddata['name'];
2078          $newdata['parentcontextid'] = $this->converter->get_contextid(CONTEXT_COURSE, 0);
2079          $newdata['showinsubcontexts'] = 0;
2080          $newdata['pagetypepattern'] = $olddata['pagetype'].='-*';
2081          $newdata['subpagepattern'] = null;
2082          $newdata['defaultregion'] = ($olddata['position']=='l')?'side-pre':'side-post';
2083          $newdata['defaultweight'] = $olddata['weight'];
2084          $newdata['configdata'] = $this->convert_configdata($olddata);
2085  
2086          return $newdata;
2087      }
2088  
2089      protected function convert_configdata(array $olddata) {
2090          return $olddata['configdata'];
2091      }
2092  
2093      protected function write_block_xml($newdata, $data) {
2094          $contextid = $this->converter->get_contextid(CONTEXT_BLOCK, $data['id']);
2095  
2096          $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/block.xml");
2097          $this->xmlwriter->begin_tag('block', array('id' => $data['id'], 'contextid' => $contextid));
2098  
2099          foreach ($newdata as $field => $value) {
2100              $this->xmlwriter->full_tag($field, $value);
2101          }
2102  
2103          $this->xmlwriter->begin_tag('block_positions');
2104          $this->xmlwriter->begin_tag('block_position', array('id' => 1));
2105          $this->xmlwriter->full_tag('contextid', $newdata['parentcontextid']);
2106          $this->xmlwriter->full_tag('pagetype', $data['pagetype']);
2107          $this->xmlwriter->full_tag('subpage', '');
2108          $this->xmlwriter->full_tag('visible', $data['visible']);
2109          $this->xmlwriter->full_tag('region', $newdata['defaultregion']);
2110          $this->xmlwriter->full_tag('weight', $newdata['defaultweight']);
2111          $this->xmlwriter->end_tag('block_position');
2112          $this->xmlwriter->end_tag('block_positions');
2113          $this->xmlwriter->end_tag('block');
2114          $this->close_xml_writer();
2115      }
2116  
2117      protected function write_inforef_xml($newdata, $data) {
2118          $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/inforef.xml");
2119          $this->xmlwriter->begin_tag('inforef');
2120          // Subclasses may provide inforef contents if needed
2121          $this->xmlwriter->end_tag('inforef');
2122          $this->close_xml_writer();
2123      }
2124  
2125      protected function write_roles_xml($newdata, $data) {
2126          // This is an empty shell, as the moodle1 converter doesn't handle user data.
2127          $this->open_xml_writer("course/blocks/{$data['name']}_{$data['id']}/roles.xml");
2128          $this->xmlwriter->begin_tag('roles');
2129          $this->xmlwriter->full_tag('role_overrides', '');
2130          $this->xmlwriter->full_tag('role_assignments', '');
2131          $this->xmlwriter->end_tag('roles');
2132          $this->close_xml_writer();
2133      }
2134  }
2135  
2136  
2137  /**
2138   * Base class for block generic handler
2139   */
2140  class moodle1_block_generic_handler extends moodle1_block_handler {
2141  
2142  }
2143  
2144  /**
2145   * Base class for the activity modules' subplugins
2146   */
2147  abstract class moodle1_submod_handler extends moodle1_plugin_handler {
2148  
2149      /** @var moodle1_mod_handler */
2150      protected $parenthandler;
2151  
2152      /**
2153       * @param moodle1_mod_handler $parenthandler the handler of a module we are subplugin of
2154       * @param string $subplugintype the type of the subplugin
2155       * @param string $subpluginname the name of the subplugin
2156       */
2157      public function __construct(moodle1_mod_handler $parenthandler, $subplugintype, $subpluginname) {
2158          $this->parenthandler = $parenthandler;
2159          parent::__construct($parenthandler->converter, $subplugintype, $subpluginname);
2160      }
2161  
2162      /**
2163       * Activity module subplugins can't declare any paths to handle
2164       *
2165       * The paths must be registered by the parent module and then re-dispatched to the
2166       * relevant subplugins for eventual processing.
2167       *
2168       * @return array empty array
2169       */
2170      final public function get_paths() {
2171          return array();
2172      }
2173  }