PageRenderTime 101ms CodeModel.GetById 32ms RepoModel.GetById 0ms app.codeStats 2ms

/backup/moodle2/restore_stepslib.php

http://github.com/moodle/moodle
PHP | 5893 lines | 3475 code | 856 blank | 1562 comment | 630 complexity | 4b8b9201b2a1e6ecb8e1ac9d58b9d40c MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Defines various restore steps that will be used by common tasks in restore
  18. *
  19. * @package core_backup
  20. * @subpackage moodle2
  21. * @category backup
  22. * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. defined('MOODLE_INTERNAL') || die();
  26. /**
  27. * delete old directories and conditionally create backup_temp_ids table
  28. */
  29. class restore_create_and_clean_temp_stuff extends restore_execution_step {
  30. protected function define_execution() {
  31. $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
  32. // If the table already exists, it's because restore_prechecks have been executed in the same
  33. // request (without problems) and it already contains a bunch of preloaded information (users...)
  34. // that we aren't going to execute again
  35. if ($exists) { // Inform plan about preloaded information
  36. $this->task->set_preloaded_information();
  37. }
  38. // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
  39. $itemid = $this->task->get_old_contextid();
  40. $newitemid = context_course::instance($this->get_courseid())->id;
  41. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
  42. // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
  43. $itemid = $this->task->get_old_system_contextid();
  44. $newitemid = context_system::instance()->id;
  45. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
  46. // Create the old-course-id to new-course-id mapping, we need that available since the beginning
  47. $itemid = $this->task->get_old_courseid();
  48. $newitemid = $this->get_courseid();
  49. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid);
  50. }
  51. }
  52. /**
  53. * delete the temp dir used by backup/restore (conditionally),
  54. * delete old directories and drop temp ids table
  55. */
  56. class restore_drop_and_clean_temp_stuff extends restore_execution_step {
  57. protected function define_execution() {
  58. global $CFG;
  59. restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
  60. $progress = $this->task->get_progress();
  61. $progress->start_progress('Deleting backup dir');
  62. backup_helper::delete_old_backup_dirs(strtotime('-1 week'), $progress); // Delete > 1 week old temp dirs.
  63. if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
  64. backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
  65. }
  66. $progress->end_progress();
  67. }
  68. }
  69. /**
  70. * Restore calculated grade items, grade categories etc
  71. */
  72. class restore_gradebook_structure_step extends restore_structure_step {
  73. /**
  74. * To conditionally decide if this step must be executed
  75. * Note the "settings" conditions are evaluated in the
  76. * corresponding task. Here we check for other conditions
  77. * not being restore settings (files, site settings...)
  78. */
  79. protected function execute_condition() {
  80. global $CFG, $DB;
  81. if ($this->get_courseid() == SITEID) {
  82. return false;
  83. }
  84. // No gradebook info found, don't execute
  85. $fullpath = $this->task->get_taskbasepath();
  86. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  87. if (!file_exists($fullpath)) {
  88. return false;
  89. }
  90. // Some module present in backup file isn't available to restore
  91. // in this site, don't execute
  92. if ($this->task->is_missing_modules()) {
  93. return false;
  94. }
  95. // Some activity has been excluded to be restored, don't execute
  96. if ($this->task->is_excluding_activities()) {
  97. return false;
  98. }
  99. // There should only be one grade category (the 1 associated with the course itself)
  100. // If other categories already exist we're restoring into an existing course.
  101. // Restoring categories into a course with an existing category structure is unlikely to go well
  102. $category = new stdclass();
  103. $category->courseid = $this->get_courseid();
  104. $catcount = $DB->count_records('grade_categories', (array)$category);
  105. if ($catcount>1) {
  106. return false;
  107. }
  108. // Identify the backup we're dealing with.
  109. $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
  110. $backupbuild = 0;
  111. preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
  112. if (!empty($matches[1])) {
  113. $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
  114. }
  115. // On older versions the freeze value has to be converted.
  116. // We do this from here as it is happening right before the file is read.
  117. // This only targets the backup files that can contain the legacy freeze.
  118. if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
  119. $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
  120. }
  121. // Arrived here, execute the step
  122. return true;
  123. }
  124. protected function define_structure() {
  125. $paths = array();
  126. $userinfo = $this->task->get_setting_value('users');
  127. $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
  128. $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
  129. $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
  130. if ($userinfo) {
  131. $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
  132. }
  133. $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
  134. $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
  135. return $paths;
  136. }
  137. protected function process_attributes($data) {
  138. // For non-merge restore types:
  139. // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
  140. $target = $this->get_task()->get_target();
  141. if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
  142. set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
  143. }
  144. if (!empty($data['calculations_freeze'])) {
  145. if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
  146. $target == backup::TARGET_EXISTING_DELETING) {
  147. set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
  148. }
  149. }
  150. }
  151. protected function process_grade_item($data) {
  152. global $DB;
  153. $data = (object)$data;
  154. $oldid = $data->id;
  155. $data->course = $this->get_courseid();
  156. $data->courseid = $this->get_courseid();
  157. if ($data->itemtype=='manual') {
  158. // manual grade items store category id in categoryid
  159. $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
  160. // if mapping failed put in course's grade category
  161. if (NULL == $data->categoryid) {
  162. $coursecat = grade_category::fetch_course_category($this->get_courseid());
  163. $data->categoryid = $coursecat->id;
  164. }
  165. } else if ($data->itemtype=='course') {
  166. // course grade item stores their category id in iteminstance
  167. $coursecat = grade_category::fetch_course_category($this->get_courseid());
  168. $data->iteminstance = $coursecat->id;
  169. } else if ($data->itemtype=='category') {
  170. // category grade items store their category id in iteminstance
  171. $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
  172. } else {
  173. throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
  174. }
  175. $data->scaleid = $this->get_mappingid('scale', $data->scaleid, NULL);
  176. $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
  177. $data->locktime = $this->apply_date_offset($data->locktime);
  178. $coursecategory = $newitemid = null;
  179. //course grade item should already exist so updating instead of inserting
  180. if($data->itemtype=='course') {
  181. //get the ID of the already created grade item
  182. $gi = new stdclass();
  183. $gi->courseid = $this->get_courseid();
  184. $gi->itemtype = $data->itemtype;
  185. //need to get the id of the grade_category that was automatically created for the course
  186. $category = new stdclass();
  187. $category->courseid = $this->get_courseid();
  188. $category->parent = null;
  189. //course category fullname starts out as ? but may be edited
  190. //$category->fullname = '?';
  191. $coursecategory = $DB->get_record('grade_categories', (array)$category);
  192. $gi->iteminstance = $coursecategory->id;
  193. $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
  194. if (!empty($existinggradeitem)) {
  195. $data->id = $newitemid = $existinggradeitem->id;
  196. $DB->update_record('grade_items', $data);
  197. }
  198. } else if ($data->itemtype == 'manual') {
  199. // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
  200. $gi = array(
  201. 'itemtype' => $data->itemtype,
  202. 'courseid' => $data->courseid,
  203. 'itemname' => $data->itemname,
  204. 'categoryid' => $data->categoryid,
  205. );
  206. $newitemid = $DB->get_field('grade_items', 'id', $gi);
  207. }
  208. if (empty($newitemid)) {
  209. //in case we found the course category but still need to insert the course grade item
  210. if ($data->itemtype=='course' && !empty($coursecategory)) {
  211. $data->iteminstance = $coursecategory->id;
  212. }
  213. $newitemid = $DB->insert_record('grade_items', $data);
  214. $data->id = $newitemid;
  215. $gradeitem = new grade_item($data);
  216. core\event\grade_item_created::create_from_grade_item($gradeitem)->trigger();
  217. }
  218. $this->set_mapping('grade_item', $oldid, $newitemid);
  219. }
  220. protected function process_grade_grade($data) {
  221. global $DB;
  222. $data = (object)$data;
  223. $oldid = $data->id;
  224. $olduserid = $data->userid;
  225. $data->itemid = $this->get_new_parentid('grade_item');
  226. $data->userid = $this->get_mappingid('user', $data->userid, null);
  227. if (!empty($data->userid)) {
  228. $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
  229. $data->locktime = $this->apply_date_offset($data->locktime);
  230. $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
  231. if ($gradeexists) {
  232. $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
  233. $this->log($message, backup::LOG_DEBUG);
  234. } else {
  235. $newitemid = $DB->insert_record('grade_grades', $data);
  236. $this->set_mapping('grade_grades', $oldid, $newitemid);
  237. }
  238. } else {
  239. $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
  240. $this->log($message, backup::LOG_DEBUG);
  241. }
  242. }
  243. protected function process_grade_category($data) {
  244. global $DB;
  245. $data = (object)$data;
  246. $oldid = $data->id;
  247. $data->course = $this->get_courseid();
  248. $data->courseid = $data->course;
  249. $newitemid = null;
  250. //no parent means a course level grade category. That may have been created when the course was created
  251. if(empty($data->parent)) {
  252. //parent was being saved as 0 when it should be null
  253. $data->parent = null;
  254. //get the already created course level grade category
  255. $category = new stdclass();
  256. $category->courseid = $this->get_courseid();
  257. $category->parent = null;
  258. $coursecategory = $DB->get_record('grade_categories', (array)$category);
  259. if (!empty($coursecategory)) {
  260. $data->id = $newitemid = $coursecategory->id;
  261. $DB->update_record('grade_categories', $data);
  262. }
  263. }
  264. // Add a warning about a removed setting.
  265. if (!empty($data->aggregatesubcats)) {
  266. set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
  267. }
  268. //need to insert a course category
  269. if (empty($newitemid)) {
  270. $newitemid = $DB->insert_record('grade_categories', $data);
  271. }
  272. $this->set_mapping('grade_category', $oldid, $newitemid);
  273. }
  274. protected function process_grade_letter($data) {
  275. global $DB;
  276. $data = (object)$data;
  277. $oldid = $data->id;
  278. $data->contextid = context_course::instance($this->get_courseid())->id;
  279. $gradeletter = (array)$data;
  280. unset($gradeletter['id']);
  281. if (!$DB->record_exists('grade_letters', $gradeletter)) {
  282. $newitemid = $DB->insert_record('grade_letters', $data);
  283. } else {
  284. $newitemid = $data->id;
  285. }
  286. $this->set_mapping('grade_letter', $oldid, $newitemid);
  287. }
  288. protected function process_grade_setting($data) {
  289. global $DB;
  290. $data = (object)$data;
  291. $oldid = $data->id;
  292. $data->courseid = $this->get_courseid();
  293. $target = $this->get_task()->get_target();
  294. if ($data->name == 'minmaxtouse' &&
  295. ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
  296. // We never restore minmaxtouse during merge.
  297. return;
  298. }
  299. if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
  300. $newitemid = $DB->insert_record('grade_settings', $data);
  301. } else {
  302. $newitemid = $data->id;
  303. }
  304. if (!empty($oldid)) {
  305. // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
  306. $this->set_mapping('grade_setting', $oldid, $newitemid);
  307. }
  308. }
  309. /**
  310. * put all activity grade items in the correct grade category and mark all for recalculation
  311. */
  312. protected function after_execute() {
  313. global $DB;
  314. $conditions = array(
  315. 'backupid' => $this->get_restoreid(),
  316. 'itemname' => 'grade_item'//,
  317. //'itemid' => $itemid
  318. );
  319. $rs = $DB->get_recordset('backup_ids_temp', $conditions);
  320. // We need this for calculation magic later on.
  321. $mappings = array();
  322. if (!empty($rs)) {
  323. foreach($rs as $grade_item_backup) {
  324. // Store the oldid with the new id.
  325. $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
  326. $updateobj = new stdclass();
  327. $updateobj->id = $grade_item_backup->newitemid;
  328. //if this is an activity grade item that needs to be put back in its correct category
  329. if (!empty($grade_item_backup->parentitemid)) {
  330. $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
  331. if (!is_null($oldcategoryid)) {
  332. $updateobj->categoryid = $oldcategoryid;
  333. $DB->update_record('grade_items', $updateobj);
  334. }
  335. } else {
  336. //mark course and category items as needing to be recalculated
  337. $updateobj->needsupdate=1;
  338. $DB->update_record('grade_items', $updateobj);
  339. }
  340. }
  341. }
  342. $rs->close();
  343. // We need to update the calculations for calculated grade items that may reference old
  344. // grade item ids using ##gi\d+##.
  345. // $mappings can be empty, use 0 if so (won't match ever)
  346. list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
  347. $sql = "SELECT gi.id, gi.calculation
  348. FROM {grade_items} gi
  349. WHERE gi.id {$sql} AND
  350. calculation IS NOT NULL";
  351. $rs = $DB->get_recordset_sql($sql, $params);
  352. foreach ($rs as $gradeitem) {
  353. // Collect all of the used grade item id references
  354. if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
  355. // This calculation doesn't reference any other grade items... EASY!
  356. continue;
  357. }
  358. // For this next bit we are going to do the replacement of id's in two steps:
  359. // 1. We will replace all old id references with a special mapping reference.
  360. // 2. We will replace all mapping references with id's
  361. // Why do we do this?
  362. // Because there potentially there will be an overlap of ids within the query and we
  363. // we substitute the wrong id.. safest way around this is the two step system
  364. $calculationmap = array();
  365. $mapcount = 0;
  366. foreach ($matches[1] as $match) {
  367. // Check that the old id is known to us, if not it was broken to begin with and will
  368. // continue to be broken.
  369. if (!array_key_exists($match, $mappings)) {
  370. continue;
  371. }
  372. // Our special mapping key
  373. $mapping = '##MAPPING'.$mapcount.'##';
  374. // The old id that exists within the calculation now
  375. $oldid = '##gi'.$match.'##';
  376. // The new id that we want to replace the old one with.
  377. $newid = '##gi'.$mappings[$match].'##';
  378. // Replace in the special mapping key
  379. $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
  380. // And record the mapping
  381. $calculationmap[$mapping] = $newid;
  382. $mapcount++;
  383. }
  384. // Iterate all special mappings for this calculation and replace in the new id's
  385. foreach ($calculationmap as $mapping => $newid) {
  386. $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
  387. }
  388. // Update the calculation now that its being remapped
  389. $DB->update_record('grade_items', $gradeitem);
  390. }
  391. $rs->close();
  392. // Need to correct the grade category path and parent
  393. $conditions = array(
  394. 'courseid' => $this->get_courseid()
  395. );
  396. $rs = $DB->get_recordset('grade_categories', $conditions);
  397. // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
  398. foreach ($rs as $gc) {
  399. if (!empty($gc->parent)) {
  400. $grade_category = new stdClass();
  401. $grade_category->id = $gc->id;
  402. $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
  403. $DB->update_record('grade_categories', $grade_category);
  404. }
  405. }
  406. $rs->close();
  407. // Now we can rebuild all the paths
  408. $rs = $DB->get_recordset('grade_categories', $conditions);
  409. foreach ($rs as $gc) {
  410. $grade_category = new stdClass();
  411. $grade_category->id = $gc->id;
  412. $grade_category->path = grade_category::build_path($gc);
  413. $grade_category->depth = substr_count($grade_category->path, '/') - 1;
  414. $DB->update_record('grade_categories', $grade_category);
  415. }
  416. $rs->close();
  417. // Check what to do with the minmaxtouse setting.
  418. $this->check_minmaxtouse();
  419. // Freeze gradebook calculations if needed.
  420. $this->gradebook_calculation_freeze();
  421. // Ensure the module cache is current when recalculating grades.
  422. rebuild_course_cache($this->get_courseid(), true);
  423. // Restore marks items as needing update. Update everything now.
  424. grade_regrade_final_grades($this->get_courseid());
  425. }
  426. /**
  427. * Freeze gradebook calculation if needed.
  428. *
  429. * This is similar to various upgrade scripts that check if the freeze is needed.
  430. */
  431. protected function gradebook_calculation_freeze() {
  432. global $CFG;
  433. $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
  434. preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
  435. $backupbuild = (int)$matches[1];
  436. // The function floatval will return a float even if there is text mixed with the release number.
  437. $backuprelease = floatval($this->get_task()->get_info()->backup_release);
  438. // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
  439. if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
  440. require_once($CFG->libdir . '/db/upgradelib.php');
  441. upgrade_extra_credit_weightoverride($this->get_courseid());
  442. }
  443. // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
  444. if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
  445. require_once($CFG->libdir . '/db/upgradelib.php');
  446. upgrade_calculated_grade_items($this->get_courseid());
  447. }
  448. // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
  449. // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
  450. // be checked for this problem.
  451. if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || $backuprelease <= 2.9)) {
  452. require_once($CFG->libdir . '/db/upgradelib.php');
  453. upgrade_course_letter_boundary($this->get_courseid());
  454. }
  455. }
  456. /**
  457. * Checks what should happen with the course grade setting minmaxtouse.
  458. *
  459. * This is related to the upgrade step at the time the setting was added.
  460. *
  461. * @see MDL-48618
  462. * @return void
  463. */
  464. protected function check_minmaxtouse() {
  465. global $CFG, $DB;
  466. require_once($CFG->libdir . '/gradelib.php');
  467. $userinfo = $this->task->get_setting_value('users');
  468. $settingname = 'minmaxtouse';
  469. $courseid = $this->get_courseid();
  470. $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
  471. $version28start = 2014111000.00;
  472. $version28last = 2014111006.05;
  473. $version29start = 2015051100.00;
  474. $version29last = 2015060400.02;
  475. $target = $this->get_task()->get_target();
  476. if ($minmaxtouse === false &&
  477. ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
  478. // The setting was not found because this setting did not exist at the time the backup was made.
  479. // And we are not restoring as merge, in which case we leave the course as it was.
  480. $version = $this->get_task()->get_info()->moodle_version;
  481. if ($version < $version28start) {
  482. // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
  483. if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
  484. grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
  485. }
  486. } else if (($version >= $version28start && $version < $version28last) ||
  487. ($version >= $version29start && $version < $version29last)) {
  488. // They should be using grade_grade when the course has inconsistencies.
  489. $sql = "SELECT gi.id
  490. FROM {grade_items} gi
  491. JOIN {grade_grades} gg
  492. ON gg.itemid = gi.id
  493. WHERE gi.courseid = ?
  494. AND (gi.itemtype != ? AND gi.itemtype != ?)
  495. AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
  496. // The course can only have inconsistencies when we restore the user info,
  497. // we do not need to act on existing grades that were not restored as part of this backup.
  498. if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
  499. // Display the notice as we do during upgrade.
  500. set_config('show_min_max_grades_changed_' . $courseid, 1);
  501. if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
  502. // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
  503. // If they are using the site-wide grade_grade setting, we only want to notice them.
  504. grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
  505. }
  506. }
  507. } else {
  508. // This should never happen because from now on minmaxtouse is always saved in backups.
  509. }
  510. }
  511. }
  512. /**
  513. * Rewrite step definition to handle the legacy freeze attribute.
  514. *
  515. * In previous backups the calculations_freeze property was stored as an attribute of the
  516. * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
  517. * It only processes definitive children, and their parent attributes.
  518. *
  519. * We had:
  520. *
  521. * <gradebook calculations_freeze="20160511">
  522. * <grade_categories>
  523. * <grade_category id="10">
  524. * <depth>1</depth>
  525. * ...
  526. * </grade_category>
  527. * </grade_categories>
  528. * ...
  529. * </gradebook>
  530. *
  531. * And this method will convert it to:
  532. *
  533. * <gradebook >
  534. * <attributes>
  535. * <calculations_freeze>20160511</calculations_freeze>
  536. * </attributes>
  537. * <grade_categories>
  538. * <grade_category id="10">
  539. * <depth>1</depth>
  540. * ...
  541. * </grade_category>
  542. * </grade_categories>
  543. * ...
  544. * </gradebook>
  545. *
  546. * Note that we cannot just load the XML file in memory as it could potentially be huge.
  547. * We can also completely ignore if the node <attributes> is already in the backup
  548. * file as it never existed before.
  549. *
  550. * @param string $filepath The absolute path to the XML file.
  551. * @return void
  552. */
  553. protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
  554. $foundnode = false;
  555. $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
  556. $fr = fopen($filepath, 'r');
  557. $fw = fopen($newfile, 'w');
  558. if ($fr && $fw) {
  559. while (($line = fgets($fr, 4096)) !== false) {
  560. if (!$foundnode && strpos($line, '<gradebook ') === 0) {
  561. $foundnode = true;
  562. $matches = array();
  563. $pattern = '@calculations_freeze=.([0-9]+).@';
  564. if (preg_match($pattern, $line, $matches)) {
  565. $freeze = $matches[1];
  566. $line = preg_replace($pattern, '', $line);
  567. $line .= " <attributes>\n <calculations_freeze>$freeze</calculations_freeze>\n </attributes>\n";
  568. }
  569. }
  570. fputs($fw, $line);
  571. }
  572. if (!feof($fr)) {
  573. throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
  574. }
  575. fclose($fr);
  576. fclose($fw);
  577. if (!rename($newfile, $filepath)) {
  578. throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
  579. }
  580. } else {
  581. if ($fr) {
  582. fclose($fr);
  583. }
  584. if ($fw) {
  585. fclose($fw);
  586. }
  587. }
  588. }
  589. }
  590. /**
  591. * Step in charge of restoring the grade history of a course.
  592. *
  593. * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
  594. * we do not want to restore the history if the gradebook and its content has not been
  595. * restored. At least for now.
  596. */
  597. class restore_grade_history_structure_step extends restore_structure_step {
  598. protected function execute_condition() {
  599. global $CFG, $DB;
  600. if ($this->get_courseid() == SITEID) {
  601. return false;
  602. }
  603. // No gradebook info found, don't execute.
  604. $fullpath = $this->task->get_taskbasepath();
  605. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  606. if (!file_exists($fullpath)) {
  607. return false;
  608. }
  609. // Some module present in backup file isn't available to restore in this site, don't execute.
  610. if ($this->task->is_missing_modules()) {
  611. return false;
  612. }
  613. // Some activity has been excluded to be restored, don't execute.
  614. if ($this->task->is_excluding_activities()) {
  615. return false;
  616. }
  617. // There should only be one grade category (the 1 associated with the course itself).
  618. $category = new stdclass();
  619. $category->courseid = $this->get_courseid();
  620. $catcount = $DB->count_records('grade_categories', (array)$category);
  621. if ($catcount > 1) {
  622. return false;
  623. }
  624. // Arrived here, execute the step.
  625. return true;
  626. }
  627. protected function define_structure() {
  628. $paths = array();
  629. // Settings to use.
  630. $userinfo = $this->get_setting_value('users');
  631. $history = $this->get_setting_value('grade_histories');
  632. if ($userinfo && $history) {
  633. $paths[] = new restore_path_element('grade_grade',
  634. '/grade_history/grade_grades/grade_grade');
  635. }
  636. return $paths;
  637. }
  638. protected function process_grade_grade($data) {
  639. global $DB;
  640. $data = (object)($data);
  641. $olduserid = $data->userid;
  642. unset($data->id);
  643. $data->userid = $this->get_mappingid('user', $data->userid, null);
  644. if (!empty($data->userid)) {
  645. // Do not apply the date offsets as this is history.
  646. $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
  647. $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
  648. $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
  649. $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
  650. $DB->insert_record('grade_grades_history', $data);
  651. } else {
  652. $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
  653. $this->log($message, backup::LOG_DEBUG);
  654. }
  655. }
  656. }
  657. /**
  658. * decode all the interlinks present in restored content
  659. * relying 100% in the restore_decode_processor that handles
  660. * both the contents to modify and the rules to be applied
  661. */
  662. class restore_decode_interlinks extends restore_execution_step {
  663. protected function define_execution() {
  664. // Get the decoder (from the plan)
  665. $decoder = $this->task->get_decoder();
  666. restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
  667. // And launch it, everything will be processed
  668. $decoder->execute();
  669. }
  670. }
  671. /**
  672. * first, ensure that we have no gaps in section numbers
  673. * and then, rebuid the course cache
  674. */
  675. class restore_rebuild_course_cache extends restore_execution_step {
  676. protected function define_execution() {
  677. global $DB;
  678. // Although there is some sort of auto-recovery of missing sections
  679. // present in course/formats... here we check that all the sections
  680. // from 0 to MAX(section->section) exist, creating them if necessary
  681. $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
  682. // Iterate over all sections
  683. for ($i = 0; $i <= $maxsection; $i++) {
  684. // If the section $i doesn't exist, create it
  685. if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
  686. $sectionrec = array(
  687. 'course' => $this->get_courseid(),
  688. 'section' => $i,
  689. 'timemodified' => time());
  690. $DB->insert_record('course_sections', $sectionrec); // missing section created
  691. }
  692. }
  693. // Rebuild cache now that all sections are in place
  694. rebuild_course_cache($this->get_courseid());
  695. cache_helper::purge_by_event('changesincourse');
  696. cache_helper::purge_by_event('changesincoursecat');
  697. }
  698. }
  699. /**
  700. * Review all the tasks having one after_restore method
  701. * executing it to perform some final adjustments of information
  702. * not available when the task was executed.
  703. */
  704. class restore_execute_after_restore extends restore_execution_step {
  705. protected function define_execution() {
  706. // Simply call to the execute_after_restore() method of the task
  707. // that always is the restore_final_task
  708. $this->task->launch_execute_after_restore();
  709. }
  710. }
  711. /**
  712. * Review all the (pending) block positions in backup_ids, matching by
  713. * contextid, creating positions as needed. This is executed by the
  714. * final task, once all the contexts have been created
  715. */
  716. class restore_review_pending_block_positions extends restore_execution_step {
  717. protected function define_execution() {
  718. global $DB;
  719. // Get all the block_position objects pending to match
  720. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
  721. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
  722. // Process block positions, creating them or accumulating for final step
  723. foreach($rs as $posrec) {
  724. // Get the complete position object out of the info field.
  725. $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
  726. // If position is for one already mapped (known) contextid
  727. // process it now, creating the position, else nothing to
  728. // do, position finally discarded
  729. if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
  730. $position->contextid = $newctx->newitemid;
  731. // Create the block position
  732. $DB->insert_record('block_positions', $position);
  733. }
  734. }
  735. $rs->close();
  736. }
  737. }
  738. /**
  739. * Updates the availability data for course modules and sections.
  740. *
  741. * Runs after the restore of all course modules, sections, and grade items has
  742. * completed. This is necessary in order to update IDs that have changed during
  743. * restore.
  744. *
  745. * @package core_backup
  746. * @copyright 2014 The Open University
  747. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  748. */
  749. class restore_update_availability extends restore_execution_step {
  750. protected function define_execution() {
  751. global $CFG, $DB;
  752. // Note: This code runs even if availability is disabled when restoring.
  753. // That will ensure that if you later turn availability on for the site,
  754. // there will be no incorrect IDs. (It doesn't take long if the restored
  755. // data does not contain any availability information.)
  756. // Get modinfo with all data after resetting cache.
  757. rebuild_course_cache($this->get_courseid(), true);
  758. $modinfo = get_fast_modinfo($this->get_courseid());
  759. // Get the date offset for this restore.
  760. $dateoffset = $this->apply_date_offset(1) - 1;
  761. // Update all sections that were restored.
  762. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
  763. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
  764. $sectionsbyid = null;
  765. foreach ($rs as $rec) {
  766. if (is_null($sectionsbyid)) {
  767. $sectionsbyid = array();
  768. foreach ($modinfo->get_section_info_all() as $section) {
  769. $sectionsbyid[$section->id] = $section;
  770. }
  771. }
  772. if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
  773. // If the section was not fully restored for some reason
  774. // (e.g. due to an earlier error), skip it.
  775. $this->get_logger()->process('Section not fully restored: id ' .
  776. $rec->newitemid, backup::LOG_WARNING);
  777. continue;
  778. }
  779. $section = $sectionsbyid[$rec->newitemid];
  780. if (!is_null($section->availability)) {
  781. $info = new \core_availability\info_section($section);
  782. $info->update_after_restore($this->get_restoreid(),
  783. $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
  784. }
  785. }
  786. $rs->close();
  787. // Update all modules that were restored.
  788. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
  789. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
  790. foreach ($rs as $rec) {
  791. if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
  792. // If the module was not fully restored for some reason
  793. // (e.g. due to an earlier error), skip it.
  794. $this->get_logger()->process('Module not fully restored: id ' .
  795. $rec->newitemid, backup::LOG_WARNING);
  796. continue;
  797. }
  798. $cm = $modinfo->get_cm($rec->newitemid);
  799. if (!is_null($cm->availability)) {
  800. $info = new \core_availability\info_module($cm);
  801. $info->update_after_restore($this->get_restoreid(),
  802. $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
  803. }
  804. }
  805. $rs->close();
  806. }
  807. }
  808. /**
  809. * Process legacy module availability records in backup_ids.
  810. *
  811. * Matches course modules and grade item id once all them have been already restored.
  812. * Only if all matchings are satisfied the availability condition will be created.
  813. * At the same time, it is required for the site to have that functionality enabled.
  814. *
  815. * This step is included only to handle legacy backups (2.6 and before). It does not
  816. * do anything for newer backups.
  817. *
  818. * @copyright 2014 The Open University
  819. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  820. */
  821. class restore_process_course_modules_availability extends restore_execution_step {
  822. protected function define_execution() {
  823. global $CFG, $DB;
  824. // Site hasn't availability enabled
  825. if (empty($CFG->enableavailability)) {
  826. return;
  827. }
  828. // Do both modules and sections.
  829. foreach (array('module', 'section') as $table) {
  830. // Get all the availability objects to process.
  831. $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
  832. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
  833. // Process availabilities, creating them if everything matches ok.
  834. foreach ($rs as $availrec) {
  835. $allmatchesok = true;
  836. // Get the complete legacy availability object.
  837. $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
  838. // Note: This code used to update IDs, but that is now handled by the
  839. // current code (after restore) instead of this legacy code.
  840. // Get showavailability option.
  841. $thingid = ($table === 'module') ? $availability->coursemoduleid :
  842. $availability->coursesectionid;
  843. $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
  844. $table . '_showavailability', $thingid);
  845. if (!$showrec) {
  846. // Should not happen.
  847. throw new coding_exception('No matching showavailability record');
  848. }
  849. $show = $showrec->info->showavailability;
  850. // The $availability object is now in the format used in the old
  851. // system. Interpret this and convert to new system.
  852. $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
  853. array('id' => $thingid), MUST_EXIST);
  854. $newvalue = \core_availability\info::add_legacy_availability_condition(
  855. $currentvalue, $availability, $show);
  856. $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
  857. array('id' => $thingid));
  858. }
  859. $rs->close();
  860. }
  861. }
  862. }
  863. /*
  864. * Execution step that, *conditionally* (if there isn't preloaded information)
  865. * will load the inforef files for all the included course/section/activity tasks
  866. * to backup_temp_ids. They will be stored with "xxxxref" as itemname
  867. */
  868. class restore_load_included_inforef_records extends restore_execution_step {
  869. protected function define_execution() {
  870. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  871. return;
  872. }
  873. // Get all the included tasks
  874. $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
  875. $progress = $this->task->get_progress();
  876. $progress->start_progress($this->get_name(), count($tasks));
  877. foreach ($tasks as $task) {
  878. // Load the inforef.xml file if exists
  879. $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
  880. if (file_exists($inforefpath)) {
  881. // Load each inforef file to temp_ids.
  882. restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
  883. }
  884. }
  885. $progress->end_progress();
  886. }
  887. }
  888. /*
  889. * Execution step that will load all the needed files into backup_files_temp
  890. * - info: contains the whole original object (times, names...)
  891. * (all them being original ids as loaded from xml)
  892. */
  893. class restore_load_included_files extends restore_structure_step {
  894. protected function define_structure() {
  895. $file = new restore_path_element('file', '/files/file');
  896. return array($file);
  897. }
  898. /**
  899. * Process one <file> element from files.xml
  900. *
  901. * @param array $data the element data
  902. */
  903. public function process_file($data) {
  904. $data = (object)$data; // handy
  905. // load it if needed:
  906. // - it it is one of the annotated inforef files (course/section/activity/block)
  907. // - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
  908. // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
  909. // but then we'll need to change it to load plugins itself (because this is executed too early in restore)
  910. $isfileref = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
  911. $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
  912. $data->component == 'grouping' || $data->component == 'grade' ||
  913. $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
  914. if ($isfileref || $iscomponent) {
  915. restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
  916. }
  917. }
  918. }
  919. /**
  920. * Execution step that, *conditionally* (if there isn't preloaded information),
  921. * will load all the needed roles to backup_temp_ids. They will be stored with
  922. * "role" itemname. Also it will perform one automatic mapping to roles existing
  923. * in the target site, based in permissions of the user performing the restore,
  924. * archetypes and other bits. At the end, each original role will have its associated
  925. * target role or 0 if it's going to be skipped. Note we wrap everything over one
  926. * restore_dbops method, as far as the same stuff is going to be also executed
  927. * by restore prechecks
  928. */
  929. class restore_load_and_map_roles extends restore_execution_step {
  930. protected function define_execution() {
  931. if ($this->task->get_preloaded_information()) { // if info is already preloaded
  932. return;
  933. }
  934. $file = $this->get_basepath() . '/roles.xml';
  935. // Load needed toles to temp_ids
  936. restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
  937. // Process roles, mapping/skipping. Any error throws exception
  938. // Note we pass controller's info because it can contain role mapping information
  939. // about manual mappings performed by UI
  940. restore_dbops::process_included_roles($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_info()->role_mappings);
  941. }
  942. }
  943. /**
  944. * Execution step that, *conditionally* (if there isn't preloaded information
  945. * and users have been selected in settings, will load all the needed users
  946. * to backup_temp_ids. They will be stored with "user" itemname and with
  947. * their original contextid as paremitemid
  948. */
  949. class restore_load_included_users extends restore_execution_step {
  950. protected function define_execution() {
  951. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  952. return;
  953. }
  954. if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
  955. return;
  956. }
  957. $file = $this->get_basepath() . '/users.xml';
  958. // Load needed users to temp_ids.
  959. restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
  960. }
  961. }
  962. /**
  963. * Execution step that, *conditionally* (if there isn't preloaded information
  964. * and users have been selected in settings, will process all the needed users
  965. * in order to decide and perform any action with them (create / map / error)
  966. * Note: Any error will cause exception, as far as this is the same processing
  967. * than the one into restore prechecks (that should have stopped process earlier)
  968. */
  969. class restore_process_included_users extends restore_execution_step {
  970. protected function define_execution() {
  971. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  972. return;
  973. }
  974. if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
  975. return;
  976. }
  977. restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
  978. $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
  979. }
  980. }
  981. /**
  982. * Execution step that will create all the needed users as calculated
  983. * by @restore_process_included_users (those having newiteind = 0)
  984. */
  985. class restore_create_included_users extends restore_execution_step {
  986. protected function define_execution() {
  987. restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
  988. $this->task->get_userid(), $this->task->get_progress());
  989. }
  990. }
  991. /**
  992. * Structure step that will create all the needed groups and groupings
  993. * by loading them from the groups.xml file performing the required matches.
  994. * Note group members only will be added if restoring user info
  995. */
  996. class restore_groups_structure_step extends restore_structure_step {
  997. protected function define_structure() {
  998. $paths = array(); // Add paths here
  999. // Do not include group/groupings information if not requested.
  1000. $groupinfo = $this->get_setting_value('groups');
  1001. if ($groupinfo) {
  1002. $paths[] = new restore_path_element('group', '/groups/group');
  1003. $paths[] = new restore_path_element('grouping', '/groups/groupings/grouping');
  1004. $paths[] = new restore_path_element('grouping_group', '/groups/groupings/grouping/grouping_groups/grouping_group');
  1005. }
  1006. return $paths;
  1007. }
  1008. // Processing functions go here
  1009. public function process_group($data) {
  1010. global $DB;
  1011. $data = (object)$data; // handy
  1012. $data->courseid = $this->get_courseid();
  1013. // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
  1014. // another a group in the same course
  1015. $context = context_course::instance($data->courseid);
  1016. if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
  1017. if (groups_get_group_by_idnumber($data->courseid, $data->idnumber)) {
  1018. unset($data->idnumber);
  1019. }
  1020. } else {
  1021. unset($data->idnumber);
  1022. }
  1023. $oldid = $data->id; // need this saved for later
  1024. $restorefiles = false; // Only if we end creating the group
  1025. // Search if the group already exists (by name & description) in the target course
  1026. $description_clause = '';
  1027. $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
  1028. if (!empty($data->description)) {
  1029. $description_clause = ' AND ' .
  1030. $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
  1031. $params['description'] = $data->description;
  1032. }
  1033. if (!$groupdb = $DB->get_record_sql("SELECT *
  1034. FROM {groups}
  1035. WHERE courseid = :courseid
  1036. AND name = :grname $description_clause", $params)) {
  1037. // group doesn't exist, create
  1038. $newitemid = $DB->insert_record('groups', $data);
  1039. $restorefiles = true; // We'll restore the files
  1040. } else {
  1041. // group exists, use it
  1042. $newitemid = $groupdb->id;
  1043. }
  1044. // Save the id mapping
  1045. $this->set_mapping('group', $oldid, $newitemid, $restorefiles);
  1046. // Invalidate the course group data cache just in case.
  1047. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
  1048. }
  1049. public function process_grouping($data) {
  1050. global $DB;
  1051. $data = (object)$data; // handy
  1052. $data->courseid = $this->get_courseid();
  1053. // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
  1054. // another a grouping in the same course
  1055. $context = context_course::instance($data->courseid);
  1056. if (isset($data->idnumber) and has_capability('moodle/course:changeidnumber', $context, $this->task->get_userid())) {
  1057. if (groups_get_grouping_by_idnumber($data->courseid, $data->idnumber)) {
  1058. unset($data->idnumber);
  1059. }
  1060. } else {
  1061. unset($data->idnumber);
  1062. }
  1063. $oldid = $data->id; // need this saved for later
  1064. $restorefiles = false; // Only if we end creating the grouping
  1065. // Search if the grouping already exists (by name & description) in the target course
  1066. $description_clause = '';
  1067. $params = array('courseid' => $this->get_courseid(), 'grname' => $data->name);
  1068. if (!empty($data->description)) {
  1069. $description_clause = ' AND ' .
  1070. $DB->sql_compare_text('description') . ' = ' . $DB->sql_compare_text(':description');
  1071. $params['description'] = $data->description;
  1072. }
  1073. if (!$groupingdb = $DB->get_record_sql("SELECT *
  1074. FROM {groupings}
  1075. WHERE courseid = :courseid
  1076. AND name = :grname $description_clause", $params)) {
  1077. // grouping doesn't exist, create
  1078. $newitemid = $DB->insert_record('groupings', $data);
  1079. $restorefiles = true; // We'll restore the files
  1080. } else {
  1081. // grouping exists, use it
  1082. $newitemid = $groupingdb->id;
  1083. }
  1084. // Save the id mapping
  1085. $this->set_mapping('grouping', $oldid, $newitemid, $restorefiles);
  1086. // Invalidate the course group data cache just in case.
  1087. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($data->courseid));
  1088. }
  1089. public function process_grouping_group($data) {
  1090. global $CFG;
  1091. require_once($CFG->dirroot.'/group/lib.php');
  1092. $data = (object)$data;
  1093. groups_assign_grouping($this->get_new_parentid('grouping'), $this->get_mappingid('group', $data->groupid), $data->timeadded);
  1094. }
  1095. protected function after_execute() {
  1096. // Add group related files, matching with "group" mappings
  1097. $this->add_related_files('group', 'icon', 'group');
  1098. $this->add_related_files('group', 'description', 'group');
  1099. // Add grouping related files, matching with "grouping" mappings
  1100. $this->add_related_files('grouping', 'description', 'grouping');
  1101. // Invalidate the course group data.
  1102. cache_helper::invalidate_by_definition('core', 'groupdata', array(), array($this->get_courseid()));
  1103. }
  1104. }
  1105. /**
  1106. * Structure step that will create all the needed group memberships
  1107. * by loading them from the groups.xml file performing the required matches.
  1108. */
  1109. class restore_groups_members_structure_step extends restore_structure_step {
  1110. protected $plugins = null;
  1111. protected function define_structure() {
  1112. $paths = array(); // Add paths here
  1113. if ($this->get_setting_value('groups') && $this->get_setting_value('users')) {
  1114. $paths[] = new restore_path_element('group', '/groups/group');
  1115. $paths[] = new restore_path_element('member', '/groups/group/group_members/group_member');
  1116. }
  1117. return $paths;
  1118. }
  1119. public function process_group($data) {
  1120. $data = (object)$data; // handy
  1121. // HACK ALERT!
  1122. // Not much to do here, this groups mapping should be already done from restore_groups_structure_step.
  1123. // Let's fake internal state to make $this->get_new_parentid('group') work.
  1124. $this->set_mapping('group', $data->id, $this->get_mappingid('group', $data->id));
  1125. }
  1126. public function process_member($data) {
  1127. global $DB, $CFG;
  1128. require_once("$CFG->dirroot/group/lib.php");
  1129. // NOTE: Always use groups_add_member() because it triggers events and verifies if user is enrolled.
  1130. $data = (object)$data; // handy
  1131. // get parent group->id
  1132. $data->groupid = $this->get_new_parentid('group');
  1133. // map user newitemid and insert if not member already
  1134. if ($data->userid = $this->get_mappingid('user', $data->userid)) {
  1135. if (!$DB->record_exists('groups_members', array('groupid' => $data->groupid, 'userid' => $data->userid))) {
  1136. // Check the component, if any, exists.
  1137. if (empty($data->component)) {
  1138. groups_add_member($data->groupid, $data->userid);
  1139. } else if ((strpos($data->component, 'enrol_') === 0)) {
  1140. // Deal with enrolment groups - ignore the component and just find out the instance via new id,
  1141. // it is possible that enrolment was restored using different plugin type.
  1142. if (!isset($this->plugins)) {
  1143. $this->plugins = enrol_get_plugins(true);
  1144. }
  1145. if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
  1146. if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
  1147. if (isset($this->plugins[$instance->enrol])) {
  1148. $this->plugins[$instance->enrol]->restore_group_member($instance, $data->groupid, $data->userid);
  1149. }
  1150. }
  1151. }
  1152. } else {
  1153. $dir = core_component::get_component_directory($data->component);
  1154. if ($dir and is_dir($dir)) {
  1155. if (component_callback($data->component, 'restore_group_member', array($this, $data), true)) {
  1156. return;
  1157. }
  1158. }
  1159. // Bad luck, plugin could not restore the data, let's add normal membership.
  1160. groups_add_member($data->groupid, $data->userid);
  1161. $message = "Restore of '$data->component/$data->itemid' group membership is not supported, using standard group membership instead.";
  1162. $this->log($message, backup::LOG_WARNING);
  1163. }
  1164. }
  1165. }
  1166. }
  1167. }
  1168. /**
  1169. * Structure step that will create all the needed scales
  1170. * by loading them from the scales.xml
  1171. */
  1172. class restore_scales_structure_step extends restore_structure_step {
  1173. protected function define_structure() {
  1174. $paths = array(); // Add paths here
  1175. $paths[] = new restore_path_element('scale', '/scales_definition/scale');
  1176. return $paths;
  1177. }
  1178. protected function process_scale($data) {
  1179. global $DB;
  1180. $data = (object)$data;
  1181. $restorefiles = false; // Only if we end creating the group
  1182. $oldid = $data->id; // need this saved for later
  1183. // Look for scale (by 'scale' both in standard (course=0) and current course
  1184. // with priority to standard scales (ORDER clause)
  1185. // scale is not course unique, use get_record_sql to suppress warning
  1186. // Going to compare LOB columns so, use the cross-db sql_compare_text() in both sides
  1187. $compare_scale_clause = $DB->sql_compare_text('scale') . ' = ' . $DB->sql_compare_text(':scaledesc');
  1188. $params = array('courseid' => $this->get_courseid(), 'scaledesc' => $data->scale);
  1189. if (!$scadb = $DB->get_record_sql("SELECT *
  1190. FROM {scale}
  1191. WHERE courseid IN (0, :courseid)
  1192. AND $compare_scale_clause
  1193. ORDER BY courseid", $params, IGNORE_MULTIPLE)) {
  1194. // Remap the user if possible, defaut to user performing the restore if not
  1195. $userid = $this->get_mappingid('user', $data->userid);
  1196. $data->userid = $userid ? $userid : $this->task->get_userid();
  1197. // Remap the course if course scale
  1198. $data->courseid = $data->courseid ? $this->get_courseid() : 0;
  1199. // If global scale (course=0), check the user has perms to create it
  1200. // falling to course scale if not
  1201. $systemctx = context_system::instance();
  1202. if ($data->courseid == 0 && !has_capability('moodle/course:managescales', $systemctx , $this->task->get_userid())) {
  1203. $data->courseid = $this->get_courseid();
  1204. }
  1205. // scale doesn't exist, create
  1206. $newitemid = $DB->insert_record('scale', $data);
  1207. $restorefiles = true; // We'll restore the files
  1208. } else {
  1209. // scale exists, use it
  1210. $newitemid = $scadb->id;
  1211. }
  1212. // Save the id mapping (with files support at system context)
  1213. $this->set_mapping('scale', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
  1214. }
  1215. protected function after_execute() {
  1216. // Add scales related files, matching with "scale" mappings
  1217. $this->add_related_files('grade', 'scale', 'scale', $this->task->get_old_system_contextid());
  1218. }
  1219. }
  1220. /**
  1221. * Structure step that will create all the needed outocomes
  1222. * by loading them from the outcomes.xml
  1223. */
  1224. class restore_outcomes_structure_step extends restore_structure_step {
  1225. protected function define_structure() {
  1226. $paths = array(); // Add paths here
  1227. $paths[] = new restore_path_element('outcome', '/outcomes_definition/outcome');
  1228. return $paths;
  1229. }
  1230. protected function process_outcome($data) {
  1231. global $DB;
  1232. $data = (object)$data;
  1233. $restorefiles = false; // Only if we end creating the group
  1234. $oldid = $data->id; // need this saved for later
  1235. // Look for outcome (by shortname both in standard (courseid=null) and current course
  1236. // with priority to standard outcomes (ORDER clause)
  1237. // outcome is not course unique, use get_record_sql to suppress warning
  1238. $params = array('courseid' => $this->get_courseid(), 'shortname' => $data->shortname);
  1239. if (!$outdb = $DB->get_record_sql('SELECT *
  1240. FROM {grade_outcomes}
  1241. WHERE shortname = :shortname
  1242. AND (courseid = :courseid OR courseid IS NULL)
  1243. ORDER BY COALESCE(courseid, 0)', $params, IGNORE_MULTIPLE)) {
  1244. // Remap the user
  1245. $userid = $this->get_mappingid('user', $data->usermodified);
  1246. $data->usermodified = $userid ? $userid : $this->task->get_userid();
  1247. // Remap the scale
  1248. $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
  1249. // Remap the course if course outcome
  1250. $data->courseid = $data->courseid ? $this->get_courseid() : null;
  1251. // If global outcome (course=null), check the user has perms to create it
  1252. // falling to course outcome if not
  1253. $systemctx = context_system::instance();
  1254. if (is_null($data->courseid) && !has_capability('moodle/grade:manageoutcomes', $systemctx , $this->task->get_userid())) {
  1255. $data->courseid = $this->get_courseid();
  1256. }
  1257. // outcome doesn't exist, create
  1258. $newitemid = $DB->insert_record('grade_outcomes', $data);
  1259. $restorefiles = true; // We'll restore the files
  1260. } else {
  1261. // scale exists, use it
  1262. $newitemid = $outdb->id;
  1263. }
  1264. // Set the corresponding grade_outcomes_courses record
  1265. $outcourserec = new stdclass();
  1266. $outcourserec->courseid = $this->get_courseid();
  1267. $outcourserec->outcomeid = $newitemid;
  1268. if (!$DB->record_exists('grade_outcomes_courses', (array)$outcourserec)) {
  1269. $DB->insert_record('grade_outcomes_courses', $outcourserec);
  1270. }
  1271. // Save the id mapping (with files support at system context)
  1272. $this->set_mapping('outcome', $oldid, $newitemid, $restorefiles, $this->task->get_old_system_contextid());
  1273. }
  1274. protected function after_execute() {
  1275. // Add outcomes related files, matching with "outcome" mappings
  1276. $this->add_related_files('grade', 'outcome', 'outcome', $this->task->get_old_system_contextid());
  1277. }
  1278. }
  1279. /**
  1280. * Execution step that, *conditionally* (if there isn't preloaded information
  1281. * will load all the question categories and questions (header info only)
  1282. * to backup_temp_ids. They will be stored with "question_category" and
  1283. * "question" itemnames and with their original contextid and question category
  1284. * id as paremitemids
  1285. */
  1286. class restore_load_categories_and_questions extends restore_execution_step {
  1287. protected function define_execution() {
  1288. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  1289. return;
  1290. }
  1291. $file = $this->get_basepath() . '/questions.xml';
  1292. restore_dbops::load_categories_and_questions_to_tempids($this->get_restoreid(), $file);
  1293. }
  1294. }
  1295. /**
  1296. * Execution step that, *conditionally* (if there isn't preloaded information)
  1297. * will process all the needed categories and questions
  1298. * in order to decide and perform any action with them (create / map / error)
  1299. * Note: Any error will cause exception, as far as this is the same processing
  1300. * than the one into restore prechecks (that should have stopped process earlier)
  1301. */
  1302. class restore_process_categories_and_questions extends restore_execution_step {
  1303. protected function define_execution() {
  1304. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  1305. return;
  1306. }
  1307. restore_dbops::process_categories_and_questions($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite());
  1308. }
  1309. }
  1310. /**
  1311. * Structure step that will read the section.xml creating/updating sections
  1312. * as needed, rebuilding course cache and other friends
  1313. */
  1314. class restore_section_structure_step extends restore_structure_step {
  1315. /** @var array Cache: Array of id => course format */
  1316. private static $courseformats = array();
  1317. /**
  1318. * Resets a static cache of course formats. Required for unit testing.
  1319. */
  1320. public static function reset_caches() {
  1321. self::$courseformats = array();
  1322. }
  1323. protected function define_structure() {
  1324. global $CFG;
  1325. $paths = array();
  1326. $section = new restore_path_element('section', '/section');
  1327. $paths[] = $section;
  1328. if ($CFG->enableavailability) {
  1329. $paths[] = new restore_path_element('availability', '/section/availability');
  1330. $paths[] = new restore_path_element('availability_field', '/section/availability_field');
  1331. }
  1332. $paths[] = new restore_path_element('course_format_options', '/section/course_format_options');
  1333. // Apply for 'format' plugins optional paths at section level
  1334. $this->add_plugin_structure('format', $section);
  1335. // Apply for 'local' plugins optional paths at section level
  1336. $this->add_plugin_structure('local', $section);
  1337. return $paths;
  1338. }
  1339. public function process_section($data) {
  1340. global $CFG, $DB;
  1341. $data = (object)$data;
  1342. $oldid = $data->id; // We'll need this later
  1343. $restorefiles = false;
  1344. // Look for the section
  1345. $section = new stdclass();
  1346. $section->course = $this->get_courseid();
  1347. $section->section = $data->number;
  1348. $section->timemodified = $data->timemodified ?? 0;
  1349. // Section doesn't exist, create it with all the info from backup
  1350. if (!$secrec = $DB->get_record('course_sections', ['course' => $this->get_courseid(), 'section' => $data->number])) {
  1351. $section->name = $data->name;
  1352. $section->summary = $data->summary;
  1353. $section->summaryformat = $data->summaryformat;
  1354. $section->sequence = '';
  1355. $section->visible = $data->visible;
  1356. if (empty($CFG->enableavailability)) { // Process availability information only if enabled.
  1357. $section->availability = null;
  1358. } else {
  1359. $section->availability = isset($data->availabilityjson) ? $data->availabilityjson : null;
  1360. // Include legacy [<2.7] availability data if provided.
  1361. if (is_null($section->availability)) {
  1362. $section->availability = \core_availability\info::convert_legacy_fields(
  1363. $data, true);
  1364. }
  1365. }
  1366. $newitemid = $DB->insert_record('course_sections', $section);
  1367. $section->id = $newitemid;
  1368. core\event\course_section_created::create_from_section($section)->trigger();
  1369. $restorefiles = true;
  1370. // Section exists, update non-empty information
  1371. } else {
  1372. $section->id = $secrec->id;
  1373. if ((string)$secrec->name === '') {
  1374. $section->name = $data->name;
  1375. }
  1376. if (empty($secrec->summary)) {
  1377. $section->summary = $data->summary;
  1378. $section->summaryformat = $data->summaryformat;
  1379. $restorefiles = true;
  1380. }
  1381. // Don't update availability (I didn't see a useful way to define
  1382. // whether existing or new one should take precedence).
  1383. $DB->update_record('course_sections', $section);
  1384. $newitemid = $secrec->id;
  1385. // Trigger an event for course section update.
  1386. $event = \core\event\course_section_updated::create(
  1387. array(
  1388. 'objectid' => $section->id,
  1389. 'courseid' => $section->course,
  1390. 'context' => context_course::instance($section->course),
  1391. 'other' => array('sectionnum' => $section->section)
  1392. )
  1393. );
  1394. $event->trigger();
  1395. }
  1396. // Annotate the section mapping, with restorefiles option if needed
  1397. $this->set_mapping('course_section', $oldid, $newitemid, $restorefiles);
  1398. // set the new course_section id in the task
  1399. $this->task->set_sectionid($newitemid);
  1400. // If there is the legacy showavailability data, store this for later use.
  1401. // (This data is not present when restoring 'new' backups.)
  1402. if (isset($data->showavailability)) {
  1403. // Cache the showavailability flag using the backup_ids data field.
  1404. restore_dbops::set_backup_ids_record($this->get_restoreid(),
  1405. 'section_showavailability', $newitemid, 0, null,
  1406. (object)array('showavailability' => $data->showavailability));
  1407. }
  1408. // Commented out. We never modify course->numsections as far as that is used
  1409. // by a lot of people to "hide" sections on purpose (so this remains as used to be in Moodle 1.x)
  1410. // Note: We keep the code here, to know about and because of the possibility of making this
  1411. // optional based on some setting/attribute in the future
  1412. // If needed, adjust course->numsections
  1413. //if ($numsections = $DB->get_field('course', 'numsections', array('id' => $this->get_courseid()))) {
  1414. // if ($numsections < $section->section) {
  1415. // $DB->set_field('course', 'numsections', $section->section, array('id' => $this->get_courseid()));
  1416. // }
  1417. //}
  1418. }
  1419. /**
  1420. * Process the legacy availability table record. This table does not exist
  1421. * in Moodle 2.7+ but we still support restore.
  1422. *
  1423. * @param stdClass $data Record data
  1424. */
  1425. public function process_availability($data) {
  1426. $data = (object)$data;
  1427. // Simply going to store the whole availability record now, we'll process
  1428. // all them later in the final task (once all activities have been restored)
  1429. // Let's call the low level one to be able to store the whole object.
  1430. $data->coursesectionid = $this->task->get_sectionid();
  1431. restore_dbops::set_backup_ids_record($this->get_restoreid(),
  1432. 'section_availability', $data->id, 0, null, $data);
  1433. }
  1434. /**
  1435. * Process the legacy availability fields table record. This table does not
  1436. * exist in Moodle 2.7+ but we still support restore.
  1437. *
  1438. * @param stdClass $data Record data
  1439. */
  1440. public function process_availability_field($data) {
  1441. global $DB;
  1442. $data = (object)$data;
  1443. // Mark it is as passed by default
  1444. $passed = true;
  1445. $customfieldid = null;
  1446. // If a customfield has been used in order to pass we must be able to match an existing
  1447. // customfield by name (data->customfield) and type (data->customfieldtype)
  1448. if (is_null($data->customfield) xor is_null($data->customfieldtype)) {
  1449. // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
  1450. // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
  1451. $passed = false;
  1452. } else if (!is_null($data->customfield)) {
  1453. $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
  1454. $customfieldid = $DB->get_field('user_info_field', 'id', $params);
  1455. $passed = ($customfieldid !== false);
  1456. }
  1457. if ($passed) {
  1458. // Create the object to insert into the database
  1459. $availfield = new stdClass();
  1460. $availfield->coursesectionid = $this->task->get_sectionid();
  1461. $availfield->userfield = $data->userfield;
  1462. $availfield->customfieldid = $customfieldid;
  1463. $availfield->operator = $data->operator;
  1464. $availfield->value = $data->value;
  1465. // Get showavailability option.
  1466. $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
  1467. 'section_showavailability', $availfield->coursesectionid);
  1468. if (!$showrec) {
  1469. // Should not happen.
  1470. throw new coding_exception('No matching showavailability record');
  1471. }
  1472. $show = $showrec->info->showavailability;
  1473. // The $availfield object is now in the format used in the old
  1474. // system. Interpret this and convert to new system.
  1475. $currentvalue = $DB->get_field('course_sections', 'availability',
  1476. array('id' => $availfield->coursesectionid), MUST_EXIST);
  1477. $newvalue = \core_availability\info::add_legacy_availability_field_condition(
  1478. $currentvalue, $availfield, $show);
  1479. $section = new stdClass();
  1480. $section->id = $availfield->coursesectionid;
  1481. $section->availability = $newvalue;
  1482. $section->timemodified = time();
  1483. $DB->update_record('course_sections', $section);
  1484. }
  1485. }
  1486. public function process_course_format_options($data) {
  1487. global $DB;
  1488. $courseid = $this->get_courseid();
  1489. if (!array_key_exists($courseid, self::$courseformats)) {
  1490. // It is safe to have a static cache of course formats because format can not be changed after this point.
  1491. self::$courseformats[$courseid] = $DB->get_field('course', 'format', array('id' => $courseid));
  1492. }
  1493. $data = (array)$data;
  1494. if (self::$courseformats[$courseid] === $data['format']) {
  1495. // Import section format options only if both courses (the one that was backed up
  1496. // and the one we are restoring into) have same formats.
  1497. $params = array(
  1498. 'courseid' => $this->get_courseid(),
  1499. 'sectionid' => $this->task->get_sectionid(),
  1500. 'format' => $data['format'],
  1501. 'name' => $data['name']
  1502. );
  1503. if ($record = $DB->get_record('course_format_options', $params, 'id, value')) {
  1504. // Do not overwrite existing information.
  1505. $newid = $record->id;
  1506. } else {
  1507. $params['value'] = $data['value'];
  1508. $newid = $DB->insert_record('course_format_options', $params);
  1509. }
  1510. $this->set_mapping('course_format_options', $data['id'], $newid);
  1511. }
  1512. }
  1513. protected function after_execute() {
  1514. // Add section related files, with 'course_section' itemid to match
  1515. $this->add_related_files('course', 'section', 'course_section');
  1516. }
  1517. }
  1518. /**
  1519. * Structure step that will read the course.xml file, loading it and performing
  1520. * various actions depending of the site/restore settings. Note that target
  1521. * course always exist before arriving here so this step will be updating
  1522. * the course record (never inserting)
  1523. */
  1524. class restore_course_structure_step extends restore_structure_step {
  1525. /**
  1526. * @var bool this gets set to true by {@link process_course()} if we are
  1527. * restoring an old coures that used the legacy 'module security' feature.
  1528. * If so, we have to do more work in {@link after_execute()}.
  1529. */
  1530. protected $legacyrestrictmodules = false;
  1531. /**
  1532. * @var array Used when {@link $legacyrestrictmodules} is true. This is an
  1533. * array with array keys the module names ('forum', 'quiz', etc.). These are
  1534. * the modules that are allowed according to the data in the backup file.
  1535. * In {@link after_execute()} we then have to prevent adding of all the other
  1536. * types of activity.
  1537. */
  1538. protected $legacyallowedmodules = array();
  1539. protected function define_structure() {
  1540. $course = new restore_path_element('course', '/course');
  1541. $category = new restore_path_element('category', '/course/category');
  1542. $tag = new restore_path_element('tag', '/course/tags/tag');
  1543. $customfield = new restore_path_element('customfield', '/course/customfields/customfield');
  1544. $allowed_module = new restore_path_element('allowed_module', '/course/allowed_modules/module');
  1545. // Apply for 'format' plugins optional paths at course level
  1546. $this->add_plugin_structure('format', $course);
  1547. // Apply for 'theme' plugins optional paths at course level
  1548. $this->add_plugin_structure('theme', $course);
  1549. // Apply for 'report' plugins optional paths at course level
  1550. $this->add_plugin_structure('report', $course);
  1551. // Apply for 'course report' plugins optional paths at course level
  1552. $this->add_plugin_structure('coursereport', $course);
  1553. // Apply for plagiarism plugins optional paths at course level
  1554. $this->add_plugin_structure('plagiarism', $course);
  1555. // Apply for local plugins optional paths at course level
  1556. $this->add_plugin_structure('local', $course);
  1557. // Apply for admin tool plugins optional paths at course level.
  1558. $this->add_plugin_structure('tool', $course);
  1559. return array($course, $category, $tag, $customfield, $allowed_module);
  1560. }
  1561. /**
  1562. * Processing functions go here
  1563. *
  1564. * @global moodledatabase $DB
  1565. * @param stdClass $data
  1566. */
  1567. public function process_course($data) {
  1568. global $CFG, $DB;
  1569. $context = context::instance_by_id($this->task->get_contextid());
  1570. $userid = $this->task->get_userid();
  1571. $target = $this->get_task()->get_target();
  1572. $isnewcourse = $target == backup::TARGET_NEW_COURSE;
  1573. // When restoring to a new course we can set all the things except for the ID number.
  1574. $canchangeidnumber = $isnewcourse || has_capability('moodle/course:changeidnumber', $context, $userid);
  1575. $canchangesummary = $isnewcourse || has_capability('moodle/course:changesummary', $context, $userid);
  1576. $canforcelanguage = has_capability('moodle/course:setforcedlanguage', $context, $userid);
  1577. $data = (object)$data;
  1578. $data->id = $this->get_courseid();
  1579. // Calculate final course names, to avoid dupes.
  1580. $fullname = $this->get_setting_value('course_fullname');
  1581. $shortname = $this->get_setting_value('course_shortname');
  1582. list($data->fullname, $data->shortname) = restore_dbops::calculate_course_names($this->get_courseid(),
  1583. $fullname === false ? $data->fullname : $fullname,
  1584. $shortname === false ? $data->shortname : $shortname);
  1585. // Do not modify the course names at all when merging and user selected to keep the names (or prohibited by cap).
  1586. if (!$isnewcourse && $fullname === false) {
  1587. unset($data->fullname);
  1588. }
  1589. if (!$isnewcourse && $shortname === false) {
  1590. unset($data->shortname);
  1591. }
  1592. // Unset summary if user can't change it.
  1593. if (!$canchangesummary) {
  1594. unset($data->summary);
  1595. unset($data->summaryformat);
  1596. }
  1597. // Unset lang if user can't change it.
  1598. if (!$canforcelanguage) {
  1599. unset($data->lang);
  1600. }
  1601. // Only allow the idnumber to be set if the user has permission and the idnumber is not already in use by
  1602. // another course on this site.
  1603. if (!empty($data->idnumber) && $canchangeidnumber && $this->task->is_samesite()
  1604. && !$DB->record_exists('course', array('idnumber' => $data->idnumber))) {
  1605. // Do not reset idnumber.
  1606. } else if (!$isnewcourse) {
  1607. // Prevent override when restoring as merge.
  1608. unset($data->idnumber);
  1609. } else {
  1610. $data->idnumber = '';
  1611. }
  1612. // Any empty value for course->hiddensections will lead to 0 (default, show collapsed).
  1613. // It has been reported that some old 1.9 courses may have it null leading to DB error. MDL-31532
  1614. if (empty($data->hiddensections)) {
  1615. $data->hiddensections = 0;
  1616. }
  1617. // Set legacyrestrictmodules to true if the course was resticting modules. If so
  1618. // then we will need to process restricted modules after execution.
  1619. $this->legacyrestrictmodules = !empty($data->restrictmodules);
  1620. $data->startdate= $this->apply_date_offset($data->startdate);
  1621. if (isset($data->enddate)) {
  1622. $data->enddate = $this->apply_date_offset($data->enddate);
  1623. }
  1624. if ($data->defaultgroupingid) {
  1625. $data->defaultgroupingid = $this->get_mappingid('grouping', $data->defaultgroupingid);
  1626. }
  1627. if (empty($CFG->enablecompletion)) {
  1628. $data->enablecompletion = 0;
  1629. $data->completionstartonenrol = 0;
  1630. $data->completionnotify = 0;
  1631. }
  1632. $languages = get_string_manager()->get_list_of_translations(); // Get languages for quick search
  1633. if (isset($data->lang) && !array_key_exists($data->lang, $languages)) {
  1634. $data->lang = '';
  1635. }
  1636. $themes = get_list_of_themes(); // Get themes for quick search later
  1637. if (!array_key_exists($data->theme, $themes) || empty($CFG->allowcoursethemes)) {
  1638. $data->theme = '';
  1639. }
  1640. // Check if this is an old SCORM course format.
  1641. if ($data->format == 'scorm') {
  1642. $data->format = 'singleactivity';
  1643. $data->activitytype = 'scorm';
  1644. }
  1645. // Course record ready, update it
  1646. $DB->update_record('course', $data);
  1647. course_get_format($data)->update_course_format_options($data);
  1648. // Role name aliases
  1649. restore_dbops::set_course_role_names($this->get_restoreid(), $this->get_courseid());
  1650. }
  1651. public function process_category($data) {
  1652. // Nothing to do with the category. UI sets it before restore starts
  1653. }
  1654. public function process_tag($data) {
  1655. global $CFG, $DB;
  1656. $data = (object)$data;
  1657. core_tag_tag::add_item_tag('core', 'course', $this->get_courseid(),
  1658. context_course::instance($this->get_courseid()), $data->rawname);
  1659. }
  1660. /**
  1661. * Process custom fields
  1662. *
  1663. * @param array $data
  1664. */
  1665. public function process_customfield($data) {
  1666. $handler = core_course\customfield\course_handler::create();
  1667. $handler->restore_instance_data_from_backup($this->task, $data);
  1668. }
  1669. public function process_allowed_module($data) {
  1670. $data = (object)$data;
  1671. // Backwards compatiblity support for the data that used to be in the
  1672. // course_allowed_modules table.
  1673. if ($this->legacyrestrictmodules) {
  1674. $this->legacyallowedmodules[$data->modulename] = 1;
  1675. }
  1676. }
  1677. protected function after_execute() {
  1678. global $DB;
  1679. // Add course related files, without itemid to match
  1680. $this->add_related_files('course', 'summary', null);
  1681. $this->add_related_files('course', 'overviewfiles', null);
  1682. // Deal with legacy allowed modules.
  1683. if ($this->legacyrestrictmodules) {
  1684. $context = context_course::instance($this->get_courseid());
  1685. list($roleids) = get_roles_with_cap_in_context($context, 'moodle/course:manageactivities');
  1686. list($managerroleids) = get_roles_with_cap_in_context($context, 'moodle/site:config');
  1687. foreach ($managerroleids as $roleid) {
  1688. unset($roleids[$roleid]);
  1689. }
  1690. foreach (core_component::get_plugin_list('mod') as $modname => $notused) {
  1691. if (isset($this->legacyallowedmodules[$modname])) {
  1692. // Module is allowed, no worries.
  1693. continue;
  1694. }
  1695. $capability = 'mod/' . $modname . ':addinstance';
  1696. if (!get_capability_info($capability)) {
  1697. $this->log("Capability '{$capability}' was not found!", backup::LOG_WARNING);
  1698. continue;
  1699. }
  1700. foreach ($roleids as $roleid) {
  1701. assign_capability($capability, CAP_PREVENT, $roleid, $context);
  1702. }
  1703. }
  1704. }
  1705. }
  1706. }
  1707. /**
  1708. * Execution step that will migrate legacy files if present.
  1709. */
  1710. class restore_course_legacy_files_step extends restore_execution_step {
  1711. public function define_execution() {
  1712. global $DB;
  1713. // Do a check for legacy files and skip if there are none.
  1714. $sql = 'SELECT count(*)
  1715. FROM {backup_files_temp}
  1716. WHERE backupid = ?
  1717. AND contextid = ?
  1718. AND component = ?
  1719. AND filearea = ?';
  1720. $params = array($this->get_restoreid(), $this->task->get_old_contextid(), 'course', 'legacy');
  1721. if ($DB->count_records_sql($sql, $params)) {
  1722. $DB->set_field('course', 'legacyfiles', 2, array('id' => $this->get_courseid()));
  1723. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'course',
  1724. 'legacy', $this->task->get_old_contextid(), $this->task->get_userid());
  1725. }
  1726. }
  1727. }
  1728. /*
  1729. * Structure step that will read the roles.xml file (at course/activity/block levels)
  1730. * containing all the role_assignments and overrides for that context. If corresponding to
  1731. * one mapped role, they will be applied to target context. Will observe the role_assignments
  1732. * setting to decide if ras are restored.
  1733. *
  1734. * Note: this needs to be executed after all users are enrolled.
  1735. */
  1736. class restore_ras_and_caps_structure_step extends restore_structure_step {
  1737. protected $plugins = null;
  1738. protected function define_structure() {
  1739. $paths = array();
  1740. // Observe the role_assignments setting
  1741. if ($this->get_setting_value('role_assignments')) {
  1742. $paths[] = new restore_path_element('assignment', '/roles/role_assignments/assignment');
  1743. }
  1744. $paths[] = new restore_path_element('override', '/roles/role_overrides/override');
  1745. return $paths;
  1746. }
  1747. /**
  1748. * Assign roles
  1749. *
  1750. * This has to be called after enrolments processing.
  1751. *
  1752. * @param mixed $data
  1753. * @return void
  1754. */
  1755. public function process_assignment($data) {
  1756. global $DB;
  1757. $data = (object)$data;
  1758. // Check roleid, userid are one of the mapped ones
  1759. if (!$newroleid = $this->get_mappingid('role', $data->roleid)) {
  1760. return;
  1761. }
  1762. if (!$newuserid = $this->get_mappingid('user', $data->userid)) {
  1763. return;
  1764. }
  1765. if (!$DB->record_exists('user', array('id' => $newuserid, 'deleted' => 0))) {
  1766. // Only assign roles to not deleted users
  1767. return;
  1768. }
  1769. if (!$contextid = $this->task->get_contextid()) {
  1770. return;
  1771. }
  1772. if (empty($data->component)) {
  1773. // assign standard manual roles
  1774. // TODO: role_assign() needs one userid param to be able to specify our restore userid
  1775. role_assign($newroleid, $newuserid, $contextid);
  1776. } else if ((strpos($data->component, 'enrol_') === 0)) {
  1777. // Deal with enrolment roles - ignore the component and just find out the instance via new id,
  1778. // it is possible that enrolment was restored using different plugin type.
  1779. if (!isset($this->plugins)) {
  1780. $this->plugins = enrol_get_plugins(true);
  1781. }
  1782. if ($enrolid = $this->get_mappingid('enrol', $data->itemid)) {
  1783. if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
  1784. if (isset($this->plugins[$instance->enrol])) {
  1785. $this->plugins[$instance->enrol]->restore_role_assignment($instance, $newroleid, $newuserid, $contextid);
  1786. }
  1787. }
  1788. }
  1789. } else {
  1790. $data->roleid = $newroleid;
  1791. $data->userid = $newuserid;
  1792. $data->contextid = $contextid;
  1793. $dir = core_component::get_component_directory($data->component);
  1794. if ($dir and is_dir($dir)) {
  1795. if (component_callback($data->component, 'restore_role_assignment', array($this, $data), true)) {
  1796. return;
  1797. }
  1798. }
  1799. // Bad luck, plugin could not restore the data, let's add normal membership.
  1800. role_assign($data->roleid, $data->userid, $data->contextid);
  1801. $message = "Restore of '$data->component/$data->itemid' role assignments is not supported, using manual role assignments instead.";
  1802. $this->log($message, backup::LOG_WARNING);
  1803. }
  1804. }
  1805. public function process_override($data) {
  1806. $data = (object)$data;
  1807. // Check roleid is one of the mapped ones
  1808. $newroleid = $this->get_mappingid('role', $data->roleid);
  1809. // If newroleid and context are valid assign it via API (it handles dupes and so on)
  1810. if ($newroleid && $this->task->get_contextid()) {
  1811. if (!get_capability_info($data->capability)) {
  1812. $this->log("Capability '{$data->capability}' was not found!", backup::LOG_WARNING);
  1813. } else {
  1814. // TODO: assign_capability() needs one userid param to be able to specify our restore userid.
  1815. assign_capability($data->capability, $data->permission, $newroleid, $this->task->get_contextid());
  1816. }
  1817. }
  1818. }
  1819. }
  1820. /**
  1821. * If no instances yet add default enrol methods the same way as when creating new course in UI.
  1822. */
  1823. class restore_default_enrolments_step extends restore_execution_step {
  1824. public function define_execution() {
  1825. global $DB;
  1826. // No enrolments in front page.
  1827. if ($this->get_courseid() == SITEID) {
  1828. return;
  1829. }
  1830. $course = $DB->get_record('course', array('id'=>$this->get_courseid()), '*', MUST_EXIST);
  1831. if ($DB->record_exists('enrol', array('courseid'=>$this->get_courseid(), 'enrol'=>'manual'))) {
  1832. // Something already added instances, do not add default instances.
  1833. $plugins = enrol_get_plugins(true);
  1834. foreach ($plugins as $plugin) {
  1835. $plugin->restore_sync_course($course);
  1836. }
  1837. } else {
  1838. // Looks like a newly created course.
  1839. enrol_course_updated(true, $course, null);
  1840. }
  1841. }
  1842. }
  1843. /**
  1844. * This structure steps restores the enrol plugins and their underlying
  1845. * enrolments, performing all the mappings and/or movements required
  1846. */
  1847. class restore_enrolments_structure_step extends restore_structure_step {
  1848. protected $enrolsynced = false;
  1849. protected $plugins = null;
  1850. protected $originalstatus = array();
  1851. /**
  1852. * Conditionally decide if this step should be executed.
  1853. *
  1854. * This function checks the following parameter:
  1855. *
  1856. * 1. the course/enrolments.xml file exists
  1857. *
  1858. * @return bool true is safe to execute, false otherwise
  1859. */
  1860. protected function execute_condition() {
  1861. if ($this->get_courseid() == SITEID) {
  1862. return false;
  1863. }
  1864. // Check it is included in the backup
  1865. $fullpath = $this->task->get_taskbasepath();
  1866. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  1867. if (!file_exists($fullpath)) {
  1868. // Not found, can't restore enrolments info
  1869. return false;
  1870. }
  1871. return true;
  1872. }
  1873. protected function define_structure() {
  1874. $userinfo = $this->get_setting_value('users');
  1875. $paths = [];
  1876. $paths[] = $enrol = new restore_path_element('enrol', '/enrolments/enrols/enrol');
  1877. if ($userinfo) {
  1878. $paths[] = new restore_path_element('enrolment', '/enrolments/enrols/enrol/user_enrolments/enrolment');
  1879. }
  1880. // Attach local plugin stucture to enrol element.
  1881. $this->add_plugin_structure('enrol', $enrol);
  1882. return $paths;
  1883. }
  1884. /**
  1885. * Create enrolment instances.
  1886. *
  1887. * This has to be called after creation of roles
  1888. * and before adding of role assignments.
  1889. *
  1890. * @param mixed $data
  1891. * @return void
  1892. */
  1893. public function process_enrol($data) {
  1894. global $DB;
  1895. $data = (object)$data;
  1896. $oldid = $data->id; // We'll need this later.
  1897. unset($data->id);
  1898. $this->originalstatus[$oldid] = $data->status;
  1899. if (!$courserec = $DB->get_record('course', array('id' => $this->get_courseid()))) {
  1900. $this->set_mapping('enrol', $oldid, 0);
  1901. return;
  1902. }
  1903. if (!isset($this->plugins)) {
  1904. $this->plugins = enrol_get_plugins(true);
  1905. }
  1906. if (!$this->enrolsynced) {
  1907. // Make sure that all plugin may create instances and enrolments automatically
  1908. // before the first instance restore - this is suitable especially for plugins
  1909. // that synchronise data automatically using course->idnumber or by course categories.
  1910. foreach ($this->plugins as $plugin) {
  1911. $plugin->restore_sync_course($courserec);
  1912. }
  1913. $this->enrolsynced = true;
  1914. }
  1915. // Map standard fields - plugin has to process custom fields manually.
  1916. $data->roleid = $this->get_mappingid('role', $data->roleid);
  1917. $data->courseid = $courserec->id;
  1918. if (!$this->get_setting_value('users') && $this->get_setting_value('enrolments') == backup::ENROL_WITHUSERS) {
  1919. $converttomanual = true;
  1920. } else {
  1921. $converttomanual = ($this->get_setting_value('enrolments') == backup::ENROL_NEVER);
  1922. }
  1923. if ($converttomanual) {
  1924. // Restore enrolments as manual enrolments.
  1925. unset($data->sortorder); // Remove useless sortorder from <2.4 backups.
  1926. if (!enrol_is_enabled('manual')) {
  1927. $this->set_mapping('enrol', $oldid, 0);
  1928. return;
  1929. }
  1930. if ($instances = $DB->get_records('enrol', array('courseid'=>$data->courseid, 'enrol'=>'manual'), 'id')) {
  1931. $instance = reset($instances);
  1932. $this->set_mapping('enrol', $oldid, $instance->id);
  1933. } else {
  1934. if ($data->enrol === 'manual') {
  1935. $instanceid = $this->plugins['manual']->add_instance($courserec, (array)$data);
  1936. } else {
  1937. $instanceid = $this->plugins['manual']->add_default_instance($courserec);
  1938. }
  1939. $this->set_mapping('enrol', $oldid, $instanceid);
  1940. }
  1941. } else {
  1942. if (!enrol_is_enabled($data->enrol) or !isset($this->plugins[$data->enrol])) {
  1943. $this->set_mapping('enrol', $oldid, 0);
  1944. $message = "Enrol plugin '$data->enrol' data can not be restored because it is not enabled, consider restoring without enrolment methods";
  1945. $this->log($message, backup::LOG_WARNING);
  1946. return;
  1947. }
  1948. if ($task = $this->get_task() and $task->get_target() == backup::TARGET_NEW_COURSE) {
  1949. // Let's keep the sortorder in old backups.
  1950. } else {
  1951. // Prevent problems with colliding sortorders in old backups,
  1952. // new 2.4 backups do not need sortorder because xml elements are ordered properly.
  1953. unset($data->sortorder);
  1954. }
  1955. // Note: plugin is responsible for setting up the mapping, it may also decide to migrate to different type.
  1956. $this->plugins[$data->enrol]->restore_instance($this, $data, $courserec, $oldid);
  1957. }
  1958. }
  1959. /**
  1960. * Create user enrolments.
  1961. *
  1962. * This has to be called after creation of enrolment instances
  1963. * and before adding of role assignments.
  1964. *
  1965. * Roles are assigned in restore_ras_and_caps_structure_step::process_assignment() processing afterwards.
  1966. *
  1967. * @param mixed $data
  1968. * @return void
  1969. */
  1970. public function process_enrolment($data) {
  1971. global $DB;
  1972. if (!isset($this->plugins)) {
  1973. $this->plugins = enrol_get_plugins(true);
  1974. }
  1975. $data = (object)$data;
  1976. // Process only if parent instance have been mapped.
  1977. if ($enrolid = $this->get_new_parentid('enrol')) {
  1978. $oldinstancestatus = ENROL_INSTANCE_ENABLED;
  1979. $oldenrolid = $this->get_old_parentid('enrol');
  1980. if (isset($this->originalstatus[$oldenrolid])) {
  1981. $oldinstancestatus = $this->originalstatus[$oldenrolid];
  1982. }
  1983. if ($instance = $DB->get_record('enrol', array('id'=>$enrolid))) {
  1984. // And only if user is a mapped one.
  1985. if ($userid = $this->get_mappingid('user', $data->userid)) {
  1986. if (isset($this->plugins[$instance->enrol])) {
  1987. $this->plugins[$instance->enrol]->restore_user_enrolment($this, $data, $instance, $userid, $oldinstancestatus);
  1988. }
  1989. }
  1990. }
  1991. }
  1992. }
  1993. }
  1994. /**
  1995. * Make sure the user restoring the course can actually access it.
  1996. */
  1997. class restore_fix_restorer_access_step extends restore_execution_step {
  1998. protected function define_execution() {
  1999. global $CFG, $DB;
  2000. if (!$userid = $this->task->get_userid()) {
  2001. return;
  2002. }
  2003. if (empty($CFG->restorernewroleid)) {
  2004. // Bad luck, no fallback role for restorers specified
  2005. return;
  2006. }
  2007. $courseid = $this->get_courseid();
  2008. $context = context_course::instance($courseid);
  2009. if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
  2010. // Current user may access the course (admin, category manager or restored teacher enrolment usually)
  2011. return;
  2012. }
  2013. // Try to add role only - we do not need enrolment if user has moodle/course:view or is already enrolled
  2014. role_assign($CFG->restorernewroleid, $userid, $context);
  2015. if (is_enrolled($context, $userid, 'moodle/course:update', true) or is_viewing($context, $userid, 'moodle/course:update')) {
  2016. // Extra role is enough, yay!
  2017. return;
  2018. }
  2019. // The last chance is to create manual enrol if it does not exist and and try to enrol the current user,
  2020. // hopefully admin selected suitable $CFG->restorernewroleid ...
  2021. if (!enrol_is_enabled('manual')) {
  2022. return;
  2023. }
  2024. if (!$enrol = enrol_get_plugin('manual')) {
  2025. return;
  2026. }
  2027. if (!$DB->record_exists('enrol', array('enrol'=>'manual', 'courseid'=>$courseid))) {
  2028. $course = $DB->get_record('course', array('id'=>$courseid), '*', MUST_EXIST);
  2029. $fields = array('status'=>ENROL_INSTANCE_ENABLED, 'enrolperiod'=>$enrol->get_config('enrolperiod', 0), 'roleid'=>$enrol->get_config('roleid', 0));
  2030. $enrol->add_instance($course, $fields);
  2031. }
  2032. enrol_try_internal_enrol($courseid, $userid);
  2033. }
  2034. }
  2035. /**
  2036. * This structure steps restores the filters and their configs
  2037. */
  2038. class restore_filters_structure_step extends restore_structure_step {
  2039. protected function define_structure() {
  2040. $paths = array();
  2041. $paths[] = new restore_path_element('active', '/filters/filter_actives/filter_active');
  2042. $paths[] = new restore_path_element('config', '/filters/filter_configs/filter_config');
  2043. return $paths;
  2044. }
  2045. public function process_active($data) {
  2046. $data = (object)$data;
  2047. if (strpos($data->filter, 'filter/') === 0) {
  2048. $data->filter = substr($data->filter, 7);
  2049. } else if (strpos($data->filter, '/') !== false) {
  2050. // Unsupported old filter.
  2051. return;
  2052. }
  2053. if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
  2054. return;
  2055. }
  2056. filter_set_local_state($data->filter, $this->task->get_contextid(), $data->active);
  2057. }
  2058. public function process_config($data) {
  2059. $data = (object)$data;
  2060. if (strpos($data->filter, 'filter/') === 0) {
  2061. $data->filter = substr($data->filter, 7);
  2062. } else if (strpos($data->filter, '/') !== false) {
  2063. // Unsupported old filter.
  2064. return;
  2065. }
  2066. if (!filter_is_enabled($data->filter)) { // Not installed or not enabled, nothing to do
  2067. return;
  2068. }
  2069. filter_set_local_config($data->filter, $this->task->get_contextid(), $data->name, $data->value);
  2070. }
  2071. }
  2072. /**
  2073. * This structure steps restores the comments
  2074. * Note: Cannot use the comments API because defaults to USER->id.
  2075. * That should change allowing to pass $userid
  2076. */
  2077. class restore_comments_structure_step extends restore_structure_step {
  2078. protected function define_structure() {
  2079. $paths = array();
  2080. $paths[] = new restore_path_element('comment', '/comments/comment');
  2081. return $paths;
  2082. }
  2083. public function process_comment($data) {
  2084. global $DB;
  2085. $data = (object)$data;
  2086. // First of all, if the comment has some itemid, ask to the task what to map
  2087. $mapping = false;
  2088. if ($data->itemid) {
  2089. $mapping = $this->task->get_comment_mapping_itemname($data->commentarea);
  2090. $data->itemid = $this->get_mappingid($mapping, $data->itemid);
  2091. }
  2092. // Only restore the comment if has no mapping OR we have found the matching mapping
  2093. if (!$mapping || $data->itemid) {
  2094. // Only if user mapping and context
  2095. $data->userid = $this->get_mappingid('user', $data->userid);
  2096. if ($data->userid && $this->task->get_contextid()) {
  2097. $data->contextid = $this->task->get_contextid();
  2098. // Only if there is another comment with same context/user/timecreated
  2099. $params = array('contextid' => $data->contextid, 'userid' => $data->userid, 'timecreated' => $data->timecreated);
  2100. if (!$DB->record_exists('comments', $params)) {
  2101. $DB->insert_record('comments', $data);
  2102. }
  2103. }
  2104. }
  2105. }
  2106. }
  2107. /**
  2108. * This structure steps restores the badges and their configs
  2109. */
  2110. class restore_badges_structure_step extends restore_structure_step {
  2111. /**
  2112. * Conditionally decide if this step should be executed.
  2113. *
  2114. * This function checks the following parameters:
  2115. *
  2116. * 1. Badges and course badges are enabled on the site.
  2117. * 2. The course/badges.xml file exists.
  2118. * 3. All modules are restorable.
  2119. * 4. All modules are marked for restore.
  2120. *
  2121. * @return bool True is safe to execute, false otherwise
  2122. */
  2123. protected function execute_condition() {
  2124. global $CFG;
  2125. // First check is badges and course level badges are enabled on this site.
  2126. if (empty($CFG->enablebadges) || empty($CFG->badges_allowcoursebadges)) {
  2127. // Disabled, don't restore course badges.
  2128. return false;
  2129. }
  2130. // Check if badges.xml is included in the backup.
  2131. $fullpath = $this->task->get_taskbasepath();
  2132. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  2133. if (!file_exists($fullpath)) {
  2134. // Not found, can't restore course badges.
  2135. return false;
  2136. }
  2137. // Check we are able to restore all backed up modules.
  2138. if ($this->task->is_missing_modules()) {
  2139. return false;
  2140. }
  2141. // Finally check all modules within the backup are being restored.
  2142. if ($this->task->is_excluding_activities()) {
  2143. return false;
  2144. }
  2145. return true;
  2146. }
  2147. protected function define_structure() {
  2148. $paths = array();
  2149. $paths[] = new restore_path_element('badge', '/badges/badge');
  2150. $paths[] = new restore_path_element('criterion', '/badges/badge/criteria/criterion');
  2151. $paths[] = new restore_path_element('parameter', '/badges/badge/criteria/criterion/parameters/parameter');
  2152. $paths[] = new restore_path_element('endorsement', '/badges/badge/endorsement');
  2153. $paths[] = new restore_path_element('alignment', '/badges/badge/alignments/alignment');
  2154. $paths[] = new restore_path_element('relatedbadge', '/badges/badge/relatedbadges/relatedbadge');
  2155. $paths[] = new restore_path_element('manual_award', '/badges/badge/manual_awards/manual_award');
  2156. return $paths;
  2157. }
  2158. public function process_badge($data) {
  2159. global $DB, $CFG;
  2160. require_once($CFG->libdir . '/badgeslib.php');
  2161. $data = (object)$data;
  2162. $data->usercreated = $this->get_mappingid('user', $data->usercreated);
  2163. if (empty($data->usercreated)) {
  2164. $data->usercreated = $this->task->get_userid();
  2165. }
  2166. $data->usermodified = $this->get_mappingid('user', $data->usermodified);
  2167. if (empty($data->usermodified)) {
  2168. $data->usermodified = $this->task->get_userid();
  2169. }
  2170. // We'll restore the badge image.
  2171. $restorefiles = true;
  2172. $courseid = $this->get_courseid();
  2173. $params = array(
  2174. 'name' => $data->name,
  2175. 'description' => $data->description,
  2176. 'timecreated' => $data->timecreated,
  2177. 'timemodified' => $data->timemodified,
  2178. 'usercreated' => $data->usercreated,
  2179. 'usermodified' => $data->usermodified,
  2180. 'issuername' => $data->issuername,
  2181. 'issuerurl' => $data->issuerurl,
  2182. 'issuercontact' => $data->issuercontact,
  2183. 'expiredate' => $this->apply_date_offset($data->expiredate),
  2184. 'expireperiod' => $data->expireperiod,
  2185. 'type' => BADGE_TYPE_COURSE,
  2186. 'courseid' => $courseid,
  2187. 'message' => $data->message,
  2188. 'messagesubject' => $data->messagesubject,
  2189. 'attachment' => $data->attachment,
  2190. 'notification' => $data->notification,
  2191. 'status' => BADGE_STATUS_INACTIVE,
  2192. 'nextcron' => $data->nextcron,
  2193. 'version' => $data->version,
  2194. 'language' => $data->language,
  2195. 'imageauthorname' => $data->imageauthorname,
  2196. 'imageauthoremail' => $data->imageauthoremail,
  2197. 'imageauthorurl' => $data->imageauthorurl,
  2198. 'imagecaption' => $data->imagecaption
  2199. );
  2200. $newid = $DB->insert_record('badge', $params);
  2201. $this->set_mapping('badge', $data->id, $newid, $restorefiles);
  2202. }
  2203. /**
  2204. * Create an endorsement for a badge.
  2205. *
  2206. * @param mixed $data
  2207. * @return void
  2208. */
  2209. public function process_endorsement($data) {
  2210. global $DB;
  2211. $data = (object)$data;
  2212. $params = [
  2213. 'badgeid' => $this->get_new_parentid('badge'),
  2214. 'issuername' => $data->issuername,
  2215. 'issuerurl' => $data->issuerurl,
  2216. 'issueremail' => $data->issueremail,
  2217. 'claimid' => $data->claimid,
  2218. 'claimcomment' => $data->claimcomment,
  2219. 'dateissued' => $this->apply_date_offset($data->dateissued)
  2220. ];
  2221. $newid = $DB->insert_record('badge_endorsement', $params);
  2222. $this->set_mapping('endorsement', $data->id, $newid);
  2223. }
  2224. /**
  2225. * Link to related badges for a badge. This relies on post processing in after_execute().
  2226. *
  2227. * @param mixed $data
  2228. * @return void
  2229. */
  2230. public function process_relatedbadge($data) {
  2231. global $DB;
  2232. $data = (object)$data;
  2233. $relatedbadgeid = $data->relatedbadgeid;
  2234. if ($relatedbadgeid) {
  2235. // Only backup and restore related badges if they are contained in the backup file.
  2236. $params = array(
  2237. 'badgeid' => $this->get_new_parentid('badge'),
  2238. 'relatedbadgeid' => $relatedbadgeid
  2239. );
  2240. $newid = $DB->insert_record('badge_related', $params);
  2241. }
  2242. }
  2243. /**
  2244. * Link to an alignment for a badge.
  2245. *
  2246. * @param mixed $data
  2247. * @return void
  2248. */
  2249. public function process_alignment($data) {
  2250. global $DB;
  2251. $data = (object)$data;
  2252. $params = array(
  2253. 'badgeid' => $this->get_new_parentid('badge'),
  2254. 'targetname' => $data->targetname,
  2255. 'targeturl' => $data->targeturl,
  2256. 'targetdescription' => $data->targetdescription,
  2257. 'targetframework' => $data->targetframework,
  2258. 'targetcode' => $data->targetcode
  2259. );
  2260. $newid = $DB->insert_record('badge_alignment', $params);
  2261. $this->set_mapping('alignment', $data->id, $newid);
  2262. }
  2263. public function process_criterion($data) {
  2264. global $DB;
  2265. $data = (object)$data;
  2266. $params = array(
  2267. 'badgeid' => $this->get_new_parentid('badge'),
  2268. 'criteriatype' => $data->criteriatype,
  2269. 'method' => $data->method,
  2270. 'description' => isset($data->description) ? $data->description : '',
  2271. 'descriptionformat' => isset($data->descriptionformat) ? $data->descriptionformat : 0,
  2272. );
  2273. $newid = $DB->insert_record('badge_criteria', $params);
  2274. $this->set_mapping('criterion', $data->id, $newid);
  2275. }
  2276. public function process_parameter($data) {
  2277. global $DB, $CFG;
  2278. require_once($CFG->libdir . '/badgeslib.php');
  2279. $data = (object)$data;
  2280. $criteriaid = $this->get_new_parentid('criterion');
  2281. // Parameter array that will go to database.
  2282. $params = array();
  2283. $params['critid'] = $criteriaid;
  2284. $oldparam = explode('_', $data->name);
  2285. if ($data->criteriatype == BADGE_CRITERIA_TYPE_ACTIVITY) {
  2286. $module = $this->get_mappingid('course_module', $oldparam[1]);
  2287. $params['name'] = $oldparam[0] . '_' . $module;
  2288. $params['value'] = $oldparam[0] == 'module' ? $module : $data->value;
  2289. } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COURSE) {
  2290. $params['name'] = $oldparam[0] . '_' . $this->get_courseid();
  2291. $params['value'] = $oldparam[0] == 'course' ? $this->get_courseid() : $data->value;
  2292. } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_MANUAL) {
  2293. $role = $this->get_mappingid('role', $data->value);
  2294. if (!empty($role)) {
  2295. $params['name'] = 'role_' . $role;
  2296. $params['value'] = $role;
  2297. } else {
  2298. return;
  2299. }
  2300. } else if ($data->criteriatype == BADGE_CRITERIA_TYPE_COMPETENCY) {
  2301. $competencyid = $this->get_mappingid('competency', $data->value);
  2302. if (!empty($competencyid)) {
  2303. $params['name'] = 'competency_' . $competencyid;
  2304. $params['value'] = $competencyid;
  2305. } else {
  2306. return;
  2307. }
  2308. }
  2309. if (!$DB->record_exists('badge_criteria_param', $params)) {
  2310. $DB->insert_record('badge_criteria_param', $params);
  2311. }
  2312. }
  2313. public function process_manual_award($data) {
  2314. global $DB;
  2315. $data = (object)$data;
  2316. $role = $this->get_mappingid('role', $data->issuerrole);
  2317. if (!empty($role)) {
  2318. $award = array(
  2319. 'badgeid' => $this->get_new_parentid('badge'),
  2320. 'recipientid' => $this->get_mappingid('user', $data->recipientid),
  2321. 'issuerid' => $this->get_mappingid('user', $data->issuerid),
  2322. 'issuerrole' => $role,
  2323. 'datemet' => $this->apply_date_offset($data->datemet)
  2324. );
  2325. // Skip the manual award if recipient or issuer can not be mapped to.
  2326. if (empty($award['recipientid']) || empty($award['issuerid'])) {
  2327. return;
  2328. }
  2329. $DB->insert_record('badge_manual_award', $award);
  2330. }
  2331. }
  2332. protected function after_execute() {
  2333. global $DB;
  2334. // Add related files.
  2335. $this->add_related_files('badges', 'badgeimage', 'badge');
  2336. $badgeid = $this->get_new_parentid('badge');
  2337. // Remap any related badges.
  2338. // We do this in the DB directly because this is backup/restore it is not valid to call into
  2339. // the component API.
  2340. $params = array('badgeid' => $badgeid);
  2341. $query = "SELECT DISTINCT br.id, br.badgeid, br.relatedbadgeid
  2342. FROM {badge_related} br
  2343. WHERE (br.badgeid = :badgeid)";
  2344. $relatedbadges = $DB->get_records_sql($query, $params);
  2345. $newrelatedids = [];
  2346. foreach ($relatedbadges as $relatedbadge) {
  2347. $relatedid = $this->get_mappingid('badge', $relatedbadge->relatedbadgeid);
  2348. $params['relatedbadgeid'] = $relatedbadge->relatedbadgeid;
  2349. $DB->delete_records_select('badge_related', '(badgeid = :badgeid AND relatedbadgeid = :relatedbadgeid)', $params);
  2350. if ($relatedid) {
  2351. $newrelatedids[] = $relatedid;
  2352. }
  2353. }
  2354. if (!empty($newrelatedids)) {
  2355. $relatedbadges = [];
  2356. foreach ($newrelatedids as $relatedid) {
  2357. $relatedbadge = new stdClass();
  2358. $relatedbadge->badgeid = $badgeid;
  2359. $relatedbadge->relatedbadgeid = $relatedid;
  2360. $relatedbadges[] = $relatedbadge;
  2361. }
  2362. $DB->insert_records('badge_related', $relatedbadges);
  2363. }
  2364. }
  2365. }
  2366. /**
  2367. * This structure steps restores the calendar events
  2368. */
  2369. class restore_calendarevents_structure_step extends restore_structure_step {
  2370. protected function define_structure() {
  2371. $paths = array();
  2372. $paths[] = new restore_path_element('calendarevents', '/events/event');
  2373. return $paths;
  2374. }
  2375. public function process_calendarevents($data) {
  2376. global $DB, $SITE, $USER;
  2377. $data = (object)$data;
  2378. $oldid = $data->id;
  2379. $restorefiles = true; // We'll restore the files
  2380. // If this is a new action event, it will automatically be populated by the adhoc task.
  2381. // Nothing to do here.
  2382. if (isset($data->type) && $data->type == CALENDAR_EVENT_TYPE_ACTION) {
  2383. return;
  2384. }
  2385. // User overrides for activities are identified by having a courseid of zero with
  2386. // both a modulename and instance value set.
  2387. $isuseroverride = !$data->courseid && $data->modulename && $data->instance;
  2388. // If we don't want to include user data and this record is a user override event
  2389. // for an activity then we should not create it. (Only activity events can be user override events - which must have this
  2390. // setting).
  2391. if ($isuseroverride && $this->task->setting_exists('userinfo') && !$this->task->get_setting_value('userinfo')) {
  2392. return;
  2393. }
  2394. // Find the userid and the groupid associated with the event.
  2395. $data->userid = $this->get_mappingid('user', $data->userid);
  2396. if ($data->userid === false) {
  2397. // Blank user ID means that we are dealing with module generated events such as quiz starting times.
  2398. // Use the current user ID for these events.
  2399. $data->userid = $USER->id;
  2400. }
  2401. if (!empty($data->groupid)) {
  2402. $data->groupid = $this->get_mappingid('group', $data->groupid);
  2403. if ($data->groupid === false) {
  2404. return;
  2405. }
  2406. }
  2407. // Handle events with empty eventtype //MDL-32827
  2408. if(empty($data->eventtype)) {
  2409. if ($data->courseid == $SITE->id) { // Site event
  2410. $data->eventtype = "site";
  2411. } else if ($data->courseid != 0 && $data->groupid == 0 && ($data->modulename == 'assignment' || $data->modulename == 'assign')) {
  2412. // Course assingment event
  2413. $data->eventtype = "due";
  2414. } else if ($data->courseid != 0 && $data->groupid == 0) { // Course event
  2415. $data->eventtype = "course";
  2416. } else if ($data->groupid) { // Group event
  2417. $data->eventtype = "group";
  2418. } else if ($data->userid) { // User event
  2419. $data->eventtype = "user";
  2420. } else {
  2421. return;
  2422. }
  2423. }
  2424. $params = array(
  2425. 'name' => $data->name,
  2426. 'description' => $data->description,
  2427. 'format' => $data->format,
  2428. // User overrides in activities use a course id of zero. All other event types
  2429. // must use the mapped course id.
  2430. 'courseid' => $data->courseid ? $this->get_courseid() : 0,
  2431. 'groupid' => $data->groupid,
  2432. 'userid' => $data->userid,
  2433. 'repeatid' => $this->get_mappingid('event', $data->repeatid),
  2434. 'modulename' => $data->modulename,
  2435. 'type' => isset($data->type) ? $data->type : 0,
  2436. 'eventtype' => $data->eventtype,
  2437. 'timestart' => $this->apply_date_offset($data->timestart),
  2438. 'timeduration' => $data->timeduration,
  2439. 'timesort' => isset($data->timesort) ? $this->apply_date_offset($data->timesort) : null,
  2440. 'visible' => $data->visible,
  2441. 'uuid' => $data->uuid,
  2442. 'sequence' => $data->sequence,
  2443. 'timemodified' => $data->timemodified,
  2444. 'priority' => isset($data->priority) ? $data->priority : null,
  2445. 'location' => isset($data->location) ? $data->location : null);
  2446. if ($this->name == 'activity_calendar') {
  2447. $params['instance'] = $this->task->get_activityid();
  2448. } else {
  2449. $params['instance'] = 0;
  2450. }
  2451. $sql = "SELECT id
  2452. FROM {event}
  2453. WHERE " . $DB->sql_compare_text('name', 255) . " = " . $DB->sql_compare_text('?', 255) . "
  2454. AND courseid = ?
  2455. AND modulename = ?
  2456. AND instance = ?
  2457. AND timestart = ?
  2458. AND timeduration = ?
  2459. AND " . $DB->sql_compare_text('description', 255) . " = " . $DB->sql_compare_text('?', 255);
  2460. $arg = array ($params['name'], $params['courseid'], $params['modulename'], $params['instance'], $params['timestart'], $params['timeduration'], $params['description']);
  2461. $result = $DB->record_exists_sql($sql, $arg);
  2462. if (empty($result)) {
  2463. $newitemid = $DB->insert_record('event', $params);
  2464. $this->set_mapping('event', $oldid, $newitemid);
  2465. $this->set_mapping('event_description', $oldid, $newitemid, $restorefiles);
  2466. }
  2467. // With repeating events, each event has the repeatid pointed at the first occurrence.
  2468. // Since the repeatid will be empty when the first occurrence is restored,
  2469. // Get the repeatid from the second occurrence of the repeating event and use that to update the first occurrence.
  2470. // Then keep a list of repeatids so we only perform this update once.
  2471. static $repeatids = array();
  2472. if (!empty($params['repeatid']) && !in_array($params['repeatid'], $repeatids)) {
  2473. // This entry is repeated so the repeatid field must be set.
  2474. $DB->set_field('event', 'repeatid', $params['repeatid'], array('id' => $params['repeatid']));
  2475. $repeatids[] = $params['repeatid'];
  2476. }
  2477. }
  2478. protected function after_execute() {
  2479. // Add related files
  2480. $this->add_related_files('calendar', 'event_description', 'event_description');
  2481. }
  2482. }
  2483. class restore_course_completion_structure_step extends restore_structure_step {
  2484. /**
  2485. * Conditionally decide if this step should be executed.
  2486. *
  2487. * This function checks parameters that are not immediate settings to ensure
  2488. * that the enviroment is suitable for the restore of course completion info.
  2489. *
  2490. * This function checks the following four parameters:
  2491. *
  2492. * 1. Course completion is enabled on the site
  2493. * 2. The backup includes course completion information
  2494. * 3. All modules are restorable
  2495. * 4. All modules are marked for restore.
  2496. * 5. No completion criteria already exist for the course.
  2497. *
  2498. * @return bool True is safe to execute, false otherwise
  2499. */
  2500. protected function execute_condition() {
  2501. global $CFG, $DB;
  2502. // First check course completion is enabled on this site
  2503. if (empty($CFG->enablecompletion)) {
  2504. // Disabled, don't restore course completion
  2505. return false;
  2506. }
  2507. // No course completion on the front page.
  2508. if ($this->get_courseid() == SITEID) {
  2509. return false;
  2510. }
  2511. // Check it is included in the backup
  2512. $fullpath = $this->task->get_taskbasepath();
  2513. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  2514. if (!file_exists($fullpath)) {
  2515. // Not found, can't restore course completion
  2516. return false;
  2517. }
  2518. // Check we are able to restore all backed up modules
  2519. if ($this->task->is_missing_modules()) {
  2520. return false;
  2521. }
  2522. // Check all modules within the backup are being restored.
  2523. if ($this->task->is_excluding_activities()) {
  2524. return false;
  2525. }
  2526. // Check that no completion criteria is already set for the course.
  2527. if ($DB->record_exists('course_completion_criteria', array('course' => $this->get_courseid()))) {
  2528. return false;
  2529. }
  2530. return true;
  2531. }
  2532. /**
  2533. * Define the course completion structure
  2534. *
  2535. * @return array Array of restore_path_element
  2536. */
  2537. protected function define_structure() {
  2538. // To know if we are including user completion info
  2539. $userinfo = $this->get_setting_value('userscompletion');
  2540. $paths = array();
  2541. $paths[] = new restore_path_element('course_completion_criteria', '/course_completion/course_completion_criteria');
  2542. $paths[] = new restore_path_element('course_completion_aggr_methd', '/course_completion/course_completion_aggr_methd');
  2543. if ($userinfo) {
  2544. $paths[] = new restore_path_element('course_completion_crit_compl', '/course_completion/course_completion_criteria/course_completion_crit_completions/course_completion_crit_compl');
  2545. $paths[] = new restore_path_element('course_completions', '/course_completion/course_completions');
  2546. }
  2547. return $paths;
  2548. }
  2549. /**
  2550. * Process course completion criteria
  2551. *
  2552. * @global moodle_database $DB
  2553. * @param stdClass $data
  2554. */
  2555. public function process_course_completion_criteria($data) {
  2556. global $DB;
  2557. $data = (object)$data;
  2558. $data->course = $this->get_courseid();
  2559. // Apply the date offset to the time end field
  2560. $data->timeend = $this->apply_date_offset($data->timeend);
  2561. // Map the role from the criteria
  2562. if (isset($data->role) && $data->role != '') {
  2563. // Newer backups should include roleshortname, which makes this much easier.
  2564. if (!empty($data->roleshortname)) {
  2565. $roleinstanceid = $DB->get_field('role', 'id', array('shortname' => $data->roleshortname));
  2566. if (!$roleinstanceid) {
  2567. $this->log(
  2568. 'Could not match the role shortname in course_completion_criteria, so skipping',
  2569. backup::LOG_DEBUG
  2570. );
  2571. return;
  2572. }
  2573. $data->role = $roleinstanceid;
  2574. } else {
  2575. $data->role = $this->get_mappingid('role', $data->role);
  2576. }
  2577. // Check we have an id, otherwise it causes all sorts of bugs.
  2578. if (!$data->role) {
  2579. $this->log(
  2580. 'Could not match role in course_completion_criteria, so skipping',
  2581. backup::LOG_DEBUG
  2582. );
  2583. return;
  2584. }
  2585. }
  2586. // If the completion criteria is for a module we need to map the module instance
  2587. // to the new module id.
  2588. if (!empty($data->moduleinstance) && !empty($data->module)) {
  2589. $data->moduleinstance = $this->get_mappingid('course_module', $data->moduleinstance);
  2590. if (empty($data->moduleinstance)) {
  2591. $this->log(
  2592. 'Could not match the module instance in course_completion_criteria, so skipping',
  2593. backup::LOG_DEBUG
  2594. );
  2595. return;
  2596. }
  2597. } else {
  2598. $data->module = null;
  2599. $data->moduleinstance = null;
  2600. }
  2601. // We backup the course shortname rather than the ID so that we can match back to the course
  2602. if (!empty($data->courseinstanceshortname)) {
  2603. $courseinstanceid = $DB->get_field('course', 'id', array('shortname'=>$data->courseinstanceshortname));
  2604. if (!$courseinstanceid) {
  2605. $this->log(
  2606. 'Could not match the course instance in course_completion_criteria, so skipping',
  2607. backup::LOG_DEBUG
  2608. );
  2609. return;
  2610. }
  2611. } else {
  2612. $courseinstanceid = null;
  2613. }
  2614. $data->courseinstance = $courseinstanceid;
  2615. $params = array(
  2616. 'course' => $data->course,
  2617. 'criteriatype' => $data->criteriatype,
  2618. 'enrolperiod' => $data->enrolperiod,
  2619. 'courseinstance' => $data->courseinstance,
  2620. 'module' => $data->module,
  2621. 'moduleinstance' => $data->moduleinstance,
  2622. 'timeend' => $data->timeend,
  2623. 'gradepass' => $data->gradepass,
  2624. 'role' => $data->role
  2625. );
  2626. $newid = $DB->insert_record('course_completion_criteria', $params);
  2627. $this->set_mapping('course_completion_criteria', $data->id, $newid);
  2628. }
  2629. /**
  2630. * Processes course compltion criteria complete records
  2631. *
  2632. * @global moodle_database $DB
  2633. * @param stdClass $data
  2634. */
  2635. public function process_course_completion_crit_compl($data) {
  2636. global $DB;
  2637. $data = (object)$data;
  2638. // This may be empty if criteria could not be restored
  2639. $data->criteriaid = $this->get_mappingid('course_completion_criteria', $data->criteriaid);
  2640. $data->course = $this->get_courseid();
  2641. $data->userid = $this->get_mappingid('user', $data->userid);
  2642. if (!empty($data->criteriaid) && !empty($data->userid)) {
  2643. $params = array(
  2644. 'userid' => $data->userid,
  2645. 'course' => $data->course,
  2646. 'criteriaid' => $data->criteriaid,
  2647. 'timecompleted' => $data->timecompleted
  2648. );
  2649. if (isset($data->gradefinal)) {
  2650. $params['gradefinal'] = $data->gradefinal;
  2651. }
  2652. if (isset($data->unenroled)) {
  2653. $params['unenroled'] = $data->unenroled;
  2654. }
  2655. $DB->insert_record('course_completion_crit_compl', $params);
  2656. }
  2657. }
  2658. /**
  2659. * Process course completions
  2660. *
  2661. * @global moodle_database $DB
  2662. * @param stdClass $data
  2663. */
  2664. public function process_course_completions($data) {
  2665. global $DB;
  2666. $data = (object)$data;
  2667. $data->course = $this->get_courseid();
  2668. $data->userid = $this->get_mappingid('user', $data->userid);
  2669. if (!empty($data->userid)) {
  2670. $params = array(
  2671. 'userid' => $data->userid,
  2672. 'course' => $data->course,
  2673. 'timeenrolled' => $data->timeenrolled,
  2674. 'timestarted' => $data->timestarted,
  2675. 'timecompleted' => $data->timecompleted,
  2676. 'reaggregate' => $data->reaggregate
  2677. );
  2678. $existing = $DB->get_record('course_completions', array(
  2679. 'userid' => $data->userid,
  2680. 'course' => $data->course
  2681. ));
  2682. // MDL-46651 - If cron writes out a new record before we get to it
  2683. // then we should replace it with the Truth data from the backup.
  2684. // This may be obsolete after MDL-48518 is resolved
  2685. if ($existing) {
  2686. $params['id'] = $existing->id;
  2687. $DB->update_record('course_completions', $params);
  2688. } else {
  2689. $DB->insert_record('course_completions', $params);
  2690. }
  2691. }
  2692. }
  2693. /**
  2694. * Process course completion aggregate methods
  2695. *
  2696. * @global moodle_database $DB
  2697. * @param stdClass $data
  2698. */
  2699. public function process_course_completion_aggr_methd($data) {
  2700. global $DB;
  2701. $data = (object)$data;
  2702. $data->course = $this->get_courseid();
  2703. // Only create the course_completion_aggr_methd records if
  2704. // the target course has not them defined. MDL-28180
  2705. if (!$DB->record_exists('course_completion_aggr_methd', array(
  2706. 'course' => $data->course,
  2707. 'criteriatype' => $data->criteriatype))) {
  2708. $params = array(
  2709. 'course' => $data->course,
  2710. 'criteriatype' => $data->criteriatype,
  2711. 'method' => $data->method,
  2712. 'value' => $data->value,
  2713. );
  2714. $DB->insert_record('course_completion_aggr_methd', $params);
  2715. }
  2716. }
  2717. }
  2718. /**
  2719. * This structure step restores course logs (cmid = 0), delegating
  2720. * the hard work to the corresponding {@link restore_logs_processor} passing the
  2721. * collection of {@link restore_log_rule} rules to be observed as they are defined
  2722. * by the task. Note this is only executed based in the 'logs' setting.
  2723. *
  2724. * NOTE: This is executed by final task, to have all the activities already restored
  2725. *
  2726. * NOTE: Not all course logs are being restored. For now only 'course' and 'user'
  2727. * records are. There are others like 'calendar' and 'upload' that will be handled
  2728. * later.
  2729. *
  2730. * NOTE: All the missing actions (not able to be restored) are sent to logs for
  2731. * debugging purposes
  2732. */
  2733. class restore_course_logs_structure_step extends restore_structure_step {
  2734. /**
  2735. * Conditionally decide if this step should be executed.
  2736. *
  2737. * This function checks the following parameter:
  2738. *
  2739. * 1. the course/logs.xml file exists
  2740. *
  2741. * @return bool true is safe to execute, false otherwise
  2742. */
  2743. protected function execute_condition() {
  2744. // Check it is included in the backup
  2745. $fullpath = $this->task->get_taskbasepath();
  2746. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  2747. if (!file_exists($fullpath)) {
  2748. // Not found, can't restore course logs
  2749. return false;
  2750. }
  2751. return true;
  2752. }
  2753. protected function define_structure() {
  2754. $paths = array();
  2755. // Simple, one plain level of information contains them
  2756. $paths[] = new restore_path_element('log', '/logs/log');
  2757. return $paths;
  2758. }
  2759. protected function process_log($data) {
  2760. global $DB;
  2761. $data = (object)($data);
  2762. // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
  2763. $data->userid = $this->get_mappingid('user', $data->userid);
  2764. $data->course = $this->get_courseid();
  2765. $data->cmid = 0;
  2766. // For any reason user wasn't remapped ok, stop processing this
  2767. if (empty($data->userid)) {
  2768. return;
  2769. }
  2770. // Everything ready, let's delegate to the restore_logs_processor
  2771. // Set some fixed values that will save tons of DB requests
  2772. $values = array(
  2773. 'course' => $this->get_courseid());
  2774. // Get instance and process log record
  2775. $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
  2776. // If we have data, insert it, else something went wrong in the restore_logs_processor
  2777. if ($data) {
  2778. if (empty($data->url)) {
  2779. $data->url = '';
  2780. }
  2781. if (empty($data->info)) {
  2782. $data->info = '';
  2783. }
  2784. // Store the data in the legacy log table if we are still using it.
  2785. $manager = get_log_manager();
  2786. if (method_exists($manager, 'legacy_add_to_log')) {
  2787. $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
  2788. $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
  2789. }
  2790. }
  2791. }
  2792. }
  2793. /**
  2794. * This structure step restores activity logs, extending {@link restore_course_logs_structure_step}
  2795. * sharing its same structure but modifying the way records are handled
  2796. */
  2797. class restore_activity_logs_structure_step extends restore_course_logs_structure_step {
  2798. protected function process_log($data) {
  2799. global $DB;
  2800. $data = (object)($data);
  2801. // There is no need to roll dates. Logs are supposed to be immutable. See MDL-44961.
  2802. $data->userid = $this->get_mappingid('user', $data->userid);
  2803. $data->course = $this->get_courseid();
  2804. $data->cmid = $this->task->get_moduleid();
  2805. // For any reason user wasn't remapped ok, stop processing this
  2806. if (empty($data->userid)) {
  2807. return;
  2808. }
  2809. // Everything ready, let's delegate to the restore_logs_processor
  2810. // Set some fixed values that will save tons of DB requests
  2811. $values = array(
  2812. 'course' => $this->get_courseid(),
  2813. 'course_module' => $this->task->get_moduleid(),
  2814. $this->task->get_modulename() => $this->task->get_activityid());
  2815. // Get instance and process log record
  2816. $data = restore_logs_processor::get_instance($this->task, $values)->process_log_record($data);
  2817. // If we have data, insert it, else something went wrong in the restore_logs_processor
  2818. if ($data) {
  2819. if (empty($data->url)) {
  2820. $data->url = '';
  2821. }
  2822. if (empty($data->info)) {
  2823. $data->info = '';
  2824. }
  2825. // Store the data in the legacy log table if we are still using it.
  2826. $manager = get_log_manager();
  2827. if (method_exists($manager, 'legacy_add_to_log')) {
  2828. $manager->legacy_add_to_log($data->course, $data->module, $data->action, $data->url,
  2829. $data->info, $data->cmid, $data->userid, $data->ip, $data->time);
  2830. }
  2831. }
  2832. }
  2833. }
  2834. /**
  2835. * Structure step in charge of restoring the logstores.xml file for the course logs.
  2836. *
  2837. * This restore step will rebuild the logs for all the enabled logstore subplugins supporting
  2838. * it, for logs belonging to the course level.
  2839. */
  2840. class restore_course_logstores_structure_step extends restore_structure_step {
  2841. /**
  2842. * Conditionally decide if this step should be executed.
  2843. *
  2844. * This function checks the following parameter:
  2845. *
  2846. * 1. the logstores.xml file exists
  2847. *
  2848. * @return bool true is safe to execute, false otherwise
  2849. */
  2850. protected function execute_condition() {
  2851. // Check it is included in the backup.
  2852. $fullpath = $this->task->get_taskbasepath();
  2853. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  2854. if (!file_exists($fullpath)) {
  2855. // Not found, can't restore logstores.xml information.
  2856. return false;
  2857. }
  2858. return true;
  2859. }
  2860. /**
  2861. * Return the elements to be processed on restore of logstores.
  2862. *
  2863. * @return restore_path_element[] array of elements to be processed on restore.
  2864. */
  2865. protected function define_structure() {
  2866. $paths = array();
  2867. $logstore = new restore_path_element('logstore', '/logstores/logstore');
  2868. $paths[] = $logstore;
  2869. // Add logstore subplugin support to the 'logstore' element.
  2870. $this->add_subplugin_structure('logstore', $logstore, 'tool', 'log');
  2871. return array($logstore);
  2872. }
  2873. /**
  2874. * Process the 'logstore' element,
  2875. *
  2876. * Note: This is empty by definition in backup, because stores do not share any
  2877. * data between them, so there is nothing to process here.
  2878. *
  2879. * @param array $data element data
  2880. */
  2881. protected function process_logstore($data) {
  2882. return;
  2883. }
  2884. }
  2885. /**
  2886. * Structure step in charge of restoring the logstores.xml file for the activity logs.
  2887. *
  2888. * Note: Activity structure is completely equivalent to the course one, so just extend it.
  2889. */
  2890. class restore_activity_logstores_structure_step extends restore_course_logstores_structure_step {
  2891. }
  2892. /**
  2893. * Restore course competencies structure step.
  2894. */
  2895. class restore_course_competencies_structure_step extends restore_structure_step {
  2896. /**
  2897. * Returns the structure.
  2898. *
  2899. * @return array
  2900. */
  2901. protected function define_structure() {
  2902. $userinfo = $this->get_setting_value('users');
  2903. $paths = array(
  2904. new restore_path_element('course_competency', '/course_competencies/competencies/competency'),
  2905. new restore_path_element('course_competency_settings', '/course_competencies/settings'),
  2906. );
  2907. if ($userinfo) {
  2908. $paths[] = new restore_path_element('user_competency_course',
  2909. '/course_competencies/user_competencies/user_competency');
  2910. }
  2911. return $paths;
  2912. }
  2913. /**
  2914. * Process a course competency settings.
  2915. *
  2916. * @param array $data The data.
  2917. */
  2918. public function process_course_competency_settings($data) {
  2919. global $DB;
  2920. $data = (object) $data;
  2921. // We do not restore the course settings during merge.
  2922. $target = $this->get_task()->get_target();
  2923. if ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING) {
  2924. return;
  2925. }
  2926. $courseid = $this->task->get_courseid();
  2927. $exists = \core_competency\course_competency_settings::record_exists_select('courseid = :courseid',
  2928. array('courseid' => $courseid));
  2929. // Strangely the course settings already exist, let's just leave them as is then.
  2930. if ($exists) {
  2931. $this->log('Course competency settings not restored, existing settings have been found.', backup::LOG_WARNING);
  2932. return;
  2933. }
  2934. $data = (object) array('courseid' => $courseid, 'pushratingstouserplans' => $data->pushratingstouserplans);
  2935. $settings = new \core_competency\course_competency_settings(0, $data);
  2936. $settings->create();
  2937. }
  2938. /**
  2939. * Process a course competency.
  2940. *
  2941. * @param array $data The data.
  2942. */
  2943. public function process_course_competency($data) {
  2944. $data = (object) $data;
  2945. // Mapping the competency by ID numbers.
  2946. $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber));
  2947. if (!$framework) {
  2948. return;
  2949. }
  2950. $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
  2951. 'competencyframeworkid' => $framework->get('id')));
  2952. if (!$competency) {
  2953. return;
  2954. }
  2955. $this->set_mapping(\core_competency\competency::TABLE, $data->id, $competency->get('id'));
  2956. $params = array(
  2957. 'competencyid' => $competency->get('id'),
  2958. 'courseid' => $this->task->get_courseid()
  2959. );
  2960. $query = 'competencyid = :competencyid AND courseid = :courseid';
  2961. $existing = \core_competency\course_competency::record_exists_select($query, $params);
  2962. if (!$existing) {
  2963. // Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
  2964. $record = (object) $params;
  2965. $record->ruleoutcome = $data->ruleoutcome;
  2966. $coursecompetency = new \core_competency\course_competency(0, $record);
  2967. $coursecompetency->create();
  2968. }
  2969. }
  2970. /**
  2971. * Process the user competency course.
  2972. *
  2973. * @param array $data The data.
  2974. */
  2975. public function process_user_competency_course($data) {
  2976. global $USER, $DB;
  2977. $data = (object) $data;
  2978. $data->competencyid = $this->get_mappingid(\core_competency\competency::TABLE, $data->competencyid);
  2979. if (!$data->competencyid) {
  2980. // This is strange, the competency does not belong to the course.
  2981. return;
  2982. } else if ($data->grade === null) {
  2983. // We do not need to do anything when there is no grade.
  2984. return;
  2985. }
  2986. $data->userid = $this->get_mappingid('user', $data->userid);
  2987. $shortname = $DB->get_field('course', 'shortname', array('id' => $this->task->get_courseid()), MUST_EXIST);
  2988. // The method add_evidence also sets the course rating.
  2989. \core_competency\api::add_evidence($data->userid,
  2990. $data->competencyid,
  2991. $this->task->get_contextid(),
  2992. \core_competency\evidence::ACTION_OVERRIDE,
  2993. 'evidence_courserestored',
  2994. 'core_competency',
  2995. $shortname,
  2996. false,
  2997. null,
  2998. $data->grade,
  2999. $USER->id);
  3000. }
  3001. /**
  3002. * Execute conditions.
  3003. *
  3004. * @return bool
  3005. */
  3006. protected function execute_condition() {
  3007. // Do not execute if competencies are not included.
  3008. if (!$this->get_setting_value('competencies')) {
  3009. return false;
  3010. }
  3011. // Do not execute if the competencies XML file is not found.
  3012. $fullpath = $this->task->get_taskbasepath();
  3013. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  3014. if (!file_exists($fullpath)) {
  3015. return false;
  3016. }
  3017. return true;
  3018. }
  3019. }
  3020. /**
  3021. * Restore activity competencies structure step.
  3022. */
  3023. class restore_activity_competencies_structure_step extends restore_structure_step {
  3024. /**
  3025. * Defines the structure.
  3026. *
  3027. * @return array
  3028. */
  3029. protected function define_structure() {
  3030. $paths = array(
  3031. new restore_path_element('course_module_competency', '/course_module_competencies/competencies/competency')
  3032. );
  3033. return $paths;
  3034. }
  3035. /**
  3036. * Process a course module competency.
  3037. *
  3038. * @param array $data The data.
  3039. */
  3040. public function process_course_module_competency($data) {
  3041. $data = (object) $data;
  3042. // Mapping the competency by ID numbers.
  3043. $framework = \core_competency\competency_framework::get_record(array('idnumber' => $data->frameworkidnumber));
  3044. if (!$framework) {
  3045. return;
  3046. }
  3047. $competency = \core_competency\competency::get_record(array('idnumber' => $data->idnumber,
  3048. 'competencyframeworkid' => $framework->get('id')));
  3049. if (!$competency) {
  3050. return;
  3051. }
  3052. $params = array(
  3053. 'competencyid' => $competency->get('id'),
  3054. 'cmid' => $this->task->get_moduleid()
  3055. );
  3056. $query = 'competencyid = :competencyid AND cmid = :cmid';
  3057. $existing = \core_competency\course_module_competency::record_exists_select($query, $params);
  3058. if (!$existing) {
  3059. // Sortorder is ignored by precaution, anyway we should walk through the records in the right order.
  3060. $record = (object) $params;
  3061. $record->ruleoutcome = $data->ruleoutcome;
  3062. $coursemodulecompetency = new \core_competency\course_module_competency(0, $record);
  3063. $coursemodulecompetency->create();
  3064. }
  3065. }
  3066. /**
  3067. * Execute conditions.
  3068. *
  3069. * @return bool
  3070. */
  3071. protected function execute_condition() {
  3072. // Do not execute if competencies are not included.
  3073. if (!$this->get_setting_value('competencies')) {
  3074. return false;
  3075. }
  3076. // Do not execute if the competencies XML file is not found.
  3077. $fullpath = $this->task->get_taskbasepath();
  3078. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  3079. if (!file_exists($fullpath)) {
  3080. return false;
  3081. }
  3082. return true;
  3083. }
  3084. }
  3085. /**
  3086. * Defines the restore step for advanced grading methods attached to the activity module
  3087. */
  3088. class restore_activity_grading_structure_step extends restore_structure_step {
  3089. /**
  3090. * This step is executed only if the grading file is present
  3091. */
  3092. protected function execute_condition() {
  3093. if ($this->get_courseid() == SITEID) {
  3094. return false;
  3095. }
  3096. $fullpath = $this->task->get_taskbasepath();
  3097. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  3098. if (!file_exists($fullpath)) {
  3099. return false;
  3100. }
  3101. return true;
  3102. }
  3103. /**
  3104. * Declares paths in the grading.xml file we are interested in
  3105. */
  3106. protected function define_structure() {
  3107. $paths = array();
  3108. $userinfo = $this->get_setting_value('userinfo');
  3109. $area = new restore_path_element('grading_area', '/areas/area');
  3110. $paths[] = $area;
  3111. // attach local plugin stucture to $area element
  3112. $this->add_plugin_structure('local', $area);
  3113. $definition = new restore_path_element('grading_definition', '/areas/area/definitions/definition');
  3114. $paths[] = $definition;
  3115. $this->add_plugin_structure('gradingform', $definition);
  3116. // attach local plugin stucture to $definition element
  3117. $this->add_plugin_structure('local', $definition);
  3118. if ($userinfo) {
  3119. $instance = new restore_path_element('grading_instance',
  3120. '/areas/area/definitions/definition/instances/instance');
  3121. $paths[] = $instance;
  3122. $this->add_plugin_structure('gradingform', $instance);
  3123. // attach local plugin stucture to $intance element
  3124. $this->add_plugin_structure('local', $instance);
  3125. }
  3126. return $paths;
  3127. }
  3128. /**
  3129. * Processes one grading area element
  3130. *
  3131. * @param array $data element data
  3132. */
  3133. protected function process_grading_area($data) {
  3134. global $DB;
  3135. $task = $this->get_task();
  3136. $data = (object)$data;
  3137. $oldid = $data->id;
  3138. $data->component = 'mod_'.$task->get_modulename();
  3139. $data->contextid = $task->get_contextid();
  3140. $newid = $DB->insert_record('grading_areas', $data);
  3141. $this->set_mapping('grading_area', $oldid, $newid);
  3142. }
  3143. /**
  3144. * Processes one grading definition element
  3145. *
  3146. * @param array $data element data
  3147. */
  3148. protected function process_grading_definition($data) {
  3149. global $DB;
  3150. $task = $this->get_task();
  3151. $data = (object)$data;
  3152. $oldid = $data->id;
  3153. $data->areaid = $this->get_new_parentid('grading_area');
  3154. $data->copiedfromid = null;
  3155. $data->timecreated = time();
  3156. $data->usercreated = $task->get_userid();
  3157. $data->timemodified = $data->timecreated;
  3158. $data->usermodified = $data->usercreated;
  3159. $newid = $DB->insert_record('grading_definitions', $data);
  3160. $this->set_mapping('grading_definition', $oldid, $newid, true);
  3161. }
  3162. /**
  3163. * Processes one grading form instance element
  3164. *
  3165. * @param array $data element data
  3166. */
  3167. protected function process_grading_instance($data) {
  3168. global $DB;
  3169. $data = (object)$data;
  3170. // new form definition id
  3171. $newformid = $this->get_new_parentid('grading_definition');
  3172. // get the name of the area we are restoring to
  3173. $sql = "SELECT ga.areaname
  3174. FROM {grading_definitions} gd
  3175. JOIN {grading_areas} ga ON gd.areaid = ga.id
  3176. WHERE gd.id = ?";
  3177. $areaname = $DB->get_field_sql($sql, array($newformid), MUST_EXIST);
  3178. // get the mapped itemid - the activity module is expected to define the mappings
  3179. // for each gradable area
  3180. $newitemid = $this->get_mappingid(restore_gradingform_plugin::itemid_mapping($areaname), $data->itemid);
  3181. $oldid = $data->id;
  3182. $data->definitionid = $newformid;
  3183. $data->raterid = $this->get_mappingid('user', $data->raterid);
  3184. $data->itemid = $newitemid;
  3185. $newid = $DB->insert_record('grading_instances', $data);
  3186. $this->set_mapping('grading_instance', $oldid, $newid);
  3187. }
  3188. /**
  3189. * Final operations when the database records are inserted
  3190. */
  3191. protected function after_execute() {
  3192. // Add files embedded into the definition description
  3193. $this->add_related_files('grading', 'description', 'grading_definition');
  3194. }
  3195. }
  3196. /**
  3197. * This structure step restores the grade items associated with one activity
  3198. * All the grade items are made child of the "course" grade item but the original
  3199. * categoryid is saved as parentitemid in the backup_ids table, so, when restoring
  3200. * the complete gradebook (categories and calculations), that information is
  3201. * available there
  3202. */
  3203. class restore_activity_grades_structure_step extends restore_structure_step {
  3204. /**
  3205. * No grades in front page.
  3206. * @return bool
  3207. */
  3208. protected function execute_condition() {
  3209. return ($this->get_courseid() != SITEID);
  3210. }
  3211. protected function define_structure() {
  3212. $paths = array();
  3213. $userinfo = $this->get_setting_value('userinfo');
  3214. $paths[] = new restore_path_element('grade_item', '/activity_gradebook/grade_items/grade_item');
  3215. $paths[] = new restore_path_element('grade_letter', '/activity_gradebook/grade_letters/grade_letter');
  3216. if ($userinfo) {
  3217. $paths[] = new restore_path_element('grade_grade',
  3218. '/activity_gradebook/grade_items/grade_item/grade_grades/grade_grade');
  3219. }
  3220. return $paths;
  3221. }
  3222. protected function process_grade_item($data) {
  3223. global $DB;
  3224. $data = (object)($data);
  3225. $oldid = $data->id; // We'll need these later
  3226. $oldparentid = $data->categoryid;
  3227. $courseid = $this->get_courseid();
  3228. $idnumber = null;
  3229. if (!empty($data->idnumber)) {
  3230. // Don't get any idnumber from course module. Keep them as they are in grade_item->idnumber
  3231. // Reason: it's not clear what happens with outcomes->idnumber or activities with multiple items (workshop)
  3232. // so the best is to keep the ones already in the gradebook
  3233. // Potential problem: duplicates if same items are restored more than once. :-(
  3234. // This needs to be fixed in some way (outcomes & activities with multiple items)
  3235. // $data->idnumber = get_coursemodule_from_instance($data->itemmodule, $data->iteminstance)->idnumber;
  3236. // In any case, verify always for uniqueness
  3237. $sql = "SELECT cm.id
  3238. FROM {course_modules} cm
  3239. WHERE cm.course = :courseid AND
  3240. cm.idnumber = :idnumber AND
  3241. cm.id <> :cmid";
  3242. $params = array(
  3243. 'courseid' => $courseid,
  3244. 'idnumber' => $data->idnumber,
  3245. 'cmid' => $this->task->get_moduleid()
  3246. );
  3247. if (!$DB->record_exists_sql($sql, $params) && !$DB->record_exists('grade_items', array('courseid' => $courseid, 'idnumber' => $data->idnumber))) {
  3248. $idnumber = $data->idnumber;
  3249. }
  3250. }
  3251. if (!empty($data->categoryid)) {
  3252. // If the grade category id of the grade item being restored belongs to this course
  3253. // then it is a fair assumption that this is the correct grade category for the activity
  3254. // and we should leave it in place, if not then unset it.
  3255. // TODO MDL-34790 Gradebook does not import if target course has gradebook categories.
  3256. $conditions = array('id' => $data->categoryid, 'courseid' => $courseid);
  3257. if (!$this->task->is_samesite() || !$DB->record_exists('grade_categories', $conditions)) {
  3258. unset($data->categoryid);
  3259. }
  3260. }
  3261. unset($data->id);
  3262. $data->courseid = $this->get_courseid();
  3263. $data->iteminstance = $this->task->get_activityid();
  3264. $data->idnumber = $idnumber;
  3265. $data->scaleid = $this->get_mappingid('scale', $data->scaleid);
  3266. $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid);
  3267. $gradeitem = new grade_item($data, false);
  3268. $gradeitem->insert('restore');
  3269. //sortorder is automatically assigned when inserting. Re-instate the previous sortorder
  3270. $gradeitem->sortorder = $data->sortorder;
  3271. $gradeitem->update('restore');
  3272. // Set mapping, saving the original category id into parentitemid
  3273. // gradebook restore (final task) will need it to reorganise items
  3274. $this->set_mapping('grade_item', $oldid, $gradeitem->id, false, null, $oldparentid);
  3275. }
  3276. protected function process_grade_grade($data) {
  3277. global $CFG;
  3278. require_once($CFG->libdir . '/grade/constants.php');
  3279. $data = (object)($data);
  3280. $olduserid = $data->userid;
  3281. $oldid = $data->id;
  3282. unset($data->id);
  3283. $data->itemid = $this->get_new_parentid('grade_item');
  3284. $data->userid = $this->get_mappingid('user', $data->userid, null);
  3285. if (!empty($data->userid)) {
  3286. $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
  3287. $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
  3288. $grade = new grade_grade($data, false);
  3289. $grade->insert('restore');
  3290. $this->set_mapping('grade_grades', $oldid, $grade->id, true);
  3291. $this->add_related_files(
  3292. GRADE_FILE_COMPONENT,
  3293. GRADE_FEEDBACK_FILEAREA,
  3294. 'grade_grades',
  3295. null,
  3296. $oldid
  3297. );
  3298. } else {
  3299. debugging("Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'");
  3300. }
  3301. }
  3302. /**
  3303. * process activity grade_letters. Note that, while these are possible,
  3304. * because grade_letters are contextid based, in practice, only course
  3305. * context letters can be defined. So we keep here this method knowing
  3306. * it won't be executed ever. gradebook restore will restore course letters.
  3307. */
  3308. protected function process_grade_letter($data) {
  3309. global $DB;
  3310. $data['contextid'] = $this->task->get_contextid();
  3311. $gradeletter = (object)$data;
  3312. // Check if it exists before adding it
  3313. unset($data['id']);
  3314. if (!$DB->record_exists('grade_letters', $data)) {
  3315. $newitemid = $DB->insert_record('grade_letters', $gradeletter);
  3316. }
  3317. // no need to save any grade_letter mapping
  3318. }
  3319. public function after_restore() {
  3320. // Fix grade item's sortorder after restore, as it might have duplicates.
  3321. $courseid = $this->get_task()->get_courseid();
  3322. grade_item::fix_duplicate_sortorder($courseid);
  3323. }
  3324. }
  3325. /**
  3326. * Step in charge of restoring the grade history of an activity.
  3327. *
  3328. * This step is added to the task regardless of the setting 'grade_histories'.
  3329. * The reason is to allow for a more flexible step in case the logic needs to be
  3330. * split accross different settings to control the history of items and/or grades.
  3331. */
  3332. class restore_activity_grade_history_structure_step extends restore_structure_step {
  3333. /**
  3334. * This step is executed only if the grade history file is present.
  3335. */
  3336. protected function execute_condition() {
  3337. if ($this->get_courseid() == SITEID) {
  3338. return false;
  3339. }
  3340. $fullpath = $this->task->get_taskbasepath();
  3341. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  3342. if (!file_exists($fullpath)) {
  3343. return false;
  3344. }
  3345. return true;
  3346. }
  3347. protected function define_structure() {
  3348. $paths = array();
  3349. // Settings to use.
  3350. $userinfo = $this->get_setting_value('userinfo');
  3351. $history = $this->get_setting_value('grade_histories');
  3352. if ($userinfo && $history) {
  3353. $paths[] = new restore_path_element('grade_grade',
  3354. '/grade_history/grade_grades/grade_grade');
  3355. }
  3356. return $paths;
  3357. }
  3358. protected function process_grade_grade($data) {
  3359. global $CFG, $DB;
  3360. require_once($CFG->libdir . '/grade/constants.php');
  3361. $data = (object) $data;
  3362. $oldhistoryid = $data->id;
  3363. $olduserid = $data->userid;
  3364. unset($data->id);
  3365. $data->userid = $this->get_mappingid('user', $data->userid, null);
  3366. if (!empty($data->userid)) {
  3367. // Do not apply the date offsets as this is history.
  3368. $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
  3369. $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
  3370. $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
  3371. $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
  3372. $newhistoryid = $DB->insert_record('grade_grades_history', $data);
  3373. $this->set_mapping('grade_grades_history', $oldhistoryid, $newhistoryid, true);
  3374. $this->add_related_files(
  3375. GRADE_FILE_COMPONENT,
  3376. GRADE_HISTORY_FEEDBACK_FILEAREA,
  3377. 'grade_grades_history',
  3378. null,
  3379. $oldhistoryid
  3380. );
  3381. } else {
  3382. $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
  3383. $this->log($message, backup::LOG_DEBUG);
  3384. }
  3385. }
  3386. }
  3387. /**
  3388. * This structure steps restores one instance + positions of one block
  3389. * Note: Positions corresponding to one existing context are restored
  3390. * here, but all the ones having unknown contexts are sent to backup_ids
  3391. * for a later chance to be restored at the end (final task)
  3392. */
  3393. class restore_block_instance_structure_step extends restore_structure_step {
  3394. protected function define_structure() {
  3395. $paths = array();
  3396. $paths[] = new restore_path_element('block', '/block', true); // Get the whole XML together
  3397. $paths[] = new restore_path_element('block_position', '/block/block_positions/block_position');
  3398. return $paths;
  3399. }
  3400. public function process_block($data) {
  3401. global $DB, $CFG;
  3402. $data = (object)$data; // Handy
  3403. $oldcontextid = $data->contextid;
  3404. $oldid = $data->id;
  3405. $positions = isset($data->block_positions['block_position']) ? $data->block_positions['block_position'] : array();
  3406. // Look for the parent contextid
  3407. if (!$data->parentcontextid = $this->get_mappingid('context', $data->parentcontextid)) {
  3408. throw new restore_step_exception('restore_block_missing_parent_ctx', $data->parentcontextid);
  3409. }
  3410. // TODO: it would be nice to use standard plugin supports instead of this instance_allow_multiple()
  3411. // If there is already one block of that type in the parent context
  3412. // and the block is not multiple, stop processing
  3413. // Use blockslib loader / method executor
  3414. if (!$bi = block_instance($data->blockname)) {
  3415. return false;
  3416. }
  3417. if (!$bi->instance_allow_multiple()) {
  3418. // The block cannot be added twice, so we will check if the same block is already being
  3419. // displayed on the same page. For this, rather than mocking a page and using the block_manager
  3420. // we use a similar query to the one in block_manager::load_blocks(), this will give us
  3421. // a very good idea of the blocks already displayed in the context.
  3422. $params = array(
  3423. 'blockname' => $data->blockname
  3424. );
  3425. // Context matching test.
  3426. $context = context::instance_by_id($data->parentcontextid);
  3427. $contextsql = 'bi.parentcontextid = :contextid';
  3428. $params['contextid'] = $context->id;
  3429. $parentcontextids = $context->get_parent_context_ids();
  3430. if ($parentcontextids) {
  3431. list($parentcontextsql, $parentcontextparams) =
  3432. $DB->get_in_or_equal($parentcontextids, SQL_PARAMS_NAMED);
  3433. $contextsql = "($contextsql OR (bi.showinsubcontexts = 1 AND bi.parentcontextid $parentcontextsql))";
  3434. $params = array_merge($params, $parentcontextparams);
  3435. }
  3436. // Page type pattern test.
  3437. $pagetypepatterns = matching_page_type_patterns_from_pattern($data->pagetypepattern);
  3438. list($pagetypepatternsql, $pagetypepatternparams) =
  3439. $DB->get_in_or_equal($pagetypepatterns, SQL_PARAMS_NAMED);
  3440. $params = array_merge($params, $pagetypepatternparams);
  3441. // Sub page pattern test.
  3442. $subpagepatternsql = 'bi.subpagepattern IS NULL';
  3443. if ($data->subpagepattern !== null) {
  3444. $subpagepatternsql = "($subpagepatternsql OR bi.subpagepattern = :subpagepattern)";
  3445. $params['subpagepattern'] = $data->subpagepattern;
  3446. }
  3447. $exists = $DB->record_exists_sql("SELECT bi.id
  3448. FROM {block_instances} bi
  3449. JOIN {block} b ON b.name = bi.blockname
  3450. WHERE bi.blockname = :blockname
  3451. AND $contextsql
  3452. AND bi.pagetypepattern $pagetypepatternsql
  3453. AND $subpagepatternsql", $params);
  3454. if ($exists) {
  3455. // There is at least one very similar block visible on the page where we
  3456. // are trying to restore the block. In these circumstances the block API
  3457. // would not allow the user to add another instance of the block, so we
  3458. // apply the same rule here.
  3459. return false;
  3460. }
  3461. }
  3462. // If there is already one block of that type in the parent context
  3463. // with the same showincontexts, pagetypepattern, subpagepattern, defaultregion and configdata
  3464. // stop processing
  3465. $params = array(
  3466. 'blockname' => $data->blockname, 'parentcontextid' => $data->parentcontextid,
  3467. 'showinsubcontexts' => $data->showinsubcontexts, 'pagetypepattern' => $data->pagetypepattern,
  3468. 'subpagepattern' => $data->subpagepattern, 'defaultregion' => $data->defaultregion);
  3469. if ($birecs = $DB->get_records('block_instances', $params)) {
  3470. foreach($birecs as $birec) {
  3471. if ($birec->configdata == $data->configdata) {
  3472. return false;
  3473. }
  3474. }
  3475. }
  3476. // Set task old contextid, blockid and blockname once we know them
  3477. $this->task->set_old_contextid($oldcontextid);
  3478. $this->task->set_old_blockid($oldid);
  3479. $this->task->set_blockname($data->blockname);
  3480. // Let's look for anything within configdata neededing processing
  3481. // (nulls and uses of legacy file.php)
  3482. if ($attrstotransform = $this->task->get_configdata_encoded_attributes()) {
  3483. $configdata = (array)unserialize(base64_decode($data->configdata));
  3484. foreach ($configdata as $attribute => $value) {
  3485. if (in_array($attribute, $attrstotransform)) {
  3486. $configdata[$attribute] = $this->contentprocessor->process_cdata($value);
  3487. }
  3488. }
  3489. $data->configdata = base64_encode(serialize((object)$configdata));
  3490. }
  3491. // Set timecreated, timemodified if not included (older backup).
  3492. if (empty($data->timecreated)) {
  3493. $data->timecreated = time();
  3494. }
  3495. if (empty($data->timemodified)) {
  3496. $data->timemodified = $data->timecreated;
  3497. }
  3498. // Create the block instance
  3499. $newitemid = $DB->insert_record('block_instances', $data);
  3500. // Save the mapping (with restorefiles support)
  3501. $this->set_mapping('block_instance', $oldid, $newitemid, true);
  3502. // Create the block context
  3503. $newcontextid = context_block::instance($newitemid)->id;
  3504. // Save the block contexts mapping and sent it to task
  3505. $this->set_mapping('context', $oldcontextid, $newcontextid);
  3506. $this->task->set_contextid($newcontextid);
  3507. $this->task->set_blockid($newitemid);
  3508. // Restore block fileareas if declared
  3509. $component = 'block_' . $this->task->get_blockname();
  3510. foreach ($this->task->get_fileareas() as $filearea) { // Simple match by contextid. No itemname needed
  3511. $this->add_related_files($component, $filearea, null);
  3512. }
  3513. // Process block positions, creating them or accumulating for final step
  3514. foreach($positions as $position) {
  3515. $position = (object)$position;
  3516. $position->blockinstanceid = $newitemid; // The instance is always the restored one
  3517. // If position is for one already mapped (known) contextid
  3518. // process it now, creating the position
  3519. if ($newpositionctxid = $this->get_mappingid('context', $position->contextid)) {
  3520. $position->contextid = $newpositionctxid;
  3521. // Create the block position
  3522. $DB->insert_record('block_positions', $position);
  3523. // The position belongs to an unknown context, send it to backup_ids
  3524. // to process them as part of the final steps of restore. We send the
  3525. // whole $position object there, hence use the low level method.
  3526. } else {
  3527. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'block_position', $position->id, 0, null, $position);
  3528. }
  3529. }
  3530. }
  3531. }
  3532. /**
  3533. * Structure step to restore common course_module information
  3534. *
  3535. * This step will process the module.xml file for one activity, in order to restore
  3536. * the corresponding information to the course_modules table, skipping various bits
  3537. * of information based on CFG settings (groupings, completion...) in order to fullfill
  3538. * all the reqs to be able to create the context to be used by all the rest of steps
  3539. * in the activity restore task
  3540. */
  3541. class restore_module_structure_step extends restore_structure_step {
  3542. protected function define_structure() {
  3543. global $CFG;
  3544. $paths = array();
  3545. $module = new restore_path_element('module', '/module');
  3546. $paths[] = $module;
  3547. if ($CFG->enableavailability) {
  3548. $paths[] = new restore_path_element('availability', '/module/availability_info/availability');
  3549. $paths[] = new restore_path_element('availability_field', '/module/availability_info/availability_field');
  3550. }
  3551. $paths[] = new restore_path_element('tag', '/module/tags/tag');
  3552. // Apply for 'format' plugins optional paths at module level
  3553. $this->add_plugin_structure('format', $module);
  3554. // Apply for 'report' plugins optional paths at module level.
  3555. $this->add_plugin_structure('report', $module);
  3556. // Apply for 'plagiarism' plugins optional paths at module level
  3557. $this->add_plugin_structure('plagiarism', $module);
  3558. // Apply for 'local' plugins optional paths at module level
  3559. $this->add_plugin_structure('local', $module);
  3560. // Apply for 'admin tool' plugins optional paths at module level.
  3561. $this->add_plugin_structure('tool', $module);
  3562. return $paths;
  3563. }
  3564. protected function process_module($data) {
  3565. global $CFG, $DB;
  3566. $data = (object)$data;
  3567. $oldid = $data->id;
  3568. $this->task->set_old_moduleversion($data->version);
  3569. $data->course = $this->task->get_courseid();
  3570. $data->module = $DB->get_field('modules', 'id', array('name' => $data->modulename));
  3571. // Map section (first try by course_section mapping match. Useful in course and section restores)
  3572. $data->section = $this->get_mappingid('course_section', $data->sectionid);
  3573. if (!$data->section) { // mapping failed, try to get section by sectionnumber matching
  3574. $params = array(
  3575. 'course' => $this->get_courseid(),
  3576. 'section' => $data->sectionnumber);
  3577. $data->section = $DB->get_field('course_sections', 'id', $params);
  3578. }
  3579. if (!$data->section) { // sectionnumber failed, try to get first section in course
  3580. $params = array(
  3581. 'course' => $this->get_courseid());
  3582. $data->section = $DB->get_field('course_sections', 'MIN(id)', $params);
  3583. }
  3584. if (!$data->section) { // no sections in course, create section 0 and 1 and assign module to 1
  3585. $sectionrec = array(
  3586. 'course' => $this->get_courseid(),
  3587. 'section' => 0,
  3588. 'timemodified' => time());
  3589. $DB->insert_record('course_sections', $sectionrec); // section 0
  3590. $sectionrec = array(
  3591. 'course' => $this->get_courseid(),
  3592. 'section' => 1,
  3593. 'timemodified' => time());
  3594. $data->section = $DB->insert_record('course_sections', $sectionrec); // section 1
  3595. }
  3596. $data->groupingid= $this->get_mappingid('grouping', $data->groupingid); // grouping
  3597. if (!grade_verify_idnumber($data->idnumber, $this->get_courseid())) { // idnumber uniqueness
  3598. $data->idnumber = '';
  3599. }
  3600. if (empty($CFG->enablecompletion)) { // completion
  3601. $data->completion = 0;
  3602. $data->completiongradeitemnumber = null;
  3603. $data->completionview = 0;
  3604. $data->completionexpected = 0;
  3605. } else {
  3606. $data->completionexpected = $this->apply_date_offset($data->completionexpected);
  3607. }
  3608. if (empty($CFG->enableavailability)) {
  3609. $data->availability = null;
  3610. }
  3611. // Backups that did not include showdescription, set it to default 0
  3612. // (this is not totally necessary as it has a db default, but just to
  3613. // be explicit).
  3614. if (!isset($data->showdescription)) {
  3615. $data->showdescription = 0;
  3616. }
  3617. $data->instance = 0; // Set to 0 for now, going to create it soon (next step)
  3618. if (empty($data->availability)) {
  3619. // If there are legacy availablility data fields (and no new format data),
  3620. // convert the old fields.
  3621. $data->availability = \core_availability\info::convert_legacy_fields(
  3622. $data, false);
  3623. } else if (!empty($data->groupmembersonly)) {
  3624. // There is current availability data, but it still has groupmembersonly
  3625. // as well (2.7 backups), convert just that part.
  3626. require_once($CFG->dirroot . '/lib/db/upgradelib.php');
  3627. $data->availability = upgrade_group_members_only($data->groupingid, $data->availability);
  3628. }
  3629. // course_module record ready, insert it
  3630. $newitemid = $DB->insert_record('course_modules', $data);
  3631. // save mapping
  3632. $this->set_mapping('course_module', $oldid, $newitemid);
  3633. // set the new course_module id in the task
  3634. $this->task->set_moduleid($newitemid);
  3635. // we can now create the context safely
  3636. $ctxid = context_module::instance($newitemid)->id;
  3637. // set the new context id in the task
  3638. $this->task->set_contextid($ctxid);
  3639. // update sequence field in course_section
  3640. if ($sequence = $DB->get_field('course_sections', 'sequence', array('id' => $data->section))) {
  3641. $sequence .= ',' . $newitemid;
  3642. } else {
  3643. $sequence = $newitemid;
  3644. }
  3645. $updatesection = new \stdClass();
  3646. $updatesection->id = $data->section;
  3647. $updatesection->sequence = $sequence;
  3648. $updatesection->timemodified = time();
  3649. $DB->update_record('course_sections', $updatesection);
  3650. // If there is the legacy showavailability data, store this for later use.
  3651. // (This data is not present when restoring 'new' backups.)
  3652. if (isset($data->showavailability)) {
  3653. // Cache the showavailability flag using the backup_ids data field.
  3654. restore_dbops::set_backup_ids_record($this->get_restoreid(),
  3655. 'module_showavailability', $newitemid, 0, null,
  3656. (object)array('showavailability' => $data->showavailability));
  3657. }
  3658. }
  3659. /**
  3660. * Fetch all the existing because tag_set() deletes them
  3661. * so everything must be reinserted on each call.
  3662. *
  3663. * @param stdClass $data Record data
  3664. */
  3665. protected function process_tag($data) {
  3666. global $CFG;
  3667. $data = (object)$data;
  3668. if (core_tag_tag::is_enabled('core', 'course_modules')) {
  3669. $modcontext = context::instance_by_id($this->task->get_contextid());
  3670. $instanceid = $this->task->get_moduleid();
  3671. core_tag_tag::add_item_tag('core', 'course_modules', $instanceid, $modcontext, $data->rawname);
  3672. }
  3673. }
  3674. /**
  3675. * Process the legacy availability table record. This table does not exist
  3676. * in Moodle 2.7+ but we still support restore.
  3677. *
  3678. * @param stdClass $data Record data
  3679. */
  3680. protected function process_availability($data) {
  3681. $data = (object)$data;
  3682. // Simply going to store the whole availability record now, we'll process
  3683. // all them later in the final task (once all activities have been restored)
  3684. // Let's call the low level one to be able to store the whole object
  3685. $data->coursemoduleid = $this->task->get_moduleid(); // Let add the availability cmid
  3686. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'module_availability', $data->id, 0, null, $data);
  3687. }
  3688. /**
  3689. * Process the legacy availability fields table record. This table does not
  3690. * exist in Moodle 2.7+ but we still support restore.
  3691. *
  3692. * @param stdClass $data Record data
  3693. */
  3694. protected function process_availability_field($data) {
  3695. global $DB;
  3696. $data = (object)$data;
  3697. // Mark it is as passed by default
  3698. $passed = true;
  3699. $customfieldid = null;
  3700. // If a customfield has been used in order to pass we must be able to match an existing
  3701. // customfield by name (data->customfield) and type (data->customfieldtype)
  3702. if (!empty($data->customfield) xor !empty($data->customfieldtype)) {
  3703. // xor is sort of uncommon. If either customfield is null or customfieldtype is null BUT not both.
  3704. // If one is null but the other isn't something clearly went wrong and we'll skip this condition.
  3705. $passed = false;
  3706. } else if (!empty($data->customfield)) {
  3707. $params = array('shortname' => $data->customfield, 'datatype' => $data->customfieldtype);
  3708. $customfieldid = $DB->get_field('user_info_field', 'id', $params);
  3709. $passed = ($customfieldid !== false);
  3710. }
  3711. if ($passed) {
  3712. // Create the object to insert into the database
  3713. $availfield = new stdClass();
  3714. $availfield->coursemoduleid = $this->task->get_moduleid(); // Lets add the availability cmid
  3715. $availfield->userfield = $data->userfield;
  3716. $availfield->customfieldid = $customfieldid;
  3717. $availfield->operator = $data->operator;
  3718. $availfield->value = $data->value;
  3719. // Get showavailability option.
  3720. $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
  3721. 'module_showavailability', $availfield->coursemoduleid);
  3722. if (!$showrec) {
  3723. // Should not happen.
  3724. throw new coding_exception('No matching showavailability record');
  3725. }
  3726. $show = $showrec->info->showavailability;
  3727. // The $availfieldobject is now in the format used in the old
  3728. // system. Interpret this and convert to new system.
  3729. $currentvalue = $DB->get_field('course_modules', 'availability',
  3730. array('id' => $availfield->coursemoduleid), MUST_EXIST);
  3731. $newvalue = \core_availability\info::add_legacy_availability_field_condition(
  3732. $currentvalue, $availfield, $show);
  3733. $DB->set_field('course_modules', 'availability', $newvalue,
  3734. array('id' => $availfield->coursemoduleid));
  3735. }
  3736. }
  3737. /**
  3738. * This method will be executed after the rest of the restore has been processed.
  3739. *
  3740. * Update old tag instance itemid(s).
  3741. */
  3742. protected function after_restore() {
  3743. global $DB;
  3744. $contextid = $this->task->get_contextid();
  3745. $instanceid = $this->task->get_activityid();
  3746. $olditemid = $this->task->get_old_activityid();
  3747. $DB->set_field('tag_instance', 'itemid', $instanceid, array('contextid' => $contextid, 'itemid' => $olditemid));
  3748. }
  3749. }
  3750. /**
  3751. * Structure step that will process the user activity completion
  3752. * information if all these conditions are met:
  3753. * - Target site has completion enabled ($CFG->enablecompletion)
  3754. * - Activity includes completion info (file_exists)
  3755. */
  3756. class restore_userscompletion_structure_step extends restore_structure_step {
  3757. /**
  3758. * To conditionally decide if this step must be executed
  3759. * Note the "settings" conditions are evaluated in the
  3760. * corresponding task. Here we check for other conditions
  3761. * not being restore settings (files, site settings...)
  3762. */
  3763. protected function execute_condition() {
  3764. global $CFG;
  3765. // Completion disabled in this site, don't execute
  3766. if (empty($CFG->enablecompletion)) {
  3767. return false;
  3768. }
  3769. // No completion on the front page.
  3770. if ($this->get_courseid() == SITEID) {
  3771. return false;
  3772. }
  3773. // No user completion info found, don't execute
  3774. $fullpath = $this->task->get_taskbasepath();
  3775. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  3776. if (!file_exists($fullpath)) {
  3777. return false;
  3778. }
  3779. // Arrived here, execute the step
  3780. return true;
  3781. }
  3782. protected function define_structure() {
  3783. $paths = array();
  3784. $paths[] = new restore_path_element('completion', '/completions/completion');
  3785. return $paths;
  3786. }
  3787. protected function process_completion($data) {
  3788. global $DB;
  3789. $data = (object)$data;
  3790. $data->coursemoduleid = $this->task->get_moduleid();
  3791. $data->userid = $this->get_mappingid('user', $data->userid);
  3792. // Find the existing record
  3793. $existing = $DB->get_record('course_modules_completion', array(
  3794. 'coursemoduleid' => $data->coursemoduleid,
  3795. 'userid' => $data->userid), 'id, timemodified');
  3796. // Check we didn't already insert one for this cmid and userid
  3797. // (there aren't supposed to be duplicates in that field, but
  3798. // it was possible until MDL-28021 was fixed).
  3799. if ($existing) {
  3800. // Update it to these new values, but only if the time is newer
  3801. if ($existing->timemodified < $data->timemodified) {
  3802. $data->id = $existing->id;
  3803. $DB->update_record('course_modules_completion', $data);
  3804. }
  3805. } else {
  3806. // Normal entry where it doesn't exist already
  3807. $DB->insert_record('course_modules_completion', $data);
  3808. }
  3809. }
  3810. }
  3811. /**
  3812. * Abstract structure step, parent of all the activity structure steps. Used to support
  3813. * the main <activity ...> tag and process it.
  3814. */
  3815. abstract class restore_activity_structure_step extends restore_structure_step {
  3816. /**
  3817. * Adds support for the 'activity' path that is common to all the activities
  3818. * and will be processed globally here
  3819. */
  3820. protected function prepare_activity_structure($paths) {
  3821. $paths[] = new restore_path_element('activity', '/activity');
  3822. return $paths;
  3823. }
  3824. /**
  3825. * Process the activity path, informing the task about various ids, needed later
  3826. */
  3827. protected function process_activity($data) {
  3828. $data = (object)$data;
  3829. $this->task->set_old_contextid($data->contextid); // Save old contextid in task
  3830. $this->set_mapping('context', $data->contextid, $this->task->get_contextid()); // Set the mapping
  3831. $this->task->set_old_activityid($data->id); // Save old activityid in task
  3832. }
  3833. /**
  3834. * This must be invoked immediately after creating the "module" activity record (forum, choice...)
  3835. * and will adjust the new activity id (the instance) in various places
  3836. */
  3837. protected function apply_activity_instance($newitemid) {
  3838. global $DB;
  3839. $this->task->set_activityid($newitemid); // Save activity id in task
  3840. // Apply the id to course_sections->instanceid
  3841. $DB->set_field('course_modules', 'instance', $newitemid, array('id' => $this->task->get_moduleid()));
  3842. // Do the mapping for modulename, preparing it for files by oldcontext
  3843. $modulename = $this->task->get_modulename();
  3844. $oldid = $this->task->get_old_activityid();
  3845. $this->set_mapping($modulename, $oldid, $newitemid, true);
  3846. }
  3847. }
  3848. /**
  3849. * Structure step in charge of creating/mapping all the qcats and qs
  3850. * by parsing the questions.xml file and checking it against the
  3851. * results calculated by {@link restore_process_categories_and_questions}
  3852. * and stored in backup_ids_temp
  3853. */
  3854. class restore_create_categories_and_questions extends restore_structure_step {
  3855. /** @var array $cachecategory store a question category */
  3856. protected $cachedcategory = null;
  3857. protected function define_structure() {
  3858. $category = new restore_path_element('question_category', '/question_categories/question_category');
  3859. $question = new restore_path_element('question', '/question_categories/question_category/questions/question');
  3860. $hint = new restore_path_element('question_hint',
  3861. '/question_categories/question_category/questions/question/question_hints/question_hint');
  3862. $tag = new restore_path_element('tag','/question_categories/question_category/questions/question/tags/tag');
  3863. // Apply for 'qtype' plugins optional paths at question level
  3864. $this->add_plugin_structure('qtype', $question);
  3865. // Apply for 'local' plugins optional paths at question level
  3866. $this->add_plugin_structure('local', $question);
  3867. return array($category, $question, $hint, $tag);
  3868. }
  3869. protected function process_question_category($data) {
  3870. global $DB;
  3871. $data = (object)$data;
  3872. $oldid = $data->id;
  3873. // Check we have one mapping for this category
  3874. if (!$mapping = $this->get_mapping('question_category', $oldid)) {
  3875. return self::SKIP_ALL_CHILDREN; // No mapping = this category doesn't need to be created/mapped
  3876. }
  3877. // Check we have to create the category (newitemid = 0)
  3878. if ($mapping->newitemid) {
  3879. // By performing this set_mapping() we make get_old/new_parentid() to work for all the
  3880. // children elements of the 'question_category' one.
  3881. $this->set_mapping('question_category', $oldid, $mapping->newitemid);
  3882. return; // newitemid != 0, this category is going to be mapped. Nothing to do
  3883. }
  3884. // Arrived here, newitemid = 0, we need to create the category
  3885. // we'll do it at parentitemid context, but for CONTEXT_MODULE
  3886. // categories, that will be created at CONTEXT_COURSE and moved
  3887. // to module context later when the activity is created
  3888. if ($mapping->info->contextlevel == CONTEXT_MODULE) {
  3889. $mapping->parentitemid = $this->get_mappingid('context', $this->task->get_old_contextid());
  3890. }
  3891. $data->contextid = $mapping->parentitemid;
  3892. // Before 3.5, question categories could be created at top level.
  3893. // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
  3894. $backuprelease = floatval($this->get_task()->get_info()->backup_release);
  3895. preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
  3896. $backupbuild = (int)$matches[1];
  3897. $before35 = false;
  3898. if ($backuprelease < 3.5 || $backupbuild < 20180205) {
  3899. $before35 = true;
  3900. }
  3901. if (empty($mapping->info->parent) && $before35) {
  3902. $top = question_get_top_category($data->contextid, true);
  3903. $data->parent = $top->id;
  3904. }
  3905. if (empty($data->parent)) {
  3906. if (!$top = question_get_top_category($data->contextid)) {
  3907. $top = question_get_top_category($data->contextid, true);
  3908. $this->set_mapping('question_category_created', $oldid, $top->id, false, null, $data->contextid);
  3909. }
  3910. $this->set_mapping('question_category', $oldid, $top->id);
  3911. } else {
  3912. // Before 3.1, the 'stamp' field could be erroneously duplicated.
  3913. // From 3.1 onwards, there's a unique index of (contextid, stamp).
  3914. // If we encounter a duplicate in an old restore file, just generate a new stamp.
  3915. // This is the same as what happens during an upgrade to 3.1+ anyway.
  3916. if ($DB->record_exists('question_categories', ['stamp' => $data->stamp, 'contextid' => $data->contextid])) {
  3917. $data->stamp = make_unique_id_code();
  3918. }
  3919. // The idnumber if it exists also needs to be unique within a context or reset it to null.
  3920. if (!empty($data->idnumber) && $DB->record_exists('question_categories',
  3921. ['idnumber' => $data->idnumber, 'contextid' => $data->contextid])) {
  3922. unset($data->idnumber);
  3923. }
  3924. // Let's create the question_category and save mapping.
  3925. $newitemid = $DB->insert_record('question_categories', $data);
  3926. $this->set_mapping('question_category', $oldid, $newitemid);
  3927. // Also annotate them as question_category_created, we need
  3928. // that later when remapping parents.
  3929. $this->set_mapping('question_category_created', $oldid, $newitemid, false, null, $data->contextid);
  3930. }
  3931. }
  3932. protected function process_question($data) {
  3933. global $DB;
  3934. $data = (object)$data;
  3935. $oldid = $data->id;
  3936. // Check we have one mapping for this question
  3937. if (!$questionmapping = $this->get_mapping('question', $oldid)) {
  3938. return; // No mapping = this question doesn't need to be created/mapped
  3939. }
  3940. // Get the mapped category (cannot use get_new_parentid() because not
  3941. // all the categories have been created, so it is not always available
  3942. // Instead we get the mapping for the question->parentitemid because
  3943. // we have loaded qcatids there for all parsed questions
  3944. $data->category = $this->get_mappingid('question_category', $questionmapping->parentitemid);
  3945. // In the past, there were some very sloppy values of penalty. Fix them.
  3946. if ($data->penalty >= 0.33 && $data->penalty <= 0.34) {
  3947. $data->penalty = 0.3333333;
  3948. }
  3949. if ($data->penalty >= 0.66 && $data->penalty <= 0.67) {
  3950. $data->penalty = 0.6666667;
  3951. }
  3952. if ($data->penalty >= 1) {
  3953. $data->penalty = 1;
  3954. }
  3955. $userid = $this->get_mappingid('user', $data->createdby);
  3956. $data->createdby = $userid ? $userid : $this->task->get_userid();
  3957. $userid = $this->get_mappingid('user', $data->modifiedby);
  3958. $data->modifiedby = $userid ? $userid : $this->task->get_userid();
  3959. // With newitemid = 0, let's create the question
  3960. if (!$questionmapping->newitemid) {
  3961. // The idnumber if it exists also needs to be unique within a category or reset it to null.
  3962. if (!empty($data->idnumber) && $DB->record_exists('question',
  3963. ['idnumber' => $data->idnumber, 'category' => $data->category])) {
  3964. unset($data->idnumber);
  3965. }
  3966. if ($data->qtype === 'random') {
  3967. // Ensure that this newly created question is considered by
  3968. // \qtype_random\task\remove_unused_questions.
  3969. $data->hidden = 0;
  3970. }
  3971. $newitemid = $DB->insert_record('question', $data);
  3972. $this->set_mapping('question', $oldid, $newitemid);
  3973. // Also annotate them as question_created, we need
  3974. // that later when remapping parents (keeping the old categoryid as parentid)
  3975. $this->set_mapping('question_created', $oldid, $newitemid, false, null, $questionmapping->parentitemid);
  3976. } else {
  3977. // By performing this set_mapping() we make get_old/new_parentid() to work for all the
  3978. // children elements of the 'question' one (so qtype plugins will know the question they belong to)
  3979. $this->set_mapping('question', $oldid, $questionmapping->newitemid);
  3980. }
  3981. // Note, we don't restore any question files yet
  3982. // as far as the CONTEXT_MODULE categories still
  3983. // haven't their contexts to be restored to
  3984. // The {@link restore_create_question_files}, executed in the final step
  3985. // step will be in charge of restoring all the question files
  3986. }
  3987. protected function process_question_hint($data) {
  3988. global $DB;
  3989. $data = (object)$data;
  3990. $oldid = $data->id;
  3991. // Detect if the question is created or mapped
  3992. $oldquestionid = $this->get_old_parentid('question');
  3993. $newquestionid = $this->get_new_parentid('question');
  3994. $questioncreated = $this->get_mappingid('question_created', $oldquestionid) ? true : false;
  3995. // If the question has been created by restore, we need to create its question_answers too
  3996. if ($questioncreated) {
  3997. // Adjust some columns
  3998. $data->questionid = $newquestionid;
  3999. // Insert record
  4000. $newitemid = $DB->insert_record('question_hints', $data);
  4001. // The question existed, we need to map the existing question_hints
  4002. } else {
  4003. // Look in question_hints by hint text matching
  4004. $sql = 'SELECT id
  4005. FROM {question_hints}
  4006. WHERE questionid = ?
  4007. AND ' . $DB->sql_compare_text('hint', 255) . ' = ' . $DB->sql_compare_text('?', 255);
  4008. $params = array($newquestionid, $data->hint);
  4009. $newitemid = $DB->get_field_sql($sql, $params);
  4010. // Not able to find the hint, let's try cleaning the hint text
  4011. // of all the question's hints in DB as slower fallback. MDL-33863.
  4012. if (!$newitemid) {
  4013. $potentialhints = $DB->get_records('question_hints',
  4014. array('questionid' => $newquestionid), '', 'id, hint');
  4015. foreach ($potentialhints as $potentialhint) {
  4016. // Clean in the same way than {@link xml_writer::xml_safe_utf8()}.
  4017. $cleanhint = preg_replace('/[\x-\x8\xb-\xc\xe-\x1f\x7f]/is','', $potentialhint->hint); // Clean CTRL chars.
  4018. $cleanhint = preg_replace("/\r\n|\r/", "\n", $cleanhint); // Normalize line ending.
  4019. if ($cleanhint === $data->hint) {
  4020. $newitemid = $data->id;
  4021. }
  4022. }
  4023. }
  4024. // If we haven't found the newitemid, something has gone really wrong, question in DB
  4025. // is missing hints, exception
  4026. if (!$newitemid) {
  4027. $info = new stdClass();
  4028. $info->filequestionid = $oldquestionid;
  4029. $info->dbquestionid = $newquestionid;
  4030. $info->hint = $data->hint;
  4031. throw new restore_step_exception('error_question_hint_missing_in_db', $info);
  4032. }
  4033. }
  4034. // Create mapping (I'm not sure if this is really needed?)
  4035. $this->set_mapping('question_hint', $oldid, $newitemid);
  4036. }
  4037. protected function process_tag($data) {
  4038. global $DB;
  4039. $data = (object)$data;
  4040. $newquestion = $this->get_new_parentid('question');
  4041. $questioncreated = (bool) $this->get_mappingid('question_created', $this->get_old_parentid('question'));
  4042. if (!$questioncreated) {
  4043. // This question already exists in the question bank. Nothing for us to do.
  4044. return;
  4045. }
  4046. if (core_tag_tag::is_enabled('core_question', 'question')) {
  4047. $tagname = $data->rawname;
  4048. if (!empty($data->contextid) && $newcontextid = $this->get_mappingid('context', $data->contextid)) {
  4049. $tagcontextid = $newcontextid;
  4050. } else {
  4051. // Get the category, so we can then later get the context.
  4052. $categoryid = $this->get_new_parentid('question_category');
  4053. if (empty($this->cachedcategory) || $this->cachedcategory->id != $categoryid) {
  4054. $this->cachedcategory = $DB->get_record('question_categories', array('id' => $categoryid));
  4055. }
  4056. $tagcontextid = $this->cachedcategory->contextid;
  4057. }
  4058. // Add the tag to the question.
  4059. core_tag_tag::add_item_tag('core_question', 'question', $newquestion,
  4060. context::instance_by_id($tagcontextid),
  4061. $tagname);
  4062. }
  4063. }
  4064. protected function after_execute() {
  4065. global $DB;
  4066. // First of all, recode all the created question_categories->parent fields
  4067. $qcats = $DB->get_records('backup_ids_temp', array(
  4068. 'backupid' => $this->get_restoreid(),
  4069. 'itemname' => 'question_category_created'));
  4070. foreach ($qcats as $qcat) {
  4071. $dbcat = $DB->get_record('question_categories', array('id' => $qcat->newitemid));
  4072. // Get new parent (mapped or created, so we look in quesiton_category mappings)
  4073. if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
  4074. 'backupid' => $this->get_restoreid(),
  4075. 'itemname' => 'question_category',
  4076. 'itemid' => $dbcat->parent))) {
  4077. // contextids must match always, as far as we always include complete qbanks, just check it
  4078. $newparentctxid = $DB->get_field('question_categories', 'contextid', array('id' => $newparent));
  4079. if ($dbcat->contextid == $newparentctxid) {
  4080. $DB->set_field('question_categories', 'parent', $newparent, array('id' => $dbcat->id));
  4081. } else {
  4082. $newparent = 0; // No ctx match for both cats, no parent relationship
  4083. }
  4084. }
  4085. // Here with $newparent empty, problem with contexts or remapping, set it to top cat
  4086. if (!$newparent && $dbcat->parent) {
  4087. $topcat = question_get_top_category($dbcat->contextid, true);
  4088. if ($dbcat->parent != $topcat->id) {
  4089. $DB->set_field('question_categories', 'parent', $topcat->id, array('id' => $dbcat->id));
  4090. }
  4091. }
  4092. }
  4093. // Now, recode all the created question->parent fields
  4094. $qs = $DB->get_records('backup_ids_temp', array(
  4095. 'backupid' => $this->get_restoreid(),
  4096. 'itemname' => 'question_created'));
  4097. foreach ($qs as $q) {
  4098. $dbq = $DB->get_record('question', array('id' => $q->newitemid));
  4099. // Get new parent (mapped or created, so we look in question mappings)
  4100. if ($newparent = $DB->get_field('backup_ids_temp', 'newitemid', array(
  4101. 'backupid' => $this->get_restoreid(),
  4102. 'itemname' => 'question',
  4103. 'itemid' => $dbq->parent))) {
  4104. $DB->set_field('question', 'parent', $newparent, array('id' => $dbq->id));
  4105. }
  4106. }
  4107. // Note, we don't restore any question files yet
  4108. // as far as the CONTEXT_MODULE categories still
  4109. // haven't their contexts to be restored to
  4110. // The {@link restore_create_question_files}, executed in the final step
  4111. // step will be in charge of restoring all the question files
  4112. }
  4113. }
  4114. /**
  4115. * Execution step that will move all the CONTEXT_MODULE question categories
  4116. * created at early stages of restore in course context (because modules weren't
  4117. * created yet) to their target module (matching by old-new-contextid mapping)
  4118. */
  4119. class restore_move_module_questions_categories extends restore_execution_step {
  4120. protected function define_execution() {
  4121. global $DB;
  4122. $backuprelease = floatval($this->task->get_info()->backup_release);
  4123. preg_match('/(\d{8})/', $this->task->get_info()->moodle_release, $matches);
  4124. $backupbuild = (int)$matches[1];
  4125. $after35 = false;
  4126. if ($backuprelease >= 3.5 && $backupbuild > 20180205) {
  4127. $after35 = true;
  4128. }
  4129. $contexts = restore_dbops::restore_get_question_banks($this->get_restoreid(), CONTEXT_MODULE);
  4130. foreach ($contexts as $contextid => $contextlevel) {
  4131. // Only if context mapping exists (i.e. the module has been restored)
  4132. if ($newcontext = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $contextid)) {
  4133. // Update all the qcats having their parentitemid set to the original contextid
  4134. $modulecats = $DB->get_records_sql("SELECT itemid, newitemid, info
  4135. FROM {backup_ids_temp}
  4136. WHERE backupid = ?
  4137. AND itemname = 'question_category'
  4138. AND parentitemid = ?", array($this->get_restoreid(), $contextid));
  4139. $top = question_get_top_category($newcontext->newitemid, true);
  4140. $oldtopid = 0;
  4141. foreach ($modulecats as $modulecat) {
  4142. // Before 3.5, question categories could be created at top level.
  4143. // From 3.5 onwards, all question categories should be a child of a special category called the "top" category.
  4144. $info = backup_controller_dbops::decode_backup_temp_info($modulecat->info);
  4145. if ($after35 && empty($info->parent)) {
  4146. $oldtopid = $modulecat->newitemid;
  4147. $modulecat->newitemid = $top->id;
  4148. } else {
  4149. $cat = new stdClass();
  4150. $cat->id = $modulecat->newitemid;
  4151. $cat->contextid = $newcontext->newitemid;
  4152. if (empty($info->parent)) {
  4153. $cat->parent = $top->id;
  4154. }
  4155. $DB->update_record('question_categories', $cat);
  4156. }
  4157. // And set new contextid (and maybe update newitemid) also in question_category mapping (will be
  4158. // used by {@link restore_create_question_files} later.
  4159. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'question_category', $modulecat->itemid,
  4160. $modulecat->newitemid, $newcontext->newitemid);
  4161. }
  4162. // Now set the parent id for the question categories that were in the top category in the course context
  4163. // and have been moved now.
  4164. if ($oldtopid) {
  4165. $DB->set_field('question_categories', 'parent', $top->id,
  4166. array('contextid' => $newcontext->newitemid, 'parent' => $oldtopid));
  4167. }
  4168. }
  4169. }
  4170. }
  4171. }
  4172. /**
  4173. * Execution step that will create all the question/answers/qtype-specific files for the restored
  4174. * questions. It must be executed after {@link restore_move_module_questions_categories}
  4175. * because only then each question is in its final category and only then the
  4176. * contexts can be determined.
  4177. */
  4178. class restore_create_question_files extends restore_execution_step {
  4179. /** @var array Question-type specific component items cache. */
  4180. private $qtypecomponentscache = array();
  4181. /**
  4182. * Preform the restore_create_question_files step.
  4183. */
  4184. protected function define_execution() {
  4185. global $DB;
  4186. // Track progress, as this task can take a long time.
  4187. $progress = $this->task->get_progress();
  4188. $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
  4189. // Parentitemids of question_createds in backup_ids_temp are the category it is in.
  4190. // MUST use a recordset, as there is no unique key in the first (or any) column.
  4191. $catqtypes = $DB->get_recordset_sql("SELECT DISTINCT bi.parentitemid AS categoryid, q.qtype as qtype
  4192. FROM {backup_ids_temp} bi
  4193. JOIN {question} q ON q.id = bi.newitemid
  4194. WHERE bi.backupid = ?
  4195. AND bi.itemname = 'question_created'
  4196. ORDER BY categoryid ASC", array($this->get_restoreid()));
  4197. $currentcatid = -1;
  4198. foreach ($catqtypes as $categoryid => $row) {
  4199. $qtype = $row->qtype;
  4200. // Check if we are in a new category.
  4201. if ($currentcatid !== $categoryid) {
  4202. // Report progress for each category.
  4203. $progress->progress();
  4204. if (!$qcatmapping = restore_dbops::get_backup_ids_record($this->get_restoreid(),
  4205. 'question_category', $categoryid)) {
  4206. // Something went really wrong, cannot find the question_category for the question_created records.
  4207. debugging('Error fetching target context for question', DEBUG_DEVELOPER);
  4208. continue;
  4209. }
  4210. // Calculate source and target contexts.
  4211. $oldctxid = $qcatmapping->info->contextid;
  4212. $newctxid = $qcatmapping->parentitemid;
  4213. $this->send_common_files($oldctxid, $newctxid, $progress);
  4214. $currentcatid = $categoryid;
  4215. }
  4216. $this->send_qtype_files($qtype, $oldctxid, $newctxid, $progress);
  4217. }
  4218. $catqtypes->close();
  4219. $progress->end_progress();
  4220. }
  4221. /**
  4222. * Send the common question files to a new context.
  4223. *
  4224. * @param int $oldctxid Old context id.
  4225. * @param int $newctxid New context id.
  4226. * @param \core\progress $progress Progress object to use.
  4227. */
  4228. private function send_common_files($oldctxid, $newctxid, $progress) {
  4229. // Add common question files (question and question_answer ones).
  4230. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'questiontext',
  4231. $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
  4232. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'generalfeedback',
  4233. $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
  4234. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answer',
  4235. $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
  4236. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'answerfeedback',
  4237. $oldctxid, $this->task->get_userid(), 'question_answer', null, $newctxid, true, $progress);
  4238. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'hint',
  4239. $oldctxid, $this->task->get_userid(), 'question_hint', null, $newctxid, true, $progress);
  4240. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'correctfeedback',
  4241. $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
  4242. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'partiallycorrectfeedback',
  4243. $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
  4244. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), 'question', 'incorrectfeedback',
  4245. $oldctxid, $this->task->get_userid(), 'question_created', null, $newctxid, true, $progress);
  4246. }
  4247. /**
  4248. * Send the question type specific files to a new context.
  4249. *
  4250. * @param text $qtype The qtype name to send.
  4251. * @param int $oldctxid Old context id.
  4252. * @param int $newctxid New context id.
  4253. * @param \core\progress $progress Progress object to use.
  4254. */
  4255. private function send_qtype_files($qtype, $oldctxid, $newctxid, $progress) {
  4256. if (!isset($this->qtypecomponentscache[$qtype])) {
  4257. $this->qtypecomponentscache[$qtype] = backup_qtype_plugin::get_components_and_fileareas($qtype);
  4258. }
  4259. $components = $this->qtypecomponentscache[$qtype];
  4260. foreach ($components as $component => $fileareas) {
  4261. foreach ($fileareas as $filearea => $mapping) {
  4262. restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component, $filearea,
  4263. $oldctxid, $this->task->get_userid(), $mapping, null, $newctxid, true, $progress);
  4264. }
  4265. }
  4266. }
  4267. }
  4268. /**
  4269. * Try to restore aliases and references to external files.
  4270. *
  4271. * The queue of these files was prepared for us in {@link restore_dbops::send_files_to_pool()}.
  4272. * We expect that all regular (non-alias) files have already been restored. Make sure
  4273. * there is no restore step executed after this one that would call send_files_to_pool() again.
  4274. *
  4275. * You may notice we have hardcoded support for Server files, Legacy course files
  4276. * and user Private files here at the moment. This could be eventually replaced with a set of
  4277. * callbacks in the future if needed.
  4278. *
  4279. * @copyright 2012 David Mudrak <david@moodle.com>
  4280. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4281. */
  4282. class restore_process_file_aliases_queue extends restore_execution_step {
  4283. /** @var array internal cache for {@link choose_repository()} */
  4284. private $cachereposbyid = array();
  4285. /** @var array internal cache for {@link choose_repository()} */
  4286. private $cachereposbytype = array();
  4287. /**
  4288. * What to do when this step is executed.
  4289. */
  4290. protected function define_execution() {
  4291. global $DB;
  4292. $this->log('processing file aliases queue', backup::LOG_DEBUG);
  4293. $fs = get_file_storage();
  4294. // Load the queue.
  4295. $rs = $DB->get_recordset('backup_ids_temp',
  4296. array('backupid' => $this->get_restoreid(), 'itemname' => 'file_aliases_queue'),
  4297. '', 'info');
  4298. // Iterate over aliases in the queue.
  4299. foreach ($rs as $record) {
  4300. $info = backup_controller_dbops::decode_backup_temp_info($record->info);
  4301. // Try to pick a repository instance that should serve the alias.
  4302. $repository = $this->choose_repository($info);
  4303. if (is_null($repository)) {
  4304. $this->notify_failure($info, 'unable to find a matching repository instance');
  4305. continue;
  4306. }
  4307. if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
  4308. // Aliases to Server files and Legacy course files may refer to a file
  4309. // contained in the backup file or to some existing file (if we are on the
  4310. // same site).
  4311. try {
  4312. $reference = file_storage::unpack_reference($info->oldfile->reference);
  4313. } catch (Exception $e) {
  4314. $this->notify_failure($info, 'invalid reference field format');
  4315. continue;
  4316. }
  4317. // Let's see if the referred source file was also included in the backup.
  4318. $candidates = $DB->get_recordset('backup_files_temp', array(
  4319. 'backupid' => $this->get_restoreid(),
  4320. 'contextid' => $reference['contextid'],
  4321. 'component' => $reference['component'],
  4322. 'filearea' => $reference['filearea'],
  4323. 'itemid' => $reference['itemid'],
  4324. ), '', 'info, newcontextid, newitemid');
  4325. $source = null;
  4326. foreach ($candidates as $candidate) {
  4327. $candidateinfo = backup_controller_dbops::decode_backup_temp_info($candidate->info);
  4328. if ($candidateinfo->filename === $reference['filename']
  4329. and $candidateinfo->filepath === $reference['filepath']
  4330. and !is_null($candidate->newcontextid)
  4331. and !is_null($candidate->newitemid) ) {
  4332. $source = $candidateinfo;
  4333. $source->contextid = $candidate->newcontextid;
  4334. $source->itemid = $candidate->newitemid;
  4335. break;
  4336. }
  4337. }
  4338. $candidates->close();
  4339. if ($source) {
  4340. // We have an alias that refers to another file also included in
  4341. // the backup. Let us change the reference field so that it refers
  4342. // to the restored copy of the original file.
  4343. $reference = file_storage::pack_reference($source);
  4344. // Send the new alias to the filepool.
  4345. $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
  4346. $this->notify_success($info);
  4347. continue;
  4348. } else {
  4349. // This is a reference to some moodle file that was not contained in the backup
  4350. // file. If we are restoring to the same site, keep the reference untouched
  4351. // and restore the alias as is if the referenced file exists.
  4352. if ($this->task->is_samesite()) {
  4353. if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
  4354. $reference['itemid'], $reference['filepath'], $reference['filename'])) {
  4355. $reference = file_storage::pack_reference($reference);
  4356. $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
  4357. $this->notify_success($info);
  4358. continue;
  4359. } else {
  4360. $this->notify_failure($info, 'referenced file not found');
  4361. continue;
  4362. }
  4363. // If we are at other site, we can't restore this alias.
  4364. } else {
  4365. $this->notify_failure($info, 'referenced file not included');
  4366. continue;
  4367. }
  4368. }
  4369. } else if ($info->oldfile->repositorytype === 'user') {
  4370. if ($this->task->is_samesite()) {
  4371. // For aliases to user Private files at the same site, we have a chance to check
  4372. // if the referenced file still exists.
  4373. try {
  4374. $reference = file_storage::unpack_reference($info->oldfile->reference);
  4375. } catch (Exception $e) {
  4376. $this->notify_failure($info, 'invalid reference field format');
  4377. continue;
  4378. }
  4379. if ($fs->file_exists($reference['contextid'], $reference['component'], $reference['filearea'],
  4380. $reference['itemid'], $reference['filepath'], $reference['filename'])) {
  4381. $reference = file_storage::pack_reference($reference);
  4382. $fs->create_file_from_reference($info->newfile, $repository->id, $reference);
  4383. $this->notify_success($info);
  4384. continue;
  4385. } else {
  4386. $this->notify_failure($info, 'referenced file not found');
  4387. continue;
  4388. }
  4389. // If we are at other site, we can't restore this alias.
  4390. } else {
  4391. $this->notify_failure($info, 'restoring at another site');
  4392. continue;
  4393. }
  4394. } else {
  4395. // This is a reference to some external file such as in boxnet or dropbox.
  4396. // If we are restoring to the same site, keep the reference untouched and
  4397. // restore the alias as is.
  4398. if ($this->task->is_samesite()) {
  4399. $fs->create_file_from_reference($info->newfile, $repository->id, $info->oldfile->reference);
  4400. $this->notify_success($info);
  4401. continue;
  4402. // If we are at other site, we can't restore this alias.
  4403. } else {
  4404. $this->notify_failure($info, 'restoring at another site');
  4405. continue;
  4406. }
  4407. }
  4408. }
  4409. $rs->close();
  4410. }
  4411. /**
  4412. * Choose the repository instance that should handle the alias.
  4413. *
  4414. * At the same site, we can rely on repository instance id and we just
  4415. * check it still exists. On other site, try to find matching Server files or
  4416. * Legacy course files repository instance. Return null if no matching
  4417. * repository instance can be found.
  4418. *
  4419. * @param stdClass $info
  4420. * @return repository|null
  4421. */
  4422. private function choose_repository(stdClass $info) {
  4423. global $DB, $CFG;
  4424. require_once($CFG->dirroot.'/repository/lib.php');
  4425. if ($this->task->is_samesite()) {
  4426. // We can rely on repository instance id.
  4427. if (array_key_exists($info->oldfile->repositoryid, $this->cachereposbyid)) {
  4428. return $this->cachereposbyid[$info->oldfile->repositoryid];
  4429. }
  4430. $this->log('looking for repository instance by id', backup::LOG_DEBUG, $info->oldfile->repositoryid, 1);
  4431. try {
  4432. $this->cachereposbyid[$info->oldfile->repositoryid] = repository::get_repository_by_id($info->oldfile->repositoryid, SYSCONTEXTID);
  4433. return $this->cachereposbyid[$info->oldfile->repositoryid];
  4434. } catch (Exception $e) {
  4435. $this->cachereposbyid[$info->oldfile->repositoryid] = null;
  4436. return null;
  4437. }
  4438. } else {
  4439. // We can rely on repository type only.
  4440. if (empty($info->oldfile->repositorytype)) {
  4441. return null;
  4442. }
  4443. if (array_key_exists($info->oldfile->repositorytype, $this->cachereposbytype)) {
  4444. return $this->cachereposbytype[$info->oldfile->repositorytype];
  4445. }
  4446. $this->log('looking for repository instance by type', backup::LOG_DEBUG, $info->oldfile->repositorytype, 1);
  4447. // Both Server files and Legacy course files repositories have a single
  4448. // instance at the system context to use. Let us try to find it.
  4449. if ($info->oldfile->repositorytype === 'local' or $info->oldfile->repositorytype === 'coursefiles') {
  4450. $sql = "SELECT ri.id
  4451. FROM {repository} r
  4452. JOIN {repository_instances} ri ON ri.typeid = r.id
  4453. WHERE r.type = ? AND ri.contextid = ?";
  4454. $ris = $DB->get_records_sql($sql, array($info->oldfile->repositorytype, SYSCONTEXTID));
  4455. if (empty($ris)) {
  4456. return null;
  4457. }
  4458. $repoids = array_keys($ris);
  4459. $repoid = reset($repoids);
  4460. try {
  4461. $this->cachereposbytype[$info->oldfile->repositorytype] = repository::get_repository_by_id($repoid, SYSCONTEXTID);
  4462. return $this->cachereposbytype[$info->oldfile->repositorytype];
  4463. } catch (Exception $e) {
  4464. $this->cachereposbytype[$info->oldfile->repositorytype] = null;
  4465. return null;
  4466. }
  4467. }
  4468. $this->cachereposbytype[$info->oldfile->repositorytype] = null;
  4469. return null;
  4470. }
  4471. }
  4472. /**
  4473. * Let the user know that the given alias was successfully restored
  4474. *
  4475. * @param stdClass $info
  4476. */
  4477. private function notify_success(stdClass $info) {
  4478. $filedesc = $this->describe_alias($info);
  4479. $this->log('successfully restored alias', backup::LOG_DEBUG, $filedesc, 1);
  4480. }
  4481. /**
  4482. * Let the user know that the given alias can't be restored
  4483. *
  4484. * @param stdClass $info
  4485. * @param string $reason detailed reason to be logged
  4486. */
  4487. private function notify_failure(stdClass $info, $reason = '') {
  4488. $filedesc = $this->describe_alias($info);
  4489. if ($reason) {
  4490. $reason = ' ('.$reason.')';
  4491. }
  4492. $this->log('unable to restore alias'.$reason, backup::LOG_WARNING, $filedesc, 1);
  4493. $this->add_result_item('file_aliases_restore_failures', $filedesc);
  4494. }
  4495. /**
  4496. * Return a human readable description of the alias file
  4497. *
  4498. * @param stdClass $info
  4499. * @return string
  4500. */
  4501. private function describe_alias(stdClass $info) {
  4502. $filedesc = $this->expected_alias_location($info->newfile);
  4503. if (!is_null($info->oldfile->source)) {
  4504. $filedesc .= ' ('.$info->oldfile->source.')';
  4505. }
  4506. return $filedesc;
  4507. }
  4508. /**
  4509. * Return the expected location of a file
  4510. *
  4511. * Please note this may and may not work as a part of URL to pluginfile.php
  4512. * (depends on how the given component/filearea deals with the itemid).
  4513. *
  4514. * @param stdClass $filerecord
  4515. * @return string
  4516. */
  4517. private function expected_alias_location($filerecord) {
  4518. $filedesc = '/'.$filerecord->contextid.'/'.$filerecord->component.'/'.$filerecord->filearea;
  4519. if (!is_null($filerecord->itemid)) {
  4520. $filedesc .= '/'.$filerecord->itemid;
  4521. }
  4522. $filedesc .= $filerecord->filepath.$filerecord->filename;
  4523. return $filedesc;
  4524. }
  4525. /**
  4526. * Append a value to the given resultset
  4527. *
  4528. * @param string $name name of the result containing a list of values
  4529. * @param mixed $value value to add as another item in that result
  4530. */
  4531. private function add_result_item($name, $value) {
  4532. $results = $this->task->get_results();
  4533. if (isset($results[$name])) {
  4534. if (!is_array($results[$name])) {
  4535. throw new coding_exception('Unable to append a result item into a non-array structure.');
  4536. }
  4537. $current = $results[$name];
  4538. $current[] = $value;
  4539. $this->task->add_result(array($name => $current));
  4540. } else {
  4541. $this->task->add_result(array($name => array($value)));
  4542. }
  4543. }
  4544. }
  4545. /**
  4546. * Helper code for use by any plugin that stores question attempt data that it needs to back up.
  4547. */
  4548. trait restore_questions_attempt_data_trait {
  4549. /** @var array question_attempt->id to qtype. */
  4550. protected $qtypes = array();
  4551. /** @var array question_attempt->id to questionid. */
  4552. protected $newquestionids = array();
  4553. /**
  4554. * Attach below $element (usually attempts) the needed restore_path_elements
  4555. * to restore question_usages and all they contain.
  4556. *
  4557. * If you use the $nameprefix parameter, then you will need to implement some
  4558. * extra methods in your class, like
  4559. *
  4560. * protected function process_{nameprefix}question_attempt($data) {
  4561. * $this->restore_question_usage_worker($data, '{nameprefix}');
  4562. * }
  4563. * protected function process_{nameprefix}question_attempt($data) {
  4564. * $this->restore_question_attempt_worker($data, '{nameprefix}');
  4565. * }
  4566. * protected function process_{nameprefix}question_attempt_step($data) {
  4567. * $this->restore_question_attempt_step_worker($data, '{nameprefix}');
  4568. * }
  4569. *
  4570. * @param restore_path_element $element the parent element that the usages are stored inside.
  4571. * @param array $paths the paths array that is being built.
  4572. * @param string $nameprefix should match the prefix passed to the corresponding
  4573. * backup_questions_activity_structure_step::add_question_usages call.
  4574. */
  4575. protected function add_question_usages($element, &$paths, $nameprefix = '') {
  4576. // Check $element is restore_path_element
  4577. if (! $element instanceof restore_path_element) {
  4578. throw new restore_step_exception('element_must_be_restore_path_element', $element);
  4579. }
  4580. // Check $paths is one array
  4581. if (!is_array($paths)) {
  4582. throw new restore_step_exception('paths_must_be_array', $paths);
  4583. }
  4584. $paths[] = new restore_path_element($nameprefix . 'question_usage',
  4585. $element->get_path() . "/{$nameprefix}question_usage");
  4586. $paths[] = new restore_path_element($nameprefix . 'question_attempt',
  4587. $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt");
  4588. $paths[] = new restore_path_element($nameprefix . 'question_attempt_step',
  4589. $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step",
  4590. true);
  4591. $paths[] = new restore_path_element($nameprefix . 'question_attempt_step_data',
  4592. $element->get_path() . "/{$nameprefix}question_usage/{$nameprefix}question_attempts/{$nameprefix}question_attempt/{$nameprefix}steps/{$nameprefix}step/{$nameprefix}response/{$nameprefix}variable");
  4593. }
  4594. /**
  4595. * Process question_usages
  4596. */
  4597. public function process_question_usage($data) {
  4598. $this->restore_question_usage_worker($data, '');
  4599. }
  4600. /**
  4601. * Process question_attempts
  4602. */
  4603. public function process_question_attempt($data) {
  4604. $this->restore_question_attempt_worker($data, '');
  4605. }
  4606. /**
  4607. * Process question_attempt_steps
  4608. */
  4609. public function process_question_attempt_step($data) {
  4610. $this->restore_question_attempt_step_worker($data, '');
  4611. }
  4612. /**
  4613. * This method does the actual work for process_question_usage or
  4614. * process_{nameprefix}_question_usage.
  4615. * @param array $data the data from the XML file.
  4616. * @param string $nameprefix the element name prefix.
  4617. */
  4618. protected function restore_question_usage_worker($data, $nameprefix) {
  4619. global $DB;
  4620. // Clear our caches.
  4621. $this->qtypes = array();
  4622. $this->newquestionids = array();
  4623. $data = (object)$data;
  4624. $oldid = $data->id;
  4625. $data->contextid = $this->task->get_contextid();
  4626. // Everything ready, insert (no mapping needed)
  4627. $newitemid = $DB->insert_record('question_usages', $data);
  4628. $this->inform_new_usage_id($newitemid);
  4629. $this->set_mapping($nameprefix . 'question_usage', $oldid, $newitemid, false);
  4630. }
  4631. /**
  4632. * When process_question_usage creates the new usage, it calls this method
  4633. * to let the activity link to the new usage. For example, the quiz uses
  4634. * this method to set quiz_attempts.uniqueid to the new usage id.
  4635. * @param integer $newusageid
  4636. */
  4637. abstract protected function inform_new_usage_id($newusageid);
  4638. /**
  4639. * This method does the actual work for process_question_attempt or
  4640. * process_{nameprefix}_question_attempt.
  4641. * @param array $data the data from the XML file.
  4642. * @param string $nameprefix the element name prefix.
  4643. */
  4644. protected function restore_question_attempt_worker($data, $nameprefix) {
  4645. global $DB;
  4646. $data = (object)$data;
  4647. $oldid = $data->id;
  4648. $question = $this->get_mapping('question', $data->questionid);
  4649. $data->questionusageid = $this->get_new_parentid($nameprefix . 'question_usage');
  4650. $data->questionid = $question->newitemid;
  4651. if (!property_exists($data, 'variant')) {
  4652. $data->variant = 1;
  4653. }
  4654. if (!property_exists($data, 'maxfraction')) {
  4655. $data->maxfraction = 1;
  4656. }
  4657. $newitemid = $DB->insert_record('question_attempts', $data);
  4658. $this->set_mapping($nameprefix . 'question_attempt', $oldid, $newitemid);
  4659. $this->qtypes[$newitemid] = $question->info->qtype;
  4660. $this->newquestionids[$newitemid] = $data->questionid;
  4661. }
  4662. /**
  4663. * This method does the actual work for process_question_attempt_step or
  4664. * process_{nameprefix}_question_attempt_step.
  4665. * @param array $data the data from the XML file.
  4666. * @param string $nameprefix the element name prefix.
  4667. */
  4668. protected function restore_question_attempt_step_worker($data, $nameprefix) {
  4669. global $DB;
  4670. $data = (object)$data;
  4671. $oldid = $data->id;
  4672. // Pull out the response data.
  4673. $response = array();
  4674. if (!empty($data->{$nameprefix . 'response'}[$nameprefix . 'variable'])) {
  4675. foreach ($data->{$nameprefix . 'response'}[$nameprefix . 'variable'] as $variable) {
  4676. $response[$variable['name']] = $variable['value'];
  4677. }
  4678. }
  4679. unset($data->response);
  4680. $data->questionattemptid = $this->get_new_parentid($nameprefix . 'question_attempt');
  4681. $data->userid = $this->get_mappingid('user', $data->userid);
  4682. // Everything ready, insert and create mapping (needed by question_sessions)
  4683. $newitemid = $DB->insert_record('question_attempt_steps', $data);
  4684. $this->set_mapping('question_attempt_step', $oldid, $newitemid, true);
  4685. // Now process the response data.
  4686. $response = $this->questions_recode_response_data(
  4687. $this->qtypes[$data->questionattemptid],
  4688. $this->newquestionids[$data->questionattemptid],
  4689. $data->sequencenumber, $response);
  4690. foreach ($response as $name => $value) {
  4691. $row = new stdClass();
  4692. $row->attemptstepid = $newitemid;
  4693. $row->name = $name;
  4694. $row->value = $value;
  4695. $DB->insert_record('question_attempt_step_data', $row, false);
  4696. }
  4697. }
  4698. /**
  4699. * Recode the respones data for a particular step of an attempt at at particular question.
  4700. * @param string $qtype the question type.
  4701. * @param int $newquestionid the question id.
  4702. * @param int $sequencenumber the sequence number.
  4703. * @param array $response the response data to recode.
  4704. */
  4705. public function questions_recode_response_data(
  4706. $qtype, $newquestionid, $sequencenumber, array $response) {
  4707. $qtyperestorer = $this->get_qtype_restorer($qtype);
  4708. if ($qtyperestorer) {
  4709. $response = $qtyperestorer->recode_response($newquestionid, $sequencenumber, $response);
  4710. }
  4711. return $response;
  4712. }
  4713. /**
  4714. * Given a list of question->ids, separated by commas, returns the
  4715. * recoded list, with all the restore question mappings applied.
  4716. * Note: Used by quiz->questions and quiz_attempts->layout
  4717. * Note: 0 = page break (unconverted)
  4718. */
  4719. protected function questions_recode_layout($layout) {
  4720. // Extracts question id from sequence
  4721. if ($questionids = explode(',', $layout)) {
  4722. foreach ($questionids as $id => $questionid) {
  4723. if ($questionid) { // If it is zero then this is a pagebreak, don't translate
  4724. $newquestionid = $this->get_mappingid('question', $questionid);
  4725. $questionids[$id] = $newquestionid;
  4726. }
  4727. }
  4728. }
  4729. return implode(',', $questionids);
  4730. }
  4731. /**
  4732. * Get the restore_qtype_plugin subclass for a specific question type.
  4733. * @param string $qtype e.g. multichoice.
  4734. * @return restore_qtype_plugin instance.
  4735. */
  4736. protected function get_qtype_restorer($qtype) {
  4737. // Build one static cache to store {@link restore_qtype_plugin}
  4738. // while we are needing them, just to save zillions of instantiations
  4739. // or using static stuff that will break our nice API
  4740. static $qtypeplugins = array();
  4741. if (!isset($qtypeplugins[$qtype])) {
  4742. $classname = 'restore_qtype_' . $qtype . '_plugin';
  4743. if (class_exists($classname)) {
  4744. $qtypeplugins[$qtype] = new $classname('qtype', $qtype, $this);
  4745. } else {
  4746. $qtypeplugins[$qtype] = null;
  4747. }
  4748. }
  4749. return $qtypeplugins[$qtype];
  4750. }
  4751. protected function after_execute() {
  4752. parent::after_execute();
  4753. // Restore any files belonging to responses.
  4754. foreach (question_engine::get_all_response_file_areas() as $filearea) {
  4755. $this->add_related_files('question', $filearea, 'question_attempt_step');
  4756. }
  4757. }
  4758. }
  4759. /**
  4760. * Abstract structure step to help activities that store question attempt data.
  4761. *
  4762. * @copyright 2011 The Open University
  4763. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4764. */
  4765. abstract class restore_questions_activity_structure_step extends restore_activity_structure_step {
  4766. use restore_questions_attempt_data_trait;
  4767. /**
  4768. * Attach below $element (usually attempts) the needed restore_path_elements
  4769. * to restore question attempt data from Moodle 2.0.
  4770. *
  4771. * When using this method, the parent element ($element) must be defined with
  4772. * $grouped = true. Then, in that elements process method, you must call
  4773. * {@link process_legacy_attempt_data()} with the groupded data. See, for
  4774. * example, the usage of this method in {@link restore_quiz_activity_structure_step}.
  4775. * @param restore_path_element $element the parent element. (E.g. a quiz attempt.)
  4776. * @param array $paths the paths array that is being built to describe the
  4777. * structure.
  4778. */
  4779. protected function add_legacy_question_attempt_data($element, &$paths) {
  4780. global $CFG;
  4781. require_once($CFG->dirroot . '/question/engine/upgrade/upgradelib.php');
  4782. // Check $element is restore_path_element
  4783. if (!($element instanceof restore_path_element)) {
  4784. throw new restore_step_exception('element_must_be_restore_path_element', $element);
  4785. }
  4786. // Check $paths is one array
  4787. if (!is_array($paths)) {
  4788. throw new restore_step_exception('paths_must_be_array', $paths);
  4789. }
  4790. $paths[] = new restore_path_element('question_state',
  4791. $element->get_path() . '/states/state');
  4792. $paths[] = new restore_path_element('question_session',
  4793. $element->get_path() . '/sessions/session');
  4794. }
  4795. protected function get_attempt_upgrader() {
  4796. if (empty($this->attemptupgrader)) {
  4797. $this->attemptupgrader = new question_engine_attempt_upgrader();
  4798. $this->attemptupgrader->prepare_to_restore();
  4799. }
  4800. return $this->attemptupgrader;
  4801. }
  4802. /**
  4803. * Process the attempt data defined by {@link add_legacy_question_attempt_data()}.
  4804. * @param object $data contains all the grouped attempt data to process.
  4805. * @param object $quiz data about the activity the attempts belong to. Required
  4806. * fields are (basically this only works for the quiz module):
  4807. * oldquestions => list of question ids in this activity - using old ids.
  4808. * preferredbehaviour => the behaviour to use for questionattempts.
  4809. */
  4810. protected function process_legacy_quiz_attempt_data($data, $quiz) {
  4811. global $DB;
  4812. $upgrader = $this->get_attempt_upgrader();
  4813. $data = (object)$data;
  4814. $layout = explode(',', $data->layout);
  4815. $newlayout = $layout;
  4816. // Convert each old question_session into a question_attempt.
  4817. $qas = array();
  4818. foreach (explode(',', $quiz->oldquestions) as $questionid) {
  4819. if ($questionid == 0) {
  4820. continue;
  4821. }
  4822. $newquestionid = $this->get_mappingid('question', $questionid);
  4823. if (!$newquestionid) {
  4824. throw new restore_step_exception('questionattemptreferstomissingquestion',
  4825. $questionid, $questionid);
  4826. }
  4827. $question = $upgrader->load_question($newquestionid, $quiz->id);
  4828. foreach ($layout as $key => $qid) {
  4829. if ($qid == $questionid) {
  4830. $newlayout[$key] = $newquestionid;
  4831. }
  4832. }
  4833. list($qsession, $qstates) = $this->find_question_session_and_states(
  4834. $data, $questionid);
  4835. if (empty($qsession) || empty($qstates)) {
  4836. throw new restore_step_exception('questionattemptdatamissing',
  4837. $questionid, $questionid);
  4838. }
  4839. list($qsession, $qstates) = $this->recode_legacy_response_data(
  4840. $question, $qsession, $qstates);
  4841. $data->layout = implode(',', $newlayout);
  4842. $qas[$newquestionid] = $upgrader->convert_question_attempt(
  4843. $quiz, $data, $question, $qsession, $qstates);
  4844. }
  4845. // Now create a new question_usage.
  4846. $usage = new stdClass();
  4847. $usage->component = 'mod_quiz';
  4848. $usage->contextid = $this->get_mappingid('context', $this->task->get_old_contextid());
  4849. $usage->preferredbehaviour = $quiz->preferredbehaviour;
  4850. $usage->id = $DB->insert_record('question_usages', $usage);
  4851. $this->inform_new_usage_id($usage->id);
  4852. $data->uniqueid = $usage->id;
  4853. $upgrader->save_usage($quiz->preferredbehaviour, $data, $qas,
  4854. $this->questions_recode_layout($quiz->oldquestions));
  4855. }
  4856. protected function find_question_session_and_states($data, $questionid) {
  4857. $qsession = null;
  4858. foreach ($data->sessions['session'] as $session) {
  4859. if ($session['questionid'] == $questionid) {
  4860. $qsession = (object) $session;
  4861. break;
  4862. }
  4863. }
  4864. $qstates = array();
  4865. foreach ($data->states['state'] as $state) {
  4866. if ($state['question'] == $questionid) {
  4867. // It would be natural to use $state['seq_number'] as the array-key
  4868. // here, but it seems that buggy behaviour in 2.0 and early can
  4869. // mean that that is not unique, so we use id, which is guaranteed
  4870. // to be unique.
  4871. $qstates[$state['id']] = (object) $state;
  4872. }
  4873. }
  4874. ksort($qstates);
  4875. $qstates = array_values($qstates);
  4876. return array($qsession, $qstates);
  4877. }
  4878. /**
  4879. * Recode any ids in the response data
  4880. * @param object $question the question data
  4881. * @param object $qsession the question sessions.
  4882. * @param array $qstates the question states.
  4883. */
  4884. protected function recode_legacy_response_data($question, $qsession, $qstates) {
  4885. $qsession->questionid = $question->id;
  4886. foreach ($qstates as &$state) {
  4887. $state->question = $question->id;
  4888. $state->answer = $this->restore_recode_legacy_answer($state, $question->qtype);
  4889. }
  4890. return array($qsession, $qstates);
  4891. }
  4892. /**
  4893. * Recode the legacy answer field.
  4894. * @param object $state the state to recode the answer of.
  4895. * @param string $qtype the question type.
  4896. */
  4897. public function restore_recode_legacy_answer($state, $qtype) {
  4898. $restorer = $this->get_qtype_restorer($qtype);
  4899. if ($restorer) {
  4900. return $restorer->recode_legacy_state_answer($state);
  4901. } else {
  4902. return $state->answer;
  4903. }
  4904. }
  4905. }
  4906. /**
  4907. * Restore completion defaults for each module type
  4908. *
  4909. * @package core_backup
  4910. * @copyright 2017 Marina Glancy
  4911. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4912. */
  4913. class restore_completion_defaults_structure_step extends restore_structure_step {
  4914. /**
  4915. * To conditionally decide if this step must be executed.
  4916. */
  4917. protected function execute_condition() {
  4918. // No completion on the front page.
  4919. if ($this->get_courseid() == SITEID) {
  4920. return false;
  4921. }
  4922. // No default completion info found, don't execute.
  4923. $fullpath = $this->task->get_taskbasepath();
  4924. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  4925. if (!file_exists($fullpath)) {
  4926. return false;
  4927. }
  4928. // Arrived here, execute the step.
  4929. return true;
  4930. }
  4931. /**
  4932. * Function that will return the structure to be processed by this restore_step.
  4933. *
  4934. * @return restore_path_element[]
  4935. */
  4936. protected function define_structure() {
  4937. return [new restore_path_element('completion_defaults', '/course_completion_defaults/course_completion_default')];
  4938. }
  4939. /**
  4940. * Processor for path element 'completion_defaults'
  4941. *
  4942. * @param stdClass|array $data
  4943. */
  4944. protected function process_completion_defaults($data) {
  4945. global $DB;
  4946. $data = (array)$data;
  4947. $oldid = $data['id'];
  4948. unset($data['id']);
  4949. // Find the module by name since id may be different in another site.
  4950. if (!$mod = $DB->get_record('modules', ['name' => $data['modulename']])) {
  4951. return;
  4952. }
  4953. unset($data['modulename']);
  4954. // Find the existing record.
  4955. $newid = $DB->get_field('course_completion_defaults', 'id',
  4956. ['course' => $this->task->get_courseid(), 'module' => $mod->id]);
  4957. if (!$newid) {
  4958. $newid = $DB->insert_record('course_completion_defaults',
  4959. ['course' => $this->task->get_courseid(), 'module' => $mod->id] + $data);
  4960. } else {
  4961. $DB->update_record('course_completion_defaults', ['id' => $newid] + $data);
  4962. }
  4963. // Save id mapping for restoring associated events.
  4964. $this->set_mapping('course_completion_defaults', $oldid, $newid);
  4965. }
  4966. }
  4967. /**
  4968. * Index course after restore.
  4969. *
  4970. * @package core_backup
  4971. * @copyright 2017 The Open University
  4972. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4973. */
  4974. class restore_course_search_index extends restore_execution_step {
  4975. /**
  4976. * When this step is executed, we add the course context to the queue for reindexing.
  4977. */
  4978. protected function define_execution() {
  4979. $context = \context_course::instance($this->task->get_courseid());
  4980. \core_search\manager::request_index($context);
  4981. }
  4982. }
  4983. /**
  4984. * Index activity after restore (when not restoring whole course).
  4985. *
  4986. * @package core_backup
  4987. * @copyright 2017 The Open University
  4988. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  4989. */
  4990. class restore_activity_search_index extends restore_execution_step {
  4991. /**
  4992. * When this step is executed, we add the activity context to the queue for reindexing.
  4993. */
  4994. protected function define_execution() {
  4995. $context = \context::instance_by_id($this->task->get_contextid());
  4996. \core_search\manager::request_index($context);
  4997. }
  4998. }
  4999. /**
  5000. * Index block after restore (when not restoring whole course).
  5001. *
  5002. * @package core_backup
  5003. * @copyright 2017 The Open University
  5004. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5005. */
  5006. class restore_block_search_index extends restore_execution_step {
  5007. /**
  5008. * When this step is executed, we add the block context to the queue for reindexing.
  5009. */
  5010. protected function define_execution() {
  5011. // A block in the restore list may be skipped because a duplicate is detected.
  5012. // In this case, there is no new blockid (or context) to get.
  5013. if (!empty($this->task->get_blockid())) {
  5014. $context = \context_block::instance($this->task->get_blockid());
  5015. \core_search\manager::request_index($context);
  5016. }
  5017. }
  5018. }
  5019. /**
  5020. * Restore action events.
  5021. *
  5022. * @package core_backup
  5023. * @copyright 2017 onwards Ankit Agarwal
  5024. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  5025. */
  5026. class restore_calendar_action_events extends restore_execution_step {
  5027. /**
  5028. * What to do when this step is executed.
  5029. */
  5030. protected function define_execution() {
  5031. // We just queue the task here rather trying to recreate everything manually.
  5032. // The task will automatically populate all data.
  5033. $task = new \core\task\refresh_mod_calendar_events_task();
  5034. $task->set_custom_data(array('courseid' => $this->get_courseid()));
  5035. \core\task\manager::queue_adhoc_task($task, true);
  5036. }
  5037. }