PageRenderTime 91ms CodeModel.GetById 18ms RepoModel.GetById 0ms app.codeStats 1ms

/backup/moodle2/restore_stepslib.php

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