PageRenderTime 36ms CodeModel.GetById 9ms RepoModel.GetById 0ms app.codeStats 0ms

/backup/util/plan/restore_structure_step.class.php

http://github.com/moodle/moodle
PHP | 548 lines | 272 code | 53 blank | 223 comment | 47 complexity | ba97024edbe26d29fac8ea6a8ffef9e2 MD5 | raw file
Possible License(s): MIT, AGPL-3.0, MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, Apache-2.0, LGPL-2.1, BSD-3-Clause
  1. <?php
  2. // This file is part of Moodle - http://moodle.org/
  3. //
  4. // Moodle is free software: you can redistribute it and/or modify
  5. // it under the terms of the GNU General Public License as published by
  6. // the Free Software Foundation, either version 3 of the License, or
  7. // (at your option) any later version.
  8. //
  9. // Moodle is distributed in the hope that it will be useful,
  10. // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. // GNU General Public License for more details.
  13. //
  14. // You should have received a copy of the GNU General Public License
  15. // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
  16. /**
  17. * @package moodlecore
  18. * @subpackage backup-plan
  19. * @copyright 2010 onwards Eloy Lafuente (stronk7) {@link http://stronk7.com}
  20. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  21. */
  22. /**
  23. * Abstract class defining the needed stuff to restore one xml file
  24. *
  25. * TODO: Finish phpdocs
  26. */
  27. abstract class restore_structure_step extends restore_step {
  28. protected $filename; // Name of the file to be parsed
  29. protected $contentprocessor; // xml parser processor being used
  30. // (need it here, apart from parser
  31. // thanks to serialized data to process -
  32. // say thanks to blocks!)
  33. protected $pathelements; // Array of pathelements to process
  34. protected $elementsoldid; // Array to store last oldid used on each element
  35. protected $elementsnewid; // Array to store last newid used on each element
  36. protected $pathlock; // Path currently locking processing of children
  37. const SKIP_ALL_CHILDREN = -991399; // To instruct the dispatcher about to ignore
  38. // all children below path processor returning it
  39. /**
  40. * Constructor - instantiates one object of this class
  41. */
  42. public function __construct($name, $filename, $task = null) {
  43. if (!is_null($task) && !($task instanceof restore_task)) {
  44. throw new restore_step_exception('wrong_restore_task_specified');
  45. }
  46. $this->filename = $filename;
  47. $this->contentprocessor = null;
  48. $this->pathelements = array();
  49. $this->elementsoldid = array();
  50. $this->elementsnewid = array();
  51. $this->pathlock = null;
  52. parent::__construct($name, $task);
  53. }
  54. final public function execute() {
  55. if (!$this->execute_condition()) { // Check any condition to execute this
  56. return;
  57. }
  58. $fullpath = $this->task->get_taskbasepath();
  59. // We MUST have one fullpath here, else, error
  60. if (empty($fullpath)) {
  61. throw new restore_step_exception('restore_structure_step_undefined_fullpath');
  62. }
  63. // Append the filename to the fullpath
  64. $fullpath = rtrim($fullpath, '/') . '/' . $this->filename;
  65. // And it MUST exist
  66. if (!file_exists($fullpath)) { // Shouldn't happen ever, but...
  67. throw new restore_step_exception('missing_moodle_backup_xml_file', $fullpath);
  68. }
  69. // Get restore_path elements array adapting and preparing it for processing
  70. $structure = $this->define_structure();
  71. if (!is_array($structure)) {
  72. throw new restore_step_exception('restore_step_structure_not_array', $this->get_name());
  73. }
  74. $this->prepare_pathelements($structure);
  75. // Create parser and processor
  76. $xmlparser = new progressive_parser();
  77. $xmlparser->set_file($fullpath);
  78. $xmlprocessor = new restore_structure_parser_processor($this->task->get_courseid(), $this);
  79. $this->contentprocessor = $xmlprocessor; // Save the reference to the contentprocessor
  80. // as far as we are going to need it out
  81. // from parser (blame serialized data!)
  82. $xmlparser->set_processor($xmlprocessor);
  83. // Add pathelements to processor
  84. foreach ($this->pathelements as $element) {
  85. $xmlprocessor->add_path($element->get_path(), $element->is_grouped());
  86. }
  87. // Set up progress tracking.
  88. $progress = $this->get_task()->get_progress();
  89. $progress->start_progress($this->get_name(), \core\progress\base::INDETERMINATE);
  90. $xmlparser->set_progress($progress);
  91. // And process it, dispatch to target methods in step will start automatically
  92. $xmlparser->process();
  93. // Have finished, launch the after_execute method of all the processing objects
  94. $this->launch_after_execute_methods();
  95. $progress->end_progress();
  96. }
  97. /**
  98. * Receive one chunk of information form the xml parser processor and
  99. * dispatch it, following the naming rules
  100. */
  101. final public function process($data) {
  102. if (!array_key_exists($data['path'], $this->pathelements)) { // Incorrect path, must not happen
  103. throw new restore_step_exception('restore_structure_step_missing_path', $data['path']);
  104. }
  105. $element = $this->pathelements[$data['path']];
  106. $object = $element->get_processing_object();
  107. $method = $element->get_processing_method();
  108. $rdata = null;
  109. if (empty($object)) { // No processing object defined
  110. throw new restore_step_exception('restore_structure_step_missing_pobject', $object);
  111. }
  112. // Release the lock if we aren't anymore within children of it
  113. if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
  114. $this->pathlock = null;
  115. }
  116. if (is_null($this->pathlock)) { // Only dispatch if there isn't any lock
  117. $rdata = $object->$method($data['tags']); // Dispatch to proper object/method
  118. }
  119. // If the dispatched method returns SKIP_ALL_CHILDREN, we grab current path in order to
  120. // lock dispatching to any children
  121. if ($rdata === self::SKIP_ALL_CHILDREN) {
  122. // Check we haven't any previous lock
  123. if (!is_null($this->pathlock)) {
  124. throw new restore_step_exception('restore_structure_step_already_skipping', $data['path']);
  125. }
  126. // Set the lock
  127. $this->pathlock = $data['path'] . '/'; // Lock everything below current path
  128. // Continue with normal processing of return values
  129. } else if ($rdata !== null) { // If the method has returned any info, set element data to it
  130. $element->set_data($rdata);
  131. } else { // Else, put the original parsed data
  132. $element->set_data($data);
  133. }
  134. }
  135. /**
  136. * To send ids pairs to backup_ids_table and to store them into paths
  137. *
  138. * This method will send the given itemname and old/new ids to the
  139. * backup_ids_temp table, and, at the same time, will save the new id
  140. * into the corresponding restore_path_element for easier access
  141. * by children. Also will inject the known old context id for the task
  142. * in case it's going to be used for restoring files later
  143. */
  144. public function set_mapping($itemname, $oldid, $newid, $restorefiles = false, $filesctxid = null, $parentid = null) {
  145. if ($restorefiles && $parentid) {
  146. throw new restore_step_exception('set_mapping_cannot_specify_both_restorefiles_and_parentitemid');
  147. }
  148. // If we haven't specified one context for the files, use the task one
  149. if (is_null($filesctxid)) {
  150. $parentitemid = $restorefiles ? $this->task->get_old_contextid() : null;
  151. } else { // Use the specified one
  152. $parentitemid = $restorefiles ? $filesctxid : null;
  153. }
  154. // We have passed one explicit parentid, apply it
  155. $parentitemid = !is_null($parentid) ? $parentid : $parentitemid;
  156. // Let's call the low level one
  157. restore_dbops::set_backup_ids_record($this->get_restoreid(), $itemname, $oldid, $newid, $parentitemid);
  158. // Now, if the itemname matches any pathelement->name, store the latest $newid
  159. if (array_key_exists($itemname, $this->elementsoldid)) { // If present in $this->elementsoldid, is valid, put both ids
  160. $this->elementsoldid[$itemname] = $oldid;
  161. $this->elementsnewid[$itemname] = $newid;
  162. }
  163. }
  164. /**
  165. * Returns the latest (parent) old id mapped by one pathelement
  166. */
  167. public function get_old_parentid($itemname) {
  168. return array_key_exists($itemname, $this->elementsoldid) ? $this->elementsoldid[$itemname] : null;
  169. }
  170. /**
  171. * Returns the latest (parent) new id mapped by one pathelement
  172. */
  173. public function get_new_parentid($itemname) {
  174. return array_key_exists($itemname, $this->elementsnewid) ? $this->elementsnewid[$itemname] : null;
  175. }
  176. /**
  177. * Return the new id of a mapping for the given itemname
  178. *
  179. * @param string $itemname the type of item
  180. * @param int $oldid the item ID from the backup
  181. * @param mixed $ifnotfound what to return if $oldid wasnt found. Defaults to false
  182. */
  183. public function get_mappingid($itemname, $oldid, $ifnotfound = false) {
  184. $mapping = $this->get_mapping($itemname, $oldid);
  185. return $mapping ? $mapping->newitemid : $ifnotfound;
  186. }
  187. /**
  188. * Return the complete mapping from the given itemname, itemid
  189. */
  190. public function get_mapping($itemname, $oldid) {
  191. return restore_dbops::get_backup_ids_record($this->get_restoreid(), $itemname, $oldid);
  192. }
  193. /**
  194. * Add all the existing file, given their component and filearea and one backup_ids itemname to match with
  195. */
  196. public function add_related_files($component, $filearea, $mappingitemname, $filesctxid = null, $olditemid = null) {
  197. // If the current progress object is set up and ready to receive
  198. // indeterminate progress, then use it, otherwise don't. (This check is
  199. // just in case this function is ever called from somewhere not within
  200. // the execute() method here, which does set up progress like this.)
  201. $progress = $this->get_task()->get_progress();
  202. if (!$progress->is_in_progress_section() ||
  203. $progress->get_current_max() !== \core\progress\base::INDETERMINATE) {
  204. $progress = null;
  205. }
  206. $filesctxid = is_null($filesctxid) ? $this->task->get_old_contextid() : $filesctxid;
  207. $results = restore_dbops::send_files_to_pool($this->get_basepath(), $this->get_restoreid(), $component,
  208. $filearea, $filesctxid, $this->task->get_userid(), $mappingitemname, $olditemid, null, false,
  209. $progress);
  210. $resultstoadd = array();
  211. foreach ($results as $result) {
  212. $this->log($result->message, $result->level);
  213. $resultstoadd[$result->code] = true;
  214. }
  215. $this->task->add_result($resultstoadd);
  216. }
  217. /**
  218. * As far as restore structure steps are implementing restore_plugin stuff, they need to
  219. * have the parent task available for wrapping purposes (get course/context....)
  220. * @return restore_task|null
  221. */
  222. public function get_task() {
  223. return $this->task;
  224. }
  225. // Protected API starts here
  226. /**
  227. * Add plugin structure to any element in the structure restore tree
  228. *
  229. * @param string $plugintype type of plugin as defined by core_component::get_plugin_types()
  230. * @param restore_path_element $element element in the structure restore tree that
  231. * we are going to add plugin information to
  232. */
  233. protected function add_plugin_structure($plugintype, $element) {
  234. global $CFG;
  235. // Check the requested plugintype is a valid one
  236. if (!array_key_exists($plugintype, core_component::get_plugin_types($plugintype))) {
  237. throw new restore_step_exception('incorrect_plugin_type', $plugintype);
  238. }
  239. // Get all the restore path elements, looking across all the plugin dirs
  240. $pluginsdirs = core_component::get_plugin_list($plugintype);
  241. foreach ($pluginsdirs as $name => $pluginsdir) {
  242. // We need to add also backup plugin classes on restore, they may contain
  243. // some stuff used both in backup & restore
  244. $backupclassname = 'backup_' . $plugintype . '_' . $name . '_plugin';
  245. $backupfile = $pluginsdir . '/backup/moodle2/' . $backupclassname . '.class.php';
  246. if (file_exists($backupfile)) {
  247. require_once($backupfile);
  248. }
  249. // Now add restore plugin classes and prepare stuff
  250. $restoreclassname = 'restore_' . $plugintype . '_' . $name . '_plugin';
  251. $restorefile = $pluginsdir . '/backup/moodle2/' . $restoreclassname . '.class.php';
  252. if (file_exists($restorefile)) {
  253. require_once($restorefile);
  254. $restoreplugin = new $restoreclassname($plugintype, $name, $this);
  255. // Add plugin paths to the step
  256. $this->prepare_pathelements($restoreplugin->define_plugin_structure($element));
  257. }
  258. }
  259. }
  260. /**
  261. * Add subplugin structure for a given plugin to any element in the structure restore tree
  262. *
  263. * This method allows the injection of subplugins (of a specific plugin) parsing and proccessing
  264. * to any element in the restore structure.
  265. *
  266. * NOTE: Initially subplugins were only available for activities (mod), so only the
  267. * {@link restore_activity_structure_step} class had support for them, always
  268. * looking for /mod/modulenanme subplugins. This new method is a generalization of the
  269. * existing one for activities, supporting all subplugins injecting information everywhere.
  270. *
  271. * @param string $subplugintype type of subplugin as defined in plugin's db/subplugins.json.
  272. * @param restore_path_element $element element in the structure restore tree that
  273. * we are going to add subplugin information to.
  274. * @param string $plugintype type of the plugin.
  275. * @param string $pluginname name of the plugin.
  276. * @return void
  277. */
  278. protected function add_subplugin_structure($subplugintype, $element, $plugintype = null, $pluginname = null) {
  279. global $CFG;
  280. // This global declaration is required, because where we do require_once($backupfile);
  281. // That file may in turn try to do require_once($CFG->dirroot ...).
  282. // That worked in the past, we should keep it working.
  283. // Verify if this is a BC call for an activity restore. See NOTE above for this special case.
  284. if ($plugintype === null and $pluginname === null) {
  285. $plugintype = 'mod';
  286. $pluginname = $this->task->get_modulename();
  287. // TODO: Once all the calls have been changed to add both not null plugintype and pluginname, add a debugging here.
  288. }
  289. // Check the requested plugintype is a valid one.
  290. if (!array_key_exists($plugintype, core_component::get_plugin_types())) {
  291. throw new restore_step_exception('incorrect_plugin_type', $plugintype);
  292. }
  293. // Check the requested pluginname, for the specified plugintype, is a valid one.
  294. if (!array_key_exists($pluginname, core_component::get_plugin_list($plugintype))) {
  295. throw new restore_step_exception('incorrect_plugin_name', array($plugintype, $pluginname));
  296. }
  297. // Check the requested subplugintype is a valid one.
  298. $subplugins = core_component::get_subplugins("{$plugintype}_{$pluginname}");
  299. if (null === $subplugins) {
  300. throw new restore_step_exception('plugin_missing_subplugins_configuration', array($plugintype, $pluginname));
  301. }
  302. if (!array_key_exists($subplugintype, $subplugins)) {
  303. throw new restore_step_exception('incorrect_subplugin_type', $subplugintype);
  304. }
  305. // Every subplugin optionally can have a common/parent subplugin
  306. // class for shared stuff.
  307. $parentclass = 'restore_' . $plugintype . '_' . $pluginname . '_' . $subplugintype . '_subplugin';
  308. $parentfile = core_component::get_component_directory($plugintype . '_' . $pluginname) .
  309. '/backup/moodle2/' . $parentclass . '.class.php';
  310. if (file_exists($parentfile)) {
  311. require_once($parentfile);
  312. }
  313. // Get all the restore path elements, looking across all the subplugin dirs.
  314. $subpluginsdirs = core_component::get_plugin_list($subplugintype);
  315. foreach ($subpluginsdirs as $name => $subpluginsdir) {
  316. $classname = 'restore_' . $subplugintype . '_' . $name . '_subplugin';
  317. $restorefile = $subpluginsdir . '/backup/moodle2/' . $classname . '.class.php';
  318. if (file_exists($restorefile)) {
  319. require_once($restorefile);
  320. $restoresubplugin = new $classname($subplugintype, $name, $this);
  321. // Add subplugin paths to the step.
  322. $this->prepare_pathelements($restoresubplugin->define_subplugin_structure($element));
  323. }
  324. }
  325. }
  326. /**
  327. * Launch all the after_execute methods present in all the processing objects
  328. *
  329. * This method will launch all the after_execute methods that can be defined
  330. * both in restore_plugin and restore_structure_step classes
  331. *
  332. * For restore_plugin classes the name of the method to be executed will be
  333. * "after_execute_" + connection point (as far as can be multiple connection
  334. * points in the same class)
  335. *
  336. * For restore_structure_step classes is will be, simply, "after_execute". Note
  337. * that this is executed *after* the plugin ones
  338. */
  339. protected function launch_after_execute_methods() {
  340. $alreadylaunched = array(); // To avoid multiple executions
  341. foreach ($this->pathelements as $key => $pathelement) {
  342. // Get the processing object
  343. $pobject = $pathelement->get_processing_object();
  344. // Skip null processors (child of grouped ones for sure)
  345. if (is_null($pobject)) {
  346. continue;
  347. }
  348. // Skip restore structure step processors (this)
  349. if ($pobject instanceof restore_structure_step) {
  350. continue;
  351. }
  352. // Skip already launched processing objects
  353. if (in_array($pobject, $alreadylaunched, true)) {
  354. continue;
  355. }
  356. // Add processing object to array of launched ones
  357. $alreadylaunched[] = $pobject;
  358. // If the processing object has support for
  359. // launching after_execute methods, use it
  360. if (method_exists($pobject, 'launch_after_execute_methods')) {
  361. $pobject->launch_after_execute_methods();
  362. }
  363. }
  364. // Finally execute own (restore_structure_step) after_execute method
  365. $this->after_execute();
  366. }
  367. /**
  368. * Launch all the after_restore methods present in all the processing objects
  369. *
  370. * This method will launch all the after_restore methods that can be defined
  371. * both in restore_plugin class
  372. *
  373. * For restore_plugin classes the name of the method to be executed will be
  374. * "after_restore_" + connection point (as far as can be multiple connection
  375. * points in the same class)
  376. */
  377. public function launch_after_restore_methods() {
  378. $alreadylaunched = array(); // To avoid multiple executions
  379. foreach ($this->pathelements as $pathelement) {
  380. // Get the processing object
  381. $pobject = $pathelement->get_processing_object();
  382. // Skip null processors (child of grouped ones for sure)
  383. if (is_null($pobject)) {
  384. continue;
  385. }
  386. // Skip restore structure step processors (this)
  387. if ($pobject instanceof restore_structure_step) {
  388. continue;
  389. }
  390. // Skip already launched processing objects
  391. if (in_array($pobject, $alreadylaunched, true)) {
  392. continue;
  393. }
  394. // Add processing object to array of launched ones
  395. $alreadylaunched[] = $pobject;
  396. // If the processing object has support for
  397. // launching after_restore methods, use it
  398. if (method_exists($pobject, 'launch_after_restore_methods')) {
  399. $pobject->launch_after_restore_methods();
  400. }
  401. }
  402. // Finally execute own (restore_structure_step) after_restore method
  403. $this->after_restore();
  404. }
  405. /**
  406. * This method will be executed after the whole structure step have been processed
  407. *
  408. * After execution method for code needed to be executed after the whole structure
  409. * has been processed. Useful for cleaning tasks, files process and others. Simply
  410. * overwrite in in your steps if needed
  411. */
  412. protected function after_execute() {
  413. // do nothing by default
  414. }
  415. /**
  416. * This method will be executed after the rest of the restore has been processed.
  417. *
  418. * Use if you need to update IDs based on things which are restored after this
  419. * step has completed.
  420. */
  421. protected function after_restore() {
  422. // do nothing by default
  423. }
  424. /**
  425. * Prepare the pathelements for processing, looking for duplicates, applying
  426. * processing objects and other adjustments
  427. */
  428. protected function prepare_pathelements($elementsarr) {
  429. // First iteration, push them to new array, indexed by name
  430. // detecting duplicates in names or paths
  431. $names = array();
  432. $paths = array();
  433. foreach($elementsarr as $element) {
  434. if (!$element instanceof restore_path_element) {
  435. throw new restore_step_exception('restore_path_element_wrong_class', get_class($element));
  436. }
  437. if (array_key_exists($element->get_name(), $names)) {
  438. throw new restore_step_exception('restore_path_element_name_alreadyexists', $element->get_name());
  439. }
  440. if (array_key_exists($element->get_path(), $paths)) {
  441. throw new restore_step_exception('restore_path_element_path_alreadyexists', $element->get_path());
  442. }
  443. $names[$element->get_name()] = true;
  444. $paths[$element->get_path()] = $element;
  445. }
  446. // Now, for each element not having one processing object, if
  447. // not child of grouped element, assign $this (the step itself) as processing element
  448. // Note method must exist or we'll get one @restore_path_element_exception
  449. foreach ($paths as $pelement) {
  450. if ($pelement->get_processing_object() === null && !$this->grouped_parent_exists($pelement, $paths)) {
  451. $pelement->set_processing_object($this);
  452. }
  453. // Populate $elementsoldid and $elementsoldid based on available pathelements
  454. $this->elementsoldid[$pelement->get_name()] = null;
  455. $this->elementsnewid[$pelement->get_name()] = null;
  456. }
  457. // Done, add them to pathelements (dupes by key - path - are discarded)
  458. $this->pathelements = array_merge($this->pathelements, $paths);
  459. }
  460. /**
  461. * Given one pathelement, return true if grouped parent was found
  462. *
  463. * @param restore_path_element $pelement the element we are interested in.
  464. * @param restore_path_element[] $elements the elements that exist.
  465. * @return bool true if this element is inside a grouped parent.
  466. */
  467. public function grouped_parent_exists($pelement, $elements) {
  468. foreach ($elements as $element) {
  469. if ($pelement->get_path() == $element->get_path()) {
  470. continue; // Don't compare against itself.
  471. }
  472. // If element is grouped and parent of pelement, return true.
  473. if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) {
  474. return true;
  475. }
  476. }
  477. return false; // No grouped parent found.
  478. }
  479. /**
  480. * To conditionally decide if one step will be executed or no
  481. *
  482. * For steps needing to be executed conditionally, based in dynamic
  483. * conditions (at execution time vs at declaration time) you must
  484. * override this function. It will return true if the step must be
  485. * executed and false if not
  486. */
  487. protected function execute_condition() {
  488. return true;
  489. }
  490. /**
  491. * Function that will return the structure to be processed by this restore_step.
  492. * Must return one array of @restore_path_element elements
  493. */
  494. abstract protected function define_structure();
  495. }