PageRenderTime 61ms CodeModel.GetById 15ms RepoModel.GetById 1ms 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

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. * 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. $pat

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