PageRenderTime 65ms CodeModel.GetById 22ms RepoModel.GetById 0ms app.codeStats 3ms

/backup/moodle2/restore_stepslib.php

http://github.com/moodle/moodle
PHP | 5893 lines | 3475 code | 856 blank | 1562 comment | 630 complexity | 4b8b9201b2a1e6ecb8e1ac9d58b9d40c MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause

Large files files are truncated, but you can click here to view the full file

  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * Defines various restore steps that will be used by common tasks in restore
  18. *
  19. * @package core_backup
  20. * @subpackage moodle2
  21. * @category backup
  22. * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  23. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  24. */
  25. defined('MOODLE_INTERNAL') || die();
  26. /**
  27. * delete old directories and conditionally create backup_temp_ids table
  28. */
  29. class restore_create_and_clean_temp_stuff extends restore_execution_step {
  30. protected function define_execution() {
  31. $exists = restore_controller_dbops::create_restore_temp_tables($this->get_restoreid()); // temp tables conditionally
  32. // If the table already exists, it's because restore_prechecks have been executed in the same
  33. // request (without problems) and it already contains a bunch of preloaded information (users...)
  34. // that we aren't going to execute again
  35. if ($exists) { // Inform plan about preloaded information
  36. $this->task->set_preloaded_information();
  37. }
  38. // Create the old-course-ctxid to new-course-ctxid mapping, we need that available since the beginning
  39. $itemid = $this->task->get_old_contextid();
  40. $newitemid = context_course::instance($this->get_courseid())->id;
  41. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
  42. // Create the old-system-ctxid to new-system-ctxid mapping, we need that available since the beginning
  43. $itemid = $this->task->get_old_system_contextid();
  44. $newitemid = context_system::instance()->id;
  45. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'context', $itemid, $newitemid);
  46. // Create the old-course-id to new-course-id mapping, we need that available since the beginning
  47. $itemid = $this->task->get_old_courseid();
  48. $newitemid = $this->get_courseid();
  49. restore_dbops::set_backup_ids_record($this->get_restoreid(), 'course', $itemid, $newitemid);
  50. }
  51. }
  52. /**
  53. * delete the temp dir used by backup/restore (conditionally),
  54. * delete old directories and drop temp ids table
  55. */
  56. class restore_drop_and_clean_temp_stuff extends restore_execution_step {
  57. protected function define_execution() {
  58. global $CFG;
  59. restore_controller_dbops::drop_restore_temp_tables($this->get_restoreid()); // Drop ids temp table
  60. $progress = $this->task->get_progress();
  61. $progress->start_progress('Deleting backup dir');
  62. backup_helper::delete_old_backup_dirs(strtotime('-1 week'), $progress); // Delete > 1 week old temp dirs.
  63. if (empty($CFG->keeptempdirectoriesonbackup)) { // Conditionally
  64. backup_helper::delete_backup_dir($this->task->get_tempdir(), $progress); // Empty restore dir
  65. }
  66. $progress->end_progress();
  67. }
  68. }
  69. /**
  70. * Restore calculated grade items, grade categories etc
  71. */
  72. class restore_gradebook_structure_step extends restore_structure_step {
  73. /**
  74. * To conditionally decide if this step must be executed
  75. * Note the "settings" conditions are evaluated in the
  76. * corresponding task. Here we check for other conditions
  77. * not being restore settings (files, site settings...)
  78. */
  79. protected function execute_condition() {
  80. global $CFG, $DB;
  81. if ($this->get_courseid() == SITEID) {
  82. return false;
  83. }
  84. // No gradebook info found, don't execute
  85. $fullpath = $this->task->get_taskbasepath();
  86. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  87. if (!file_exists($fullpath)) {
  88. return false;
  89. }
  90. // Some module present in backup file isn't available to restore
  91. // in this site, don't execute
  92. if ($this->task->is_missing_modules()) {
  93. return false;
  94. }
  95. // Some activity has been excluded to be restored, don't execute
  96. if ($this->task->is_excluding_activities()) {
  97. return false;
  98. }
  99. // There should only be one grade category (the 1 associated with the course itself)
  100. // If other categories already exist we're restoring into an existing course.
  101. // Restoring categories into a course with an existing category structure is unlikely to go well
  102. $category = new stdclass();
  103. $category->courseid = $this->get_courseid();
  104. $catcount = $DB->count_records('grade_categories', (array)$category);
  105. if ($catcount>1) {
  106. return false;
  107. }
  108. // Identify the backup we're dealing with.
  109. $backuprelease = floatval($this->get_task()->get_info()->backup_release); // The major version: 2.9, 3.0, ...
  110. $backupbuild = 0;
  111. preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
  112. if (!empty($matches[1])) {
  113. $backupbuild = (int) $matches[1]; // The date of Moodle build at the time of the backup.
  114. }
  115. // On older versions the freeze value has to be converted.
  116. // We do this from here as it is happening right before the file is read.
  117. // This only targets the backup files that can contain the legacy freeze.
  118. if ($backupbuild > 20150618 && ($backuprelease < 3.0 || $backupbuild < 20160527)) {
  119. $this->rewrite_step_backup_file_for_legacy_freeze($fullpath);
  120. }
  121. // Arrived here, execute the step
  122. return true;
  123. }
  124. protected function define_structure() {
  125. $paths = array();
  126. $userinfo = $this->task->get_setting_value('users');
  127. $paths[] = new restore_path_element('attributes', '/gradebook/attributes');
  128. $paths[] = new restore_path_element('grade_category', '/gradebook/grade_categories/grade_category');
  129. $paths[] = new restore_path_element('grade_item', '/gradebook/grade_items/grade_item');
  130. if ($userinfo) {
  131. $paths[] = new restore_path_element('grade_grade', '/gradebook/grade_items/grade_item/grade_grades/grade_grade');
  132. }
  133. $paths[] = new restore_path_element('grade_letter', '/gradebook/grade_letters/grade_letter');
  134. $paths[] = new restore_path_element('grade_setting', '/gradebook/grade_settings/grade_setting');
  135. return $paths;
  136. }
  137. protected function process_attributes($data) {
  138. // For non-merge restore types:
  139. // Unset 'gradebook_calculations_freeze_' in the course and replace with the one from the backup.
  140. $target = $this->get_task()->get_target();
  141. if ($target == backup::TARGET_CURRENT_DELETING || $target == backup::TARGET_EXISTING_DELETING) {
  142. set_config('gradebook_calculations_freeze_' . $this->get_courseid(), null);
  143. }
  144. if (!empty($data['calculations_freeze'])) {
  145. if ($target == backup::TARGET_NEW_COURSE || $target == backup::TARGET_CURRENT_DELETING ||
  146. $target == backup::TARGET_EXISTING_DELETING) {
  147. set_config('gradebook_calculations_freeze_' . $this->get_courseid(), $data['calculations_freeze']);
  148. }
  149. }
  150. }
  151. protected function process_grade_item($data) {
  152. global $DB;
  153. $data = (object)$data;
  154. $oldid = $data->id;
  155. $data->course = $this->get_courseid();
  156. $data->courseid = $this->get_courseid();
  157. if ($data->itemtype=='manual') {
  158. // manual grade items store category id in categoryid
  159. $data->categoryid = $this->get_mappingid('grade_category', $data->categoryid, NULL);
  160. // if mapping failed put in course's grade category
  161. if (NULL == $data->categoryid) {
  162. $coursecat = grade_category::fetch_course_category($this->get_courseid());
  163. $data->categoryid = $coursecat->id;
  164. }
  165. } else if ($data->itemtype=='course') {
  166. // course grade item stores their category id in iteminstance
  167. $coursecat = grade_category::fetch_course_category($this->get_courseid());
  168. $data->iteminstance = $coursecat->id;
  169. } else if ($data->itemtype=='category') {
  170. // category grade items store their category id in iteminstance
  171. $data->iteminstance = $this->get_mappingid('grade_category', $data->iteminstance, NULL);
  172. } else {
  173. throw new restore_step_exception('unexpected_grade_item_type', $data->itemtype);
  174. }
  175. $data->scaleid = $this->get_mappingid('scale', $data->scaleid, NULL);
  176. $data->outcomeid = $this->get_mappingid('outcome', $data->outcomeid, NULL);
  177. $data->locktime = $this->apply_date_offset($data->locktime);
  178. $coursecategory = $newitemid = null;
  179. //course grade item should already exist so updating instead of inserting
  180. if($data->itemtype=='course') {
  181. //get the ID of the already created grade item
  182. $gi = new stdclass();
  183. $gi->courseid = $this->get_courseid();
  184. $gi->itemtype = $data->itemtype;
  185. //need to get the id of the grade_category that was automatically created for the course
  186. $category = new stdclass();
  187. $category->courseid = $this->get_courseid();
  188. $category->parent = null;
  189. //course category fullname starts out as ? but may be edited
  190. //$category->fullname = '?';
  191. $coursecategory = $DB->get_record('grade_categories', (array)$category);
  192. $gi->iteminstance = $coursecategory->id;
  193. $existinggradeitem = $DB->get_record('grade_items', (array)$gi);
  194. if (!empty($existinggradeitem)) {
  195. $data->id = $newitemid = $existinggradeitem->id;
  196. $DB->update_record('grade_items', $data);
  197. }
  198. } else if ($data->itemtype == 'manual') {
  199. // Manual items aren't assigned to a cm, so don't go duplicating them in the target if one exists.
  200. $gi = array(
  201. 'itemtype' => $data->itemtype,
  202. 'courseid' => $data->courseid,
  203. 'itemname' => $data->itemname,
  204. 'categoryid' => $data->categoryid,
  205. );
  206. $newitemid = $DB->get_field('grade_items', 'id', $gi);
  207. }
  208. if (empty($newitemid)) {
  209. //in case we found the course category but still need to insert the course grade item
  210. if ($data->itemtype=='course' && !empty($coursecategory)) {
  211. $data->iteminstance = $coursecategory->id;
  212. }
  213. $newitemid = $DB->insert_record('grade_items', $data);
  214. $data->id = $newitemid;
  215. $gradeitem = new grade_item($data);
  216. core\event\grade_item_created::create_from_grade_item($gradeitem)->trigger();
  217. }
  218. $this->set_mapping('grade_item', $oldid, $newitemid);
  219. }
  220. protected function process_grade_grade($data) {
  221. global $DB;
  222. $data = (object)$data;
  223. $oldid = $data->id;
  224. $olduserid = $data->userid;
  225. $data->itemid = $this->get_new_parentid('grade_item');
  226. $data->userid = $this->get_mappingid('user', $data->userid, null);
  227. if (!empty($data->userid)) {
  228. $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
  229. $data->locktime = $this->apply_date_offset($data->locktime);
  230. $gradeexists = $DB->record_exists('grade_grades', array('userid' => $data->userid, 'itemid' => $data->itemid));
  231. if ($gradeexists) {
  232. $message = "User id '{$data->userid}' already has a grade entry for grade item id '{$data->itemid}'";
  233. $this->log($message, backup::LOG_DEBUG);
  234. } else {
  235. $newitemid = $DB->insert_record('grade_grades', $data);
  236. $this->set_mapping('grade_grades', $oldid, $newitemid);
  237. }
  238. } else {
  239. $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
  240. $this->log($message, backup::LOG_DEBUG);
  241. }
  242. }
  243. protected function process_grade_category($data) {
  244. global $DB;
  245. $data = (object)$data;
  246. $oldid = $data->id;
  247. $data->course = $this->get_courseid();
  248. $data->courseid = $data->course;
  249. $newitemid = null;
  250. //no parent means a course level grade category. That may have been created when the course was created
  251. if(empty($data->parent)) {
  252. //parent was being saved as 0 when it should be null
  253. $data->parent = null;
  254. //get the already created course level grade category
  255. $category = new stdclass();
  256. $category->courseid = $this->get_courseid();
  257. $category->parent = null;
  258. $coursecategory = $DB->get_record('grade_categories', (array)$category);
  259. if (!empty($coursecategory)) {
  260. $data->id = $newitemid = $coursecategory->id;
  261. $DB->update_record('grade_categories', $data);
  262. }
  263. }
  264. // Add a warning about a removed setting.
  265. if (!empty($data->aggregatesubcats)) {
  266. set_config('show_aggregatesubcats_upgrade_' . $data->courseid, 1);
  267. }
  268. //need to insert a course category
  269. if (empty($newitemid)) {
  270. $newitemid = $DB->insert_record('grade_categories', $data);
  271. }
  272. $this->set_mapping('grade_category', $oldid, $newitemid);
  273. }
  274. protected function process_grade_letter($data) {
  275. global $DB;
  276. $data = (object)$data;
  277. $oldid = $data->id;
  278. $data->contextid = context_course::instance($this->get_courseid())->id;
  279. $gradeletter = (array)$data;
  280. unset($gradeletter['id']);
  281. if (!$DB->record_exists('grade_letters', $gradeletter)) {
  282. $newitemid = $DB->insert_record('grade_letters', $data);
  283. } else {
  284. $newitemid = $data->id;
  285. }
  286. $this->set_mapping('grade_letter', $oldid, $newitemid);
  287. }
  288. protected function process_grade_setting($data) {
  289. global $DB;
  290. $data = (object)$data;
  291. $oldid = $data->id;
  292. $data->courseid = $this->get_courseid();
  293. $target = $this->get_task()->get_target();
  294. if ($data->name == 'minmaxtouse' &&
  295. ($target == backup::TARGET_CURRENT_ADDING || $target == backup::TARGET_EXISTING_ADDING)) {
  296. // We never restore minmaxtouse during merge.
  297. return;
  298. }
  299. if (!$DB->record_exists('grade_settings', array('courseid' => $data->courseid, 'name' => $data->name))) {
  300. $newitemid = $DB->insert_record('grade_settings', $data);
  301. } else {
  302. $newitemid = $data->id;
  303. }
  304. if (!empty($oldid)) {
  305. // In rare cases (minmaxtouse), it is possible that there wasn't any ID associated with the setting.
  306. $this->set_mapping('grade_setting', $oldid, $newitemid);
  307. }
  308. }
  309. /**
  310. * put all activity grade items in the correct grade category and mark all for recalculation
  311. */
  312. protected function after_execute() {
  313. global $DB;
  314. $conditions = array(
  315. 'backupid' => $this->get_restoreid(),
  316. 'itemname' => 'grade_item'//,
  317. //'itemid' => $itemid
  318. );
  319. $rs = $DB->get_recordset('backup_ids_temp', $conditions);
  320. // We need this for calculation magic later on.
  321. $mappings = array();
  322. if (!empty($rs)) {
  323. foreach($rs as $grade_item_backup) {
  324. // Store the oldid with the new id.
  325. $mappings[$grade_item_backup->itemid] = $grade_item_backup->newitemid;
  326. $updateobj = new stdclass();
  327. $updateobj->id = $grade_item_backup->newitemid;
  328. //if this is an activity grade item that needs to be put back in its correct category
  329. if (!empty($grade_item_backup->parentitemid)) {
  330. $oldcategoryid = $this->get_mappingid('grade_category', $grade_item_backup->parentitemid, null);
  331. if (!is_null($oldcategoryid)) {
  332. $updateobj->categoryid = $oldcategoryid;
  333. $DB->update_record('grade_items', $updateobj);
  334. }
  335. } else {
  336. //mark course and category items as needing to be recalculated
  337. $updateobj->needsupdate=1;
  338. $DB->update_record('grade_items', $updateobj);
  339. }
  340. }
  341. }
  342. $rs->close();
  343. // We need to update the calculations for calculated grade items that may reference old
  344. // grade item ids using ##gi\d+##.
  345. // $mappings can be empty, use 0 if so (won't match ever)
  346. list($sql, $params) = $DB->get_in_or_equal(array_values($mappings), SQL_PARAMS_NAMED, 'param', true, 0);
  347. $sql = "SELECT gi.id, gi.calculation
  348. FROM {grade_items} gi
  349. WHERE gi.id {$sql} AND
  350. calculation IS NOT NULL";
  351. $rs = $DB->get_recordset_sql($sql, $params);
  352. foreach ($rs as $gradeitem) {
  353. // Collect all of the used grade item id references
  354. if (preg_match_all('/##gi(\d+)##/', $gradeitem->calculation, $matches) < 1) {
  355. // This calculation doesn't reference any other grade items... EASY!
  356. continue;
  357. }
  358. // For this next bit we are going to do the replacement of id's in two steps:
  359. // 1. We will replace all old id references with a special mapping reference.
  360. // 2. We will replace all mapping references with id's
  361. // Why do we do this?
  362. // Because there potentially there will be an overlap of ids within the query and we
  363. // we substitute the wrong id.. safest way around this is the two step system
  364. $calculationmap = array();
  365. $mapcount = 0;
  366. foreach ($matches[1] as $match) {
  367. // Check that the old id is known to us, if not it was broken to begin with and will
  368. // continue to be broken.
  369. if (!array_key_exists($match, $mappings)) {
  370. continue;
  371. }
  372. // Our special mapping key
  373. $mapping = '##MAPPING'.$mapcount.'##';
  374. // The old id that exists within the calculation now
  375. $oldid = '##gi'.$match.'##';
  376. // The new id that we want to replace the old one with.
  377. $newid = '##gi'.$mappings[$match].'##';
  378. // Replace in the special mapping key
  379. $gradeitem->calculation = str_replace($oldid, $mapping, $gradeitem->calculation);
  380. // And record the mapping
  381. $calculationmap[$mapping] = $newid;
  382. $mapcount++;
  383. }
  384. // Iterate all special mappings for this calculation and replace in the new id's
  385. foreach ($calculationmap as $mapping => $newid) {
  386. $gradeitem->calculation = str_replace($mapping, $newid, $gradeitem->calculation);
  387. }
  388. // Update the calculation now that its being remapped
  389. $DB->update_record('grade_items', $gradeitem);
  390. }
  391. $rs->close();
  392. // Need to correct the grade category path and parent
  393. $conditions = array(
  394. 'courseid' => $this->get_courseid()
  395. );
  396. $rs = $DB->get_recordset('grade_categories', $conditions);
  397. // Get all the parents correct first as grade_category::build_path() loads category parents from the DB
  398. foreach ($rs as $gc) {
  399. if (!empty($gc->parent)) {
  400. $grade_category = new stdClass();
  401. $grade_category->id = $gc->id;
  402. $grade_category->parent = $this->get_mappingid('grade_category', $gc->parent);
  403. $DB->update_record('grade_categories', $grade_category);
  404. }
  405. }
  406. $rs->close();
  407. // Now we can rebuild all the paths
  408. $rs = $DB->get_recordset('grade_categories', $conditions);
  409. foreach ($rs as $gc) {
  410. $grade_category = new stdClass();
  411. $grade_category->id = $gc->id;
  412. $grade_category->path = grade_category::build_path($gc);
  413. $grade_category->depth = substr_count($grade_category->path, '/') - 1;
  414. $DB->update_record('grade_categories', $grade_category);
  415. }
  416. $rs->close();
  417. // Check what to do with the minmaxtouse setting.
  418. $this->check_minmaxtouse();
  419. // Freeze gradebook calculations if needed.
  420. $this->gradebook_calculation_freeze();
  421. // Ensure the module cache is current when recalculating grades.
  422. rebuild_course_cache($this->get_courseid(), true);
  423. // Restore marks items as needing update. Update everything now.
  424. grade_regrade_final_grades($this->get_courseid());
  425. }
  426. /**
  427. * Freeze gradebook calculation if needed.
  428. *
  429. * This is similar to various upgrade scripts that check if the freeze is needed.
  430. */
  431. protected function gradebook_calculation_freeze() {
  432. global $CFG;
  433. $gradebookcalculationsfreeze = get_config('core', 'gradebook_calculations_freeze_' . $this->get_courseid());
  434. preg_match('/(\d{8})/', $this->get_task()->get_info()->moodle_release, $matches);
  435. $backupbuild = (int)$matches[1];
  436. // The function floatval will return a float even if there is text mixed with the release number.
  437. $backuprelease = floatval($this->get_task()->get_info()->backup_release);
  438. // Extra credits need adjustments only for backups made between 2.8 release (20141110) and the fix release (20150619).
  439. if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150619) {
  440. require_once($CFG->libdir . '/db/upgradelib.php');
  441. upgrade_extra_credit_weightoverride($this->get_courseid());
  442. }
  443. // Calculated grade items need recalculating for backups made between 2.8 release (20141110) and the fix release (20150627).
  444. if (!$gradebookcalculationsfreeze && $backupbuild >= 20141110 && $backupbuild < 20150627) {
  445. require_once($CFG->libdir . '/db/upgradelib.php');
  446. upgrade_calculated_grade_items($this->get_courseid());
  447. }
  448. // Courses from before 3.1 (20160518) may have a letter boundary problem and should be checked for this issue.
  449. // Backups from before and including 2.9 could have a build number that is greater than 20160518 and should
  450. // be checked for this problem.
  451. if (!$gradebookcalculationsfreeze && ($backupbuild < 20160518 || $backuprelease <= 2.9)) {
  452. require_once($CFG->libdir . '/db/upgradelib.php');
  453. upgrade_course_letter_boundary($this->get_courseid());
  454. }
  455. }
  456. /**
  457. * Checks what should happen with the course grade setting minmaxtouse.
  458. *
  459. * This is related to the upgrade step at the time the setting was added.
  460. *
  461. * @see MDL-48618
  462. * @return void
  463. */
  464. protected function check_minmaxtouse() {
  465. global $CFG, $DB;
  466. require_once($CFG->libdir . '/gradelib.php');
  467. $userinfo = $this->task->get_setting_value('users');
  468. $settingname = 'minmaxtouse';
  469. $courseid = $this->get_courseid();
  470. $minmaxtouse = $DB->get_field('grade_settings', 'value', array('courseid' => $courseid, 'name' => $settingname));
  471. $version28start = 2014111000.00;
  472. $version28last = 2014111006.05;
  473. $version29start = 2015051100.00;
  474. $version29last = 2015060400.02;
  475. $target = $this->get_task()->get_target();
  476. if ($minmaxtouse === false &&
  477. ($target != backup::TARGET_CURRENT_ADDING && $target != backup::TARGET_EXISTING_ADDING)) {
  478. // The setting was not found because this setting did not exist at the time the backup was made.
  479. // And we are not restoring as merge, in which case we leave the course as it was.
  480. $version = $this->get_task()->get_info()->moodle_version;
  481. if ($version < $version28start) {
  482. // We need to set it to use grade_item, but only if the site-wide setting is different. No need to notice them.
  483. if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_ITEM) {
  484. grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_ITEM);
  485. }
  486. } else if (($version >= $version28start && $version < $version28last) ||
  487. ($version >= $version29start && $version < $version29last)) {
  488. // They should be using grade_grade when the course has inconsistencies.
  489. $sql = "SELECT gi.id
  490. FROM {grade_items} gi
  491. JOIN {grade_grades} gg
  492. ON gg.itemid = gi.id
  493. WHERE gi.courseid = ?
  494. AND (gi.itemtype != ? AND gi.itemtype != ?)
  495. AND (gg.rawgrademax != gi.grademax OR gg.rawgrademin != gi.grademin)";
  496. // The course can only have inconsistencies when we restore the user info,
  497. // we do not need to act on existing grades that were not restored as part of this backup.
  498. if ($userinfo && $DB->record_exists_sql($sql, array($courseid, 'course', 'category'))) {
  499. // Display the notice as we do during upgrade.
  500. set_config('show_min_max_grades_changed_' . $courseid, 1);
  501. if ($CFG->grade_minmaxtouse != GRADE_MIN_MAX_FROM_GRADE_GRADE) {
  502. // We need set the setting as their site-wise setting is not GRADE_MIN_MAX_FROM_GRADE_GRADE.
  503. // If they are using the site-wide grade_grade setting, we only want to notice them.
  504. grade_set_setting($courseid, $settingname, GRADE_MIN_MAX_FROM_GRADE_GRADE);
  505. }
  506. }
  507. } else {
  508. // This should never happen because from now on minmaxtouse is always saved in backups.
  509. }
  510. }
  511. }
  512. /**
  513. * Rewrite step definition to handle the legacy freeze attribute.
  514. *
  515. * In previous backups the calculations_freeze property was stored as an attribute of the
  516. * top level node <gradebook>. The backup API, however, do not process grandparent nodes.
  517. * It only processes definitive children, and their parent attributes.
  518. *
  519. * We had:
  520. *
  521. * <gradebook calculations_freeze="20160511">
  522. * <grade_categories>
  523. * <grade_category id="10">
  524. * <depth>1</depth>
  525. * ...
  526. * </grade_category>
  527. * </grade_categories>
  528. * ...
  529. * </gradebook>
  530. *
  531. * And this method will convert it to:
  532. *
  533. * <gradebook >
  534. * <attributes>
  535. * <calculations_freeze>20160511</calculations_freeze>
  536. * </attributes>
  537. * <grade_categories>
  538. * <grade_category id="10">
  539. * <depth>1</depth>
  540. * ...
  541. * </grade_category>
  542. * </grade_categories>
  543. * ...
  544. * </gradebook>
  545. *
  546. * Note that we cannot just load the XML file in memory as it could potentially be huge.
  547. * We can also completely ignore if the node <attributes> is already in the backup
  548. * file as it never existed before.
  549. *
  550. * @param string $filepath The absolute path to the XML file.
  551. * @return void
  552. */
  553. protected function rewrite_step_backup_file_for_legacy_freeze($filepath) {
  554. $foundnode = false;
  555. $newfile = make_request_directory(true) . DIRECTORY_SEPARATOR . 'file.xml';
  556. $fr = fopen($filepath, 'r');
  557. $fw = fopen($newfile, 'w');
  558. if ($fr && $fw) {
  559. while (($line = fgets($fr, 4096)) !== false) {
  560. if (!$foundnode && strpos($line, '<gradebook ') === 0) {
  561. $foundnode = true;
  562. $matches = array();
  563. $pattern = '@calculations_freeze=.([0-9]+).@';
  564. if (preg_match($pattern, $line, $matches)) {
  565. $freeze = $matches[1];
  566. $line = preg_replace($pattern, '', $line);
  567. $line .= " <attributes>\n <calculations_freeze>$freeze</calculations_freeze>\n </attributes>\n";
  568. }
  569. }
  570. fputs($fw, $line);
  571. }
  572. if (!feof($fr)) {
  573. throw new restore_step_exception('Error while attempting to rewrite the gradebook step file.');
  574. }
  575. fclose($fr);
  576. fclose($fw);
  577. if (!rename($newfile, $filepath)) {
  578. throw new restore_step_exception('Error while attempting to rename the gradebook step file.');
  579. }
  580. } else {
  581. if ($fr) {
  582. fclose($fr);
  583. }
  584. if ($fw) {
  585. fclose($fw);
  586. }
  587. }
  588. }
  589. }
  590. /**
  591. * Step in charge of restoring the grade history of a course.
  592. *
  593. * The execution conditions are itendical to {@link restore_gradebook_structure_step} because
  594. * we do not want to restore the history if the gradebook and its content has not been
  595. * restored. At least for now.
  596. */
  597. class restore_grade_history_structure_step extends restore_structure_step {
  598. protected function execute_condition() {
  599. global $CFG, $DB;
  600. if ($this->get_courseid() == SITEID) {
  601. return false;
  602. }
  603. // No gradebook info found, don't execute.
  604. $fullpath = $this->task->get_taskbasepath();
  605. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  606. if (!file_exists($fullpath)) {
  607. return false;
  608. }
  609. // Some module present in backup file isn't available to restore in this site, don't execute.
  610. if ($this->task->is_missing_modules()) {
  611. return false;
  612. }
  613. // Some activity has been excluded to be restored, don't execute.
  614. if ($this->task->is_excluding_activities()) {
  615. return false;
  616. }
  617. // There should only be one grade category (the 1 associated with the course itself).
  618. $category = new stdclass();
  619. $category->courseid = $this->get_courseid();
  620. $catcount = $DB->count_records('grade_categories', (array)$category);
  621. if ($catcount > 1) {
  622. return false;
  623. }
  624. // Arrived here, execute the step.
  625. return true;
  626. }
  627. protected function define_structure() {
  628. $paths = array();
  629. // Settings to use.
  630. $userinfo = $this->get_setting_value('users');
  631. $history = $this->get_setting_value('grade_histories');
  632. if ($userinfo && $history) {
  633. $paths[] = new restore_path_element('grade_grade',
  634. '/grade_history/grade_grades/grade_grade');
  635. }
  636. return $paths;
  637. }
  638. protected function process_grade_grade($data) {
  639. global $DB;
  640. $data = (object)($data);
  641. $olduserid = $data->userid;
  642. unset($data->id);
  643. $data->userid = $this->get_mappingid('user', $data->userid, null);
  644. if (!empty($data->userid)) {
  645. // Do not apply the date offsets as this is history.
  646. $data->itemid = $this->get_mappingid('grade_item', $data->itemid);
  647. $data->oldid = $this->get_mappingid('grade_grades', $data->oldid);
  648. $data->usermodified = $this->get_mappingid('user', $data->usermodified, null);
  649. $data->rawscaleid = $this->get_mappingid('scale', $data->rawscaleid);
  650. $DB->insert_record('grade_grades_history', $data);
  651. } else {
  652. $message = "Mapped user id not found for user id '{$olduserid}', grade item id '{$data->itemid}'";
  653. $this->log($message, backup::LOG_DEBUG);
  654. }
  655. }
  656. }
  657. /**
  658. * decode all the interlinks present in restored content
  659. * relying 100% in the restore_decode_processor that handles
  660. * both the contents to modify and the rules to be applied
  661. */
  662. class restore_decode_interlinks extends restore_execution_step {
  663. protected function define_execution() {
  664. // Get the decoder (from the plan)
  665. $decoder = $this->task->get_decoder();
  666. restore_decode_processor::register_link_decoders($decoder); // Add decoder contents and rules
  667. // And launch it, everything will be processed
  668. $decoder->execute();
  669. }
  670. }
  671. /**
  672. * first, ensure that we have no gaps in section numbers
  673. * and then, rebuid the course cache
  674. */
  675. class restore_rebuild_course_cache extends restore_execution_step {
  676. protected function define_execution() {
  677. global $DB;
  678. // Although there is some sort of auto-recovery of missing sections
  679. // present in course/formats... here we check that all the sections
  680. // from 0 to MAX(section->section) exist, creating them if necessary
  681. $maxsection = $DB->get_field('course_sections', 'MAX(section)', array('course' => $this->get_courseid()));
  682. // Iterate over all sections
  683. for ($i = 0; $i <= $maxsection; $i++) {
  684. // If the section $i doesn't exist, create it
  685. if (!$DB->record_exists('course_sections', array('course' => $this->get_courseid(), 'section' => $i))) {
  686. $sectionrec = array(
  687. 'course' => $this->get_courseid(),
  688. 'section' => $i,
  689. 'timemodified' => time());
  690. $DB->insert_record('course_sections', $sectionrec); // missing section created
  691. }
  692. }
  693. // Rebuild cache now that all sections are in place
  694. rebuild_course_cache($this->get_courseid());
  695. cache_helper::purge_by_event('changesincourse');
  696. cache_helper::purge_by_event('changesincoursecat');
  697. }
  698. }
  699. /**
  700. * Review all the tasks having one after_restore method
  701. * executing it to perform some final adjustments of information
  702. * not available when the task was executed.
  703. */
  704. class restore_execute_after_restore extends restore_execution_step {
  705. protected function define_execution() {
  706. // Simply call to the execute_after_restore() method of the task
  707. // that always is the restore_final_task
  708. $this->task->launch_execute_after_restore();
  709. }
  710. }
  711. /**
  712. * Review all the (pending) block positions in backup_ids, matching by
  713. * contextid, creating positions as needed. This is executed by the
  714. * final task, once all the contexts have been created
  715. */
  716. class restore_review_pending_block_positions extends restore_execution_step {
  717. protected function define_execution() {
  718. global $DB;
  719. // Get all the block_position objects pending to match
  720. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'block_position');
  721. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
  722. // Process block positions, creating them or accumulating for final step
  723. foreach($rs as $posrec) {
  724. // Get the complete position object out of the info field.
  725. $position = backup_controller_dbops::decode_backup_temp_info($posrec->info);
  726. // If position is for one already mapped (known) contextid
  727. // process it now, creating the position, else nothing to
  728. // do, position finally discarded
  729. if ($newctx = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'context', $position->contextid)) {
  730. $position->contextid = $newctx->newitemid;
  731. // Create the block position
  732. $DB->insert_record('block_positions', $position);
  733. }
  734. }
  735. $rs->close();
  736. }
  737. }
  738. /**
  739. * Updates the availability data for course modules and sections.
  740. *
  741. * Runs after the restore of all course modules, sections, and grade items has
  742. * completed. This is necessary in order to update IDs that have changed during
  743. * restore.
  744. *
  745. * @package core_backup
  746. * @copyright 2014 The Open University
  747. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  748. */
  749. class restore_update_availability extends restore_execution_step {
  750. protected function define_execution() {
  751. global $CFG, $DB;
  752. // Note: This code runs even if availability is disabled when restoring.
  753. // That will ensure that if you later turn availability on for the site,
  754. // there will be no incorrect IDs. (It doesn't take long if the restored
  755. // data does not contain any availability information.)
  756. // Get modinfo with all data after resetting cache.
  757. rebuild_course_cache($this->get_courseid(), true);
  758. $modinfo = get_fast_modinfo($this->get_courseid());
  759. // Get the date offset for this restore.
  760. $dateoffset = $this->apply_date_offset(1) - 1;
  761. // Update all sections that were restored.
  762. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_section');
  763. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
  764. $sectionsbyid = null;
  765. foreach ($rs as $rec) {
  766. if (is_null($sectionsbyid)) {
  767. $sectionsbyid = array();
  768. foreach ($modinfo->get_section_info_all() as $section) {
  769. $sectionsbyid[$section->id] = $section;
  770. }
  771. }
  772. if (!array_key_exists($rec->newitemid, $sectionsbyid)) {
  773. // If the section was not fully restored for some reason
  774. // (e.g. due to an earlier error), skip it.
  775. $this->get_logger()->process('Section not fully restored: id ' .
  776. $rec->newitemid, backup::LOG_WARNING);
  777. continue;
  778. }
  779. $section = $sectionsbyid[$rec->newitemid];
  780. if (!is_null($section->availability)) {
  781. $info = new \core_availability\info_section($section);
  782. $info->update_after_restore($this->get_restoreid(),
  783. $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
  784. }
  785. }
  786. $rs->close();
  787. // Update all modules that were restored.
  788. $params = array('backupid' => $this->get_restoreid(), 'itemname' => 'course_module');
  789. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'newitemid');
  790. foreach ($rs as $rec) {
  791. if (!array_key_exists($rec->newitemid, $modinfo->cms)) {
  792. // If the module was not fully restored for some reason
  793. // (e.g. due to an earlier error), skip it.
  794. $this->get_logger()->process('Module not fully restored: id ' .
  795. $rec->newitemid, backup::LOG_WARNING);
  796. continue;
  797. }
  798. $cm = $modinfo->get_cm($rec->newitemid);
  799. if (!is_null($cm->availability)) {
  800. $info = new \core_availability\info_module($cm);
  801. $info->update_after_restore($this->get_restoreid(),
  802. $this->get_courseid(), $this->get_logger(), $dateoffset, $this->task);
  803. }
  804. }
  805. $rs->close();
  806. }
  807. }
  808. /**
  809. * Process legacy module availability records in backup_ids.
  810. *
  811. * Matches course modules and grade item id once all them have been already restored.
  812. * Only if all matchings are satisfied the availability condition will be created.
  813. * At the same time, it is required for the site to have that functionality enabled.
  814. *
  815. * This step is included only to handle legacy backups (2.6 and before). It does not
  816. * do anything for newer backups.
  817. *
  818. * @copyright 2014 The Open University
  819. * @license http://www.gnu.org/copyleft/gpl.html GNU Public License
  820. */
  821. class restore_process_course_modules_availability extends restore_execution_step {
  822. protected function define_execution() {
  823. global $CFG, $DB;
  824. // Site hasn't availability enabled
  825. if (empty($CFG->enableavailability)) {
  826. return;
  827. }
  828. // Do both modules and sections.
  829. foreach (array('module', 'section') as $table) {
  830. // Get all the availability objects to process.
  831. $params = array('backupid' => $this->get_restoreid(), 'itemname' => $table . '_availability');
  832. $rs = $DB->get_recordset('backup_ids_temp', $params, '', 'itemid, info');
  833. // Process availabilities, creating them if everything matches ok.
  834. foreach ($rs as $availrec) {
  835. $allmatchesok = true;
  836. // Get the complete legacy availability object.
  837. $availability = backup_controller_dbops::decode_backup_temp_info($availrec->info);
  838. // Note: This code used to update IDs, but that is now handled by the
  839. // current code (after restore) instead of this legacy code.
  840. // Get showavailability option.
  841. $thingid = ($table === 'module') ? $availability->coursemoduleid :
  842. $availability->coursesectionid;
  843. $showrec = restore_dbops::get_backup_ids_record($this->get_restoreid(),
  844. $table . '_showavailability', $thingid);
  845. if (!$showrec) {
  846. // Should not happen.
  847. throw new coding_exception('No matching showavailability record');
  848. }
  849. $show = $showrec->info->showavailability;
  850. // The $availability object is now in the format used in the old
  851. // system. Interpret this and convert to new system.
  852. $currentvalue = $DB->get_field('course_' . $table . 's', 'availability',
  853. array('id' => $thingid), MUST_EXIST);
  854. $newvalue = \core_availability\info::add_legacy_availability_condition(
  855. $currentvalue, $availability, $show);
  856. $DB->set_field('course_' . $table . 's', 'availability', $newvalue,
  857. array('id' => $thingid));
  858. }
  859. $rs->close();
  860. }
  861. }
  862. }
  863. /*
  864. * Execution step that, *conditionally* (if there isn't preloaded information)
  865. * will load the inforef files for all the included course/section/activity tasks
  866. * to backup_temp_ids. They will be stored with "xxxxref" as itemname
  867. */
  868. class restore_load_included_inforef_records extends restore_execution_step {
  869. protected function define_execution() {
  870. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  871. return;
  872. }
  873. // Get all the included tasks
  874. $tasks = restore_dbops::get_included_tasks($this->get_restoreid());
  875. $progress = $this->task->get_progress();
  876. $progress->start_progress($this->get_name(), count($tasks));
  877. foreach ($tasks as $task) {
  878. // Load the inforef.xml file if exists
  879. $inforefpath = $task->get_taskbasepath() . '/inforef.xml';
  880. if (file_exists($inforefpath)) {
  881. // Load each inforef file to temp_ids.
  882. restore_dbops::load_inforef_to_tempids($this->get_restoreid(), $inforefpath, $progress);
  883. }
  884. }
  885. $progress->end_progress();
  886. }
  887. }
  888. /*
  889. * Execution step that will load all the needed files into backup_files_temp
  890. * - info: contains the whole original object (times, names...)
  891. * (all them being original ids as loaded from xml)
  892. */
  893. class restore_load_included_files extends restore_structure_step {
  894. protected function define_structure() {
  895. $file = new restore_path_element('file', '/files/file');
  896. return array($file);
  897. }
  898. /**
  899. * Process one <file> element from files.xml
  900. *
  901. * @param array $data the element data
  902. */
  903. public function process_file($data) {
  904. $data = (object)$data; // handy
  905. // load it if needed:
  906. // - it it is one of the annotated inforef files (course/section/activity/block)
  907. // - it is one "user", "group", "grouping", "grade", "question" or "qtype_xxxx" component file (that aren't sent to inforef ever)
  908. // TODO: qtype_xxx should be replaced by proper backup_qtype_plugin::get_components_and_fileareas() use,
  909. // but then we'll need to change it to load plugins itself (because this is executed too early in restore)
  910. $isfileref = restore_dbops::get_backup_ids_record($this->get_restoreid(), 'fileref', $data->id);
  911. $iscomponent = ($data->component == 'user' || $data->component == 'group' || $data->component == 'badges' ||
  912. $data->component == 'grouping' || $data->component == 'grade' ||
  913. $data->component == 'question' || substr($data->component, 0, 5) == 'qtype');
  914. if ($isfileref || $iscomponent) {
  915. restore_dbops::set_backup_files_record($this->get_restoreid(), $data);
  916. }
  917. }
  918. }
  919. /**
  920. * Execution step that, *conditionally* (if there isn't preloaded information),
  921. * will load all the needed roles to backup_temp_ids. They will be stored with
  922. * "role" itemname. Also it will perform one automatic mapping to roles existing
  923. * in the target site, based in permissions of the user performing the restore,
  924. * archetypes and other bits. At the end, each original role will have its associated
  925. * target role or 0 if it's going to be skipped. Note we wrap everything over one
  926. * restore_dbops method, as far as the same stuff is going to be also executed
  927. * by restore prechecks
  928. */
  929. class restore_load_and_map_roles extends restore_execution_step {
  930. protected function define_execution() {
  931. if ($this->task->get_preloaded_information()) { // if info is already preloaded
  932. return;
  933. }
  934. $file = $this->get_basepath() . '/roles.xml';
  935. // Load needed toles to temp_ids
  936. restore_dbops::load_roles_to_tempids($this->get_restoreid(), $file);
  937. // Process roles, mapping/skipping. Any error throws exception
  938. // Note we pass controller's info because it can contain role mapping information
  939. // about manual mappings performed by UI
  940. restore_dbops::process_included_roles($this->get_restoreid(), $this->task->get_courseid(), $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_info()->role_mappings);
  941. }
  942. }
  943. /**
  944. * Execution step that, *conditionally* (if there isn't preloaded information
  945. * and users have been selected in settings, will load all the needed users
  946. * to backup_temp_ids. They will be stored with "user" itemname and with
  947. * their original contextid as paremitemid
  948. */
  949. class restore_load_included_users extends restore_execution_step {
  950. protected function define_execution() {
  951. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  952. return;
  953. }
  954. if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
  955. return;
  956. }
  957. $file = $this->get_basepath() . '/users.xml';
  958. // Load needed users to temp_ids.
  959. restore_dbops::load_users_to_tempids($this->get_restoreid(), $file, $this->task->get_progress());
  960. }
  961. }
  962. /**
  963. * Execution step that, *conditionally* (if there isn't preloaded information
  964. * and users have been selected in settings, will process all the needed users
  965. * in order to decide and perform any action with them (create / map / error)
  966. * Note: Any error will cause exception, as far as this is the same processing
  967. * than the one into restore prechecks (that should have stopped process earlier)
  968. */
  969. class restore_process_included_users extends restore_execution_step {
  970. protected function define_execution() {
  971. if ($this->task->get_preloaded_information()) { // if info is already preloaded, nothing to do
  972. return;
  973. }
  974. if (!$this->task->get_setting_value('users')) { // No userinfo being restored, nothing to do
  975. return;
  976. }
  977. restore_dbops::process_included_users($this->get_restoreid(), $this->task->get_courseid(),
  978. $this->task->get_userid(), $this->task->is_samesite(), $this->task->get_progress());
  979. }
  980. }
  981. /**
  982. * Execution step that will create all the needed users as calculated
  983. * by @restore_process_included_users (those having newiteind = 0)
  984. */
  985. class restore_create_included_users extends restore_execution_step {
  986. protected function define_execution() {
  987. restore_dbops::create_included_users($this->get_basepath(), $this->get_restoreid(),
  988. $this->task->get_userid(), $this->task->get_progress());
  989. }
  990. }
  991. /**
  992. * Structure step that will create all the needed groups and groupings
  993. * by loading them from the groups.xml file performing the required matches.
  994. * Note group members only will be added if restoring user info
  995. */
  996. class restore_groups_structure_step extends restore_structure_step {
  997. protected function define_structure() {
  998. $paths = array(); // Add paths here
  999. // Do not include group/groupings information if not requested.
  1000. $groupinfo = $this->get_setting_value('groups');
  1001. if ($groupinfo) {
  1002. $paths[] = new restore_path_element('group', '/groups/group');
  1003. $paths[] = new rest

Large files files are truncated, but you can click here to view the full file