PageRenderTime 51ms CodeModel.GetById 14ms RepoModel.GetById 1ms app.codeStats 0ms

/backup/converter/moodle1/lib.php

https://bitbucket.org/synergylearning/campusconnect
PHP | 1549 lines | 781 code | 222 blank | 546 comment | 95 complexity | 62479fbc318d968a9af2a560be448ed1 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-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. * Provides classes used by the moodle1 converter
  18. *
  19. * @package backup-convert
  20. * @subpackage moodle1
  21. * @copyright 2011 Mark Nielsen <mark@moodlerooms.com>
  22. * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23. */
  24. defined('MOODLE_INTERNAL') || die();
  25. require_once($CFG->dirroot . '/backup/converter/convertlib.php');
  26. require_once($CFG->dirroot . '/backup/util/xml/parser/progressive_parser.class.php');
  27. require_once($CFG->dirroot . '/backup/util/xml/parser/processors/grouped_parser_processor.class.php');
  28. require_once($CFG->dirroot . '/backup/util/dbops/backup_dbops.class.php');
  29. require_once($CFG->dirroot . '/backup/util/dbops/backup_controller_dbops.class.php');
  30. require_once($CFG->dirroot . '/backup/util/dbops/restore_dbops.class.php');
  31. require_once($CFG->dirroot . '/backup/util/xml/contenttransformer/xml_contenttransformer.class.php');
  32. require_once(dirname(__FILE__) . '/handlerlib.php');
  33. /**
  34. * Converter of Moodle 1.9 backup into Moodle 2.x format
  35. */
  36. class moodle1_converter extends base_converter {
  37. /** @var progressive_parser moodle.xml file parser */
  38. protected $xmlparser;
  39. /** @var moodle1_parser_processor */
  40. protected $xmlprocessor;
  41. /** @var array of {@link convert_path} to process */
  42. protected $pathelements = array();
  43. /** @var null|string the current module being processed - used to expand the MOD paths */
  44. protected $currentmod = null;
  45. /** @var null|string the current block being processed - used to expand the BLOCK paths */
  46. protected $currentblock = null;
  47. /** @var string path currently locking processing of children */
  48. protected $pathlock;
  49. /** @var int used by the serial number {@link get_nextid()} */
  50. private $nextid = 1;
  51. /**
  52. * Instructs the dispatcher to ignore all children below path processor returning it
  53. */
  54. const SKIP_ALL_CHILDREN = -991399;
  55. /**
  56. * Log a message
  57. *
  58. * @see parent::log()
  59. * @param string $message message text
  60. * @param int $level message level {@example backup::LOG_WARNING}
  61. * @param null|mixed $a additional information
  62. * @param null|int $depth the message depth
  63. * @param bool $display whether the message should be sent to the output, too
  64. */
  65. public function log($message, $level, $a = null, $depth = null, $display = false) {
  66. parent::log('(moodle1) '.$message, $level, $a, $depth, $display);
  67. }
  68. /**
  69. * Detects the Moodle 1.9 format of the backup directory
  70. *
  71. * @param string $tempdir the name of the backup directory
  72. * @return null|string backup::FORMAT_MOODLE1 if the Moodle 1.9 is detected, null otherwise
  73. */
  74. public static function detect_format($tempdir) {
  75. global $CFG;
  76. $filepath = $CFG->tempdir . '/backup/' . $tempdir . '/moodle.xml';
  77. if (file_exists($filepath)) {
  78. // looks promising, lets load some information
  79. $handle = fopen($filepath, 'r');
  80. $first_chars = fread($handle, 200);
  81. fclose($handle);
  82. // check if it has the required strings
  83. if (strpos($first_chars,'<?xml version="1.0" encoding="UTF-8"?>') !== false and
  84. strpos($first_chars,'<MOODLE_BACKUP>') !== false and
  85. strpos($first_chars,'<INFO>') !== false) {
  86. return backup::FORMAT_MOODLE1;
  87. }
  88. }
  89. return null;
  90. }
  91. /**
  92. * Initialize the instance if needed, called by the constructor
  93. *
  94. * Here we create objects we need before the execution.
  95. */
  96. protected function init() {
  97. // ask your mother first before going out playing with toys
  98. parent::init();
  99. $this->log('initializing '.$this->get_name().' converter', backup::LOG_INFO);
  100. // good boy, prepare XML parser and processor
  101. $this->log('setting xml parser', backup::LOG_DEBUG, null, 1);
  102. $this->xmlparser = new progressive_parser();
  103. $this->xmlparser->set_file($this->get_tempdir_path() . '/moodle.xml');
  104. $this->log('setting xml processor', backup::LOG_DEBUG, null, 1);
  105. $this->xmlprocessor = new moodle1_parser_processor($this);
  106. $this->xmlparser->set_processor($this->xmlprocessor);
  107. // make sure that MOD and BLOCK paths are visited
  108. $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/MODULES/MOD');
  109. $this->xmlprocessor->add_path('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK');
  110. // register the conversion handlers
  111. foreach (moodle1_handlers_factory::get_handlers($this) as $handler) {
  112. $this->log('registering handler', backup::LOG_DEBUG, get_class($handler), 1);
  113. $this->register_handler($handler, $handler->get_paths());
  114. }
  115. }
  116. /**
  117. * Converts the contents of the tempdir into the target format in the workdir
  118. */
  119. protected function execute() {
  120. $this->log('creating the stash storage', backup::LOG_DEBUG);
  121. $this->create_stash_storage();
  122. $this->log('parsing moodle.xml starts', backup::LOG_DEBUG);
  123. $this->xmlparser->process();
  124. $this->log('parsing moodle.xml done', backup::LOG_DEBUG);
  125. $this->log('dropping the stash storage', backup::LOG_DEBUG);
  126. $this->drop_stash_storage();
  127. }
  128. /**
  129. * Register a handler for the given path elements
  130. */
  131. protected function register_handler(moodle1_handler $handler, array $elements) {
  132. // first iteration, push them to new array, indexed by name
  133. // to detect duplicates in names or paths
  134. $names = array();
  135. $paths = array();
  136. foreach($elements as $element) {
  137. if (!$element instanceof convert_path) {
  138. throw new convert_exception('path_element_wrong_class', get_class($element));
  139. }
  140. if (array_key_exists($element->get_name(), $names)) {
  141. throw new convert_exception('path_element_name_alreadyexists', $element->get_name());
  142. }
  143. if (array_key_exists($element->get_path(), $paths)) {
  144. throw new convert_exception('path_element_path_alreadyexists', $element->get_path());
  145. }
  146. $names[$element->get_name()] = true;
  147. $paths[$element->get_path()] = $element;
  148. }
  149. // now, for each element not having a processing object yet, assign the handler
  150. // if the element is not a memeber of a group
  151. foreach($paths as $key => $element) {
  152. if (is_null($element->get_processing_object()) and !$this->grouped_parent_exists($element, $paths)) {
  153. $paths[$key]->set_processing_object($handler);
  154. }
  155. // add the element path to the processor
  156. $this->xmlprocessor->add_path($element->get_path(), $element->is_grouped());
  157. }
  158. // done, store the paths (duplicates by path are discarded)
  159. $this->pathelements = array_merge($this->pathelements, $paths);
  160. // remove the injected plugin name element from the MOD and BLOCK paths
  161. // and register such collapsed path, too
  162. foreach ($elements as $element) {
  163. $path = $element->get_path();
  164. $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/MODULES\/MOD\/(\w+)\//', '/MOODLE_BACKUP/COURSE/MODULES/MOD/', $path);
  165. $path = preg_replace('/^\/MOODLE_BACKUP\/COURSE\/BLOCKS\/BLOCK\/(\w+)\//', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/', $path);
  166. if (!empty($path) and $path != $element->get_path()) {
  167. $this->xmlprocessor->add_path($path, false);
  168. }
  169. }
  170. }
  171. /**
  172. * Helper method used by {@link self::register_handler()}
  173. *
  174. * @param convert_path $pelement path element
  175. * @param array of convert_path instances
  176. * @return bool true if grouped parent was found, false otherwise
  177. */
  178. protected function grouped_parent_exists($pelement, $elements) {
  179. foreach ($elements as $element) {
  180. if ($pelement->get_path() == $element->get_path()) {
  181. // don't compare against itself
  182. continue;
  183. }
  184. // if the element is grouped and it is a parent of pelement, return true
  185. if ($element->is_grouped() and strpos($pelement->get_path() . '/', $element->get_path()) === 0) {
  186. return true;
  187. }
  188. }
  189. // no grouped parent found
  190. return false;
  191. }
  192. /**
  193. * Process the data obtained from the XML parser processor
  194. *
  195. * This methods receives one chunk of information from the XML parser
  196. * processor and dispatches it, following the naming rules.
  197. * We are expanding the modules and blocks paths here to include the plugin's name.
  198. *
  199. * @param array $data
  200. */
  201. public function process_chunk($data) {
  202. $path = $data['path'];
  203. // expand the MOD paths so that they contain the module name
  204. if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
  205. $this->currentmod = strtoupper($data['tags']['MODTYPE']);
  206. $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
  207. } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
  208. $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
  209. }
  210. // expand the BLOCK paths so that they contain the module name
  211. if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
  212. $this->currentblock = strtoupper($data['tags']['NAME']);
  213. $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
  214. } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
  215. $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path);
  216. }
  217. if ($path !== $data['path']) {
  218. if (!array_key_exists($path, $this->pathelements)) {
  219. // no handler registered for the transformed MOD or BLOCK path
  220. $this->log('no handler attached', backup::LOG_WARNING, $path);
  221. return;
  222. } else {
  223. // pretend as if the original $data contained the tranformed path
  224. $data['path'] = $path;
  225. }
  226. }
  227. if (!array_key_exists($data['path'], $this->pathelements)) {
  228. // path added to the processor without the handler
  229. throw new convert_exception('missing_path_handler', $data['path']);
  230. }
  231. $element = $this->pathelements[$data['path']];
  232. $object = $element->get_processing_object();
  233. $method = $element->get_processing_method();
  234. $returned = null; // data returned by the processing method, if any
  235. if (empty($object)) {
  236. throw new convert_exception('missing_processing_object', null, $data['path']);
  237. }
  238. // release the lock if we aren't anymore within children of it
  239. if (!is_null($this->pathlock) and strpos($data['path'], $this->pathlock) === false) {
  240. $this->pathlock = null;
  241. }
  242. // if the path is not locked, apply the element's recipes and dispatch
  243. // the cooked tags to the processing method
  244. if (is_null($this->pathlock)) {
  245. $rawdatatags = $data['tags'];
  246. $data['tags'] = $element->apply_recipes($data['tags']);
  247. // if the processing method exists, give it a chance to modify data
  248. if (method_exists($object, $method)) {
  249. $returned = $object->$method($data['tags'], $rawdatatags);
  250. }
  251. }
  252. // if the dispatched method returned SKIP_ALL_CHILDREN, remember the current path
  253. // and lock it so that its children are not dispatched
  254. if ($returned === self::SKIP_ALL_CHILDREN) {
  255. // check we haven't any previous lock
  256. if (!is_null($this->pathlock)) {
  257. throw new convert_exception('already_locked_path', $data['path']);
  258. }
  259. // set the lock - nothing below the current path will be dispatched
  260. $this->pathlock = $data['path'] . '/';
  261. // if the method has returned any info, set element data to it
  262. } else if (!is_null($returned)) {
  263. $element->set_tags($returned);
  264. // use just the cooked parsed data otherwise
  265. } else {
  266. $element->set_tags($data['tags']);
  267. }
  268. }
  269. /**
  270. * Executes operations required at the start of a watched path
  271. *
  272. * For MOD and BLOCK paths, this is supported only for the sub-paths, not the root
  273. * module/block element. For the illustration:
  274. *
  275. * You CAN'T attach on_xxx_start() listener to a path like
  276. * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP because the <MOD> must
  277. * be processed first in {@link self::process_chunk()} where $this->currentmod
  278. * is set.
  279. *
  280. * You CAN attach some on_xxx_start() listener to a path like
  281. * /MOODLE_BACKUP/COURSE/MODULES/MOD/WORKSHOP/SUBMISSIONS because it is
  282. * a sub-path under <MOD> and we have $this->currentmod already set when the
  283. * <SUBMISSIONS> is reached.
  284. *
  285. * @param string $path in the original file
  286. */
  287. public function path_start_reached($path) {
  288. if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
  289. $this->currentmod = null;
  290. $forbidden = true;
  291. } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
  292. // expand the MOD paths so that they contain the module name
  293. $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
  294. }
  295. if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
  296. $this->currentblock = null;
  297. $forbidden = true;
  298. } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
  299. // expand the BLOCK paths so that they contain the module name
  300. $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path);
  301. }
  302. if (empty($this->pathelements[$path])) {
  303. return;
  304. }
  305. $element = $this->pathelements[$path];
  306. $pobject = $element->get_processing_object();
  307. $method = $element->get_start_method();
  308. if (method_exists($pobject, $method)) {
  309. if (empty($forbidden)) {
  310. $pobject->$method();
  311. } else {
  312. // this path is not supported because we do not know the module/block yet
  313. throw new coding_exception('Attaching the on-start event listener to the root MOD or BLOCK element is forbidden.');
  314. }
  315. }
  316. }
  317. /**
  318. * Executes operations required at the end of a watched path
  319. *
  320. * @param string $path in the original file
  321. */
  322. public function path_end_reached($path) {
  323. // expand the MOD paths so that they contain the current module name
  324. if ($path === '/MOODLE_BACKUP/COURSE/MODULES/MOD') {
  325. $path = '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod;
  326. } else if (strpos($path, '/MOODLE_BACKUP/COURSE/MODULES/MOD') === 0) {
  327. $path = str_replace('/MOODLE_BACKUP/COURSE/MODULES/MOD', '/MOODLE_BACKUP/COURSE/MODULES/MOD/' . $this->currentmod, $path);
  328. }
  329. // expand the BLOCK paths so that they contain the module name
  330. if ($path === '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') {
  331. $path = '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock;
  332. } else if (strpos($path, '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK') === 0) {
  333. $path = str_replace('/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK', '/MOODLE_BACKUP/COURSE/BLOCKS/BLOCK/' . $this->currentblock, $path);
  334. }
  335. if (empty($this->pathelements[$path])) {
  336. return;
  337. }
  338. $element = $this->pathelements[$path];
  339. $pobject = $element->get_processing_object();
  340. $method = $element->get_end_method();
  341. $tags = $element->get_tags();
  342. if (method_exists($pobject, $method)) {
  343. $pobject->$method($tags);
  344. }
  345. }
  346. /**
  347. * Creates the temporary storage for stashed data
  348. *
  349. * This implementation uses backup_ids_temp table.
  350. */
  351. public function create_stash_storage() {
  352. backup_controller_dbops::create_backup_ids_temp_table($this->get_id());
  353. }
  354. /**
  355. * Drops the temporary storage of stashed data
  356. *
  357. * This implementation uses backup_ids_temp table.
  358. */
  359. public function drop_stash_storage() {
  360. backup_controller_dbops::drop_backup_ids_temp_table($this->get_id());
  361. }
  362. /**
  363. * Stores some information for later processing
  364. *
  365. * This implementation uses backup_ids_temp table to store data. Make
  366. * sure that the $stashname + $itemid combo is unique.
  367. *
  368. * @param string $stashname name of the stash
  369. * @param mixed $info information to stash
  370. * @param int $itemid optional id for multiple infos within the same stashname
  371. */
  372. public function set_stash($stashname, $info, $itemid = 0) {
  373. try {
  374. restore_dbops::set_backup_ids_record($this->get_id(), $stashname, $itemid, 0, null, $info);
  375. } catch (dml_exception $e) {
  376. throw new moodle1_convert_storage_exception('unable_to_restore_stash', null, $e->getMessage());
  377. }
  378. }
  379. /**
  380. * Restores a given stash stored previously by {@link self::set_stash()}
  381. *
  382. * @param string $stashname name of the stash
  383. * @param int $itemid optional id for multiple infos within the same stashname
  384. * @throws moodle1_convert_empty_storage_exception if the info has not been stashed previously
  385. * @return mixed stashed data
  386. */
  387. public function get_stash($stashname, $itemid = 0) {
  388. $record = restore_dbops::get_backup_ids_record($this->get_id(), $stashname, $itemid);
  389. if (empty($record)) {
  390. throw new moodle1_convert_empty_storage_exception('required_not_stashed_data', array($stashname, $itemid));
  391. } else {
  392. return $record->info;
  393. }
  394. }
  395. /**
  396. * Restores a given stash or returns the given default if there is no such stash
  397. *
  398. * @param string $stashname name of the stash
  399. * @param int $itemid optional id for multiple infos within the same stashname
  400. * @param mixed $default information to return if the info has not been stashed previously
  401. * @return mixed stashed data or the default value
  402. */
  403. public function get_stash_or_default($stashname, $itemid = 0, $default = null) {
  404. try {
  405. return $this->get_stash($stashname, $itemid);
  406. } catch (moodle1_convert_empty_storage_exception $e) {
  407. return $default;
  408. }
  409. }
  410. /**
  411. * Returns the list of existing stashes
  412. *
  413. * @return array
  414. */
  415. public function get_stash_names() {
  416. global $DB;
  417. $search = array(
  418. 'backupid' => $this->get_id(),
  419. );
  420. return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemname'));
  421. }
  422. /**
  423. * Returns the list of stashed $itemids in the given stash
  424. *
  425. * @param string $stashname
  426. * @return array
  427. */
  428. public function get_stash_itemids($stashname) {
  429. global $DB;
  430. $search = array(
  431. 'backupid' => $this->get_id(),
  432. 'itemname' => $stashname
  433. );
  434. return array_keys($DB->get_records('backup_ids_temp', $search, '', 'itemid'));
  435. }
  436. /**
  437. * Generates an artificial context id
  438. *
  439. * Moodle 1.9 backups do not contain any context information. But we need them
  440. * in Moodle 2.x format so here we generate fictive context id for every given
  441. * context level + instance combo.
  442. *
  443. * CONTEXT_SYSTEM and CONTEXT_COURSE ignore the $instance as they represent a
  444. * single system or the course being restored.
  445. *
  446. * @see context_system::instance()
  447. * @see context_course::instance()
  448. * @param int $level the context level, like CONTEXT_COURSE or CONTEXT_MODULE
  449. * @param int $instance the instance id, for example $course->id for courses or $cm->id for activity modules
  450. * @return int the context id
  451. */
  452. public function get_contextid($level, $instance = 0) {
  453. $stashname = 'context' . $level;
  454. if ($level == CONTEXT_SYSTEM or $level == CONTEXT_COURSE) {
  455. $instance = 0;
  456. }
  457. try {
  458. // try the previously stashed id
  459. return $this->get_stash($stashname, $instance);
  460. } catch (moodle1_convert_empty_storage_exception $e) {
  461. // this context level + instance is required for the first time
  462. $newid = $this->get_nextid();
  463. $this->set_stash($stashname, $newid, $instance);
  464. return $newid;
  465. }
  466. }
  467. /**
  468. * Simple autoincrement generator
  469. *
  470. * @return int the next number in a row of numbers
  471. */
  472. public function get_nextid() {
  473. return $this->nextid++;
  474. }
  475. /**
  476. * Creates and returns new instance of the file manager
  477. *
  478. * @param int $contextid the default context id of the files being migrated
  479. * @param string $component the default component name of the files being migrated
  480. * @param string $filearea the default file area of the files being migrated
  481. * @param int $itemid the default item id of the files being migrated
  482. * @param int $userid initial user id of the files being migrated
  483. * @return moodle1_file_manager
  484. */
  485. public function get_file_manager($contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) {
  486. return new moodle1_file_manager($this, $contextid, $component, $filearea, $itemid, $userid);
  487. }
  488. /**
  489. * Creates and returns new instance of the inforef manager
  490. *
  491. * @param string $name the name of the annotator (like course, section, activity, block)
  492. * @param int $id the id of the annotator if required
  493. * @return moodle1_inforef_manager
  494. */
  495. public function get_inforef_manager($name, $id = 0) {
  496. return new moodle1_inforef_manager($this, $name, $id);
  497. }
  498. /**
  499. * Migrates all course files referenced from the hypertext using the given filemanager
  500. *
  501. * This is typically used to convert images embedded into the intro fields.
  502. *
  503. * @param string $text hypertext containing $@FILEPHP@$ referenced
  504. * @param moodle1_file_manager $fileman file manager to use for the file migration
  505. * @return string the original $text with $@FILEPHP@$ references replaced with the new @@PLUGINFILE@@
  506. */
  507. public static function migrate_referenced_files($text, moodle1_file_manager $fileman) {
  508. $files = self::find_referenced_files($text);
  509. if (!empty($files)) {
  510. foreach ($files as $file) {
  511. try {
  512. $fileman->migrate_file('course_files'.$file, dirname($file));
  513. } catch (moodle1_convert_exception $e) {
  514. // file probably does not exist
  515. $fileman->log('error migrating file', backup::LOG_WARNING, 'course_files'.$file);
  516. }
  517. }
  518. $text = self::rewrite_filephp_usage($text, $files);
  519. }
  520. return $text;
  521. }
  522. /**
  523. * Detects all links to file.php encoded via $@FILEPHP@$ and returns the files to migrate
  524. *
  525. * @see self::migrate_referenced_files()
  526. * @param string $text
  527. * @return array
  528. */
  529. public static function find_referenced_files($text) {
  530. $files = array();
  531. if (empty($text) or is_numeric($text)) {
  532. return $files;
  533. }
  534. $matches = array();
  535. $pattern = '|(["\'])(\$@FILEPHP@\$.+?)\1|';
  536. $result = preg_match_all($pattern, $text, $matches);
  537. if ($result === false) {
  538. throw new moodle1_convert_exception('error_while_searching_for_referenced_files');
  539. }
  540. if ($result == 0) {
  541. return $files;
  542. }
  543. foreach ($matches[2] as $match) {
  544. $file = str_replace(array('$@FILEPHP@$', '$@SLASH@$', '$@FORCEDOWNLOAD@$'), array('', '/', ''), $match);
  545. if ($file === clean_param($file, PARAM_PATH)) {
  546. $files[] = rawurldecode($file);
  547. }
  548. }
  549. return array_unique($files);
  550. }
  551. /**
  552. * Given the list of migrated files, rewrites references to them from $@FILEPHP@$ form to the @@PLUGINFILE@@ one
  553. *
  554. * @see self::migrate_referenced_files()
  555. * @param string $text
  556. * @param array $files
  557. * @return string
  558. */
  559. public static function rewrite_filephp_usage($text, array $files) {
  560. foreach ($files as $file) {
  561. // Expect URLs properly encoded by default.
  562. $parts = explode('/', $file);
  563. $encoded = implode('/', array_map('rawurlencode', $parts));
  564. $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $encoded);
  565. $text = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text);
  566. $text = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text);
  567. // Add support for URLs without any encoding.
  568. $fileref = '$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $file);
  569. $text = str_replace($fileref.'$@FORCEDOWNLOAD@$', '@@PLUGINFILE@@'.$encoded.'?forcedownload=1', $text);
  570. $text = str_replace($fileref, '@@PLUGINFILE@@'.$encoded, $text);
  571. }
  572. return $text;
  573. }
  574. /**
  575. * @see parent::description()
  576. */
  577. public static function description() {
  578. return array(
  579. 'from' => backup::FORMAT_MOODLE1,
  580. 'to' => backup::FORMAT_MOODLE,
  581. 'cost' => 10,
  582. );
  583. }
  584. }
  585. /**
  586. * Exception thrown by this converter
  587. */
  588. class moodle1_convert_exception extends convert_exception {
  589. }
  590. /**
  591. * Exception thrown by the temporary storage subsystem of moodle1_converter
  592. */
  593. class moodle1_convert_storage_exception extends moodle1_convert_exception {
  594. }
  595. /**
  596. * Exception thrown by the temporary storage subsystem of moodle1_converter
  597. */
  598. class moodle1_convert_empty_storage_exception extends moodle1_convert_exception {
  599. }
  600. /**
  601. * XML parser processor used for processing parsed moodle.xml
  602. */
  603. class moodle1_parser_processor extends grouped_parser_processor {
  604. /** @var moodle1_converter */
  605. protected $converter;
  606. public function __construct(moodle1_converter $converter) {
  607. $this->converter = $converter;
  608. parent::__construct();
  609. }
  610. /**
  611. * Provides NULL decoding
  612. *
  613. * Note that we do not decode $@FILEPHP@$ and friends here as we are going to write them
  614. * back immediately into another XML file.
  615. */
  616. public function process_cdata($cdata) {
  617. if ($cdata === '$@NULL@$') {
  618. return null;
  619. }
  620. return $cdata;
  621. }
  622. /**
  623. * Dispatches the data chunk to the converter class
  624. *
  625. * @param array $data the chunk of parsed data
  626. */
  627. protected function dispatch_chunk($data) {
  628. $this->converter->process_chunk($data);
  629. }
  630. /**
  631. * Informs the converter at the start of a watched path
  632. *
  633. * @param string $path
  634. */
  635. protected function notify_path_start($path) {
  636. $this->converter->path_start_reached($path);
  637. }
  638. /**
  639. * Informs the converter at the end of a watched path
  640. *
  641. * @param string $path
  642. */
  643. protected function notify_path_end($path) {
  644. $this->converter->path_end_reached($path);
  645. }
  646. }
  647. /**
  648. * XML transformer that modifies the content of the files being written during the conversion
  649. *
  650. * @see backup_xml_transformer
  651. */
  652. class moodle1_xml_transformer extends xml_contenttransformer {
  653. /**
  654. * Modify the content before it is writter to a file
  655. *
  656. * @param string|mixed $content
  657. */
  658. public function process($content) {
  659. // the content should be a string. If array or object is given, try our best recursively
  660. // but inform the developer
  661. if (is_array($content)) {
  662. debugging('Moodle1 XML transformer should not process arrays but plain content always', DEBUG_DEVELOPER);
  663. foreach($content as $key => $plaincontent) {
  664. $content[$key] = $this->process($plaincontent);
  665. }
  666. return $content;
  667. } else if (is_object($content)) {
  668. debugging('Moodle1 XML transformer should not process objects but plain content always', DEBUG_DEVELOPER);
  669. foreach((array)$content as $key => $plaincontent) {
  670. $content[$key] = $this->process($plaincontent);
  671. }
  672. return (object)$content;
  673. }
  674. // try to deal with some trivial cases first
  675. if (is_null($content)) {
  676. return '$@NULL@$';
  677. } else if ($content === '') {
  678. return '';
  679. } else if (is_numeric($content)) {
  680. return $content;
  681. } else if (strlen($content) < 32) {
  682. return $content;
  683. }
  684. return $content;
  685. }
  686. }
  687. /**
  688. * Class representing a path to be converted from XML file
  689. *
  690. * This was created as a copy of {@link restore_path_element} and should be refactored
  691. * probably.
  692. */
  693. class convert_path {
  694. /** @var string name of the element */
  695. protected $name;
  696. /** @var string path within the XML file this element will handle */
  697. protected $path;
  698. /** @var bool flag to define if this element will get child ones grouped or no */
  699. protected $grouped;
  700. /** @var object object instance in charge of processing this element. */
  701. protected $pobject = null;
  702. /** @var string the name of the processing method */
  703. protected $pmethod = null;
  704. /** @var string the name of the path start event handler */
  705. protected $smethod = null;
  706. /** @var string the name of the path end event handler */
  707. protected $emethod = null;
  708. /** @var mixed last data read for this element or returned data by processing method */
  709. protected $tags = null;
  710. /** @var array of deprecated fields that are dropped */
  711. protected $dropfields = array();
  712. /** @var array of fields renaming */
  713. protected $renamefields = array();
  714. /** @var array of new fields to add and their initial values */
  715. protected $newfields = array();
  716. /**
  717. * Constructor
  718. *
  719. * @param string $name name of the element
  720. * @param string $path path of the element
  721. * @param array $recipe basic description of the structure conversion
  722. * @param bool $grouped to gather information in grouped mode or no
  723. */
  724. public function __construct($name, $path, array $recipe = array(), $grouped = false) {
  725. $this->validate_name($name);
  726. $this->name = $name;
  727. $this->path = $path;
  728. $this->grouped = $grouped;
  729. // set the default method names
  730. $this->set_processing_method('process_' . $name);
  731. $this->set_start_method('on_'.$name.'_start');
  732. $this->set_end_method('on_'.$name.'_end');
  733. if ($grouped and !empty($recipe)) {
  734. throw new convert_path_exception('recipes_not_supported_for_grouped_elements');
  735. }
  736. if (isset($recipe['dropfields']) and is_array($recipe['dropfields'])) {
  737. $this->set_dropped_fields($recipe['dropfields']);
  738. }
  739. if (isset($recipe['renamefields']) and is_array($recipe['renamefields'])) {
  740. $this->set_renamed_fields($recipe['renamefields']);
  741. }
  742. if (isset($recipe['newfields']) and is_array($recipe['newfields'])) {
  743. $this->set_new_fields($recipe['newfields']);
  744. }
  745. }
  746. /**
  747. * Validates and sets the given processing object
  748. *
  749. * @param object $pobject processing object, must provide a method to be called
  750. */
  751. public function set_processing_object($pobject) {
  752. $this->validate_pobject($pobject);
  753. $this->pobject = $pobject;
  754. }
  755. /**
  756. * Sets the name of the processing method
  757. *
  758. * @param string $pmethod
  759. */
  760. public function set_processing_method($pmethod) {
  761. $this->pmethod = $pmethod;
  762. }
  763. /**
  764. * Sets the name of the path start event listener
  765. *
  766. * @param string $smethod
  767. */
  768. public function set_start_method($smethod) {
  769. $this->smethod = $smethod;
  770. }
  771. /**
  772. * Sets the name of the path end event listener
  773. *
  774. * @param string $emethod
  775. */
  776. public function set_end_method($emethod) {
  777. $this->emethod = $emethod;
  778. }
  779. /**
  780. * Sets the element tags
  781. *
  782. * @param array $tags
  783. */
  784. public function set_tags($tags) {
  785. $this->tags = $tags;
  786. }
  787. /**
  788. * Sets the list of deprecated fields to drop
  789. *
  790. * @param array $fields
  791. */
  792. public function set_dropped_fields(array $fields) {
  793. $this->dropfields = $fields;
  794. }
  795. /**
  796. * Sets the required new names of the current fields
  797. *
  798. * @param array $fields (string)$currentname => (string)$newname
  799. */
  800. public function set_renamed_fields(array $fields) {
  801. $this->renamefields = $fields;
  802. }
  803. /**
  804. * Sets the new fields and their values
  805. *
  806. * @param array $fields (string)$field => (mixed)value
  807. */
  808. public function set_new_fields(array $fields) {
  809. $this->newfields = $fields;
  810. }
  811. /**
  812. * Cooks the parsed tags data by applying known recipes
  813. *
  814. * Recipes are used for common trivial operations like adding new fields
  815. * or renaming fields. The handler's processing method receives cooked
  816. * data.
  817. *
  818. * @param array $data the contents of the element
  819. * @return array
  820. */
  821. public function apply_recipes(array $data) {
  822. $cooked = array();
  823. foreach ($data as $name => $value) {
  824. // lower case rocks!
  825. $name = strtolower($name);
  826. if (is_array($value)) {
  827. if ($this->is_grouped()) {
  828. $value = $this->apply_recipes($value);
  829. } else {
  830. throw new convert_path_exception('non_grouped_path_with_array_values');
  831. }
  832. }
  833. // drop legacy fields
  834. if (in_array($name, $this->dropfields)) {
  835. continue;
  836. }
  837. // fields renaming
  838. if (array_key_exists($name, $this->renamefields)) {
  839. $name = $this->renamefields[$name];
  840. }
  841. $cooked[$name] = $value;
  842. }
  843. // adding new fields
  844. foreach ($this->newfields as $name => $value) {
  845. $cooked[$name] = $value;
  846. }
  847. return $cooked;
  848. }
  849. /**
  850. * @return string the element given name
  851. */
  852. public function get_name() {
  853. return $this->name;
  854. }
  855. /**
  856. * @return string the path to the element
  857. */
  858. public function get_path() {
  859. return $this->path;
  860. }
  861. /**
  862. * @return bool flag to define if this element will get child ones grouped or no
  863. */
  864. public function is_grouped() {
  865. return $this->grouped;
  866. }
  867. /**
  868. * @return object the processing object providing the processing method
  869. */
  870. public function get_processing_object() {
  871. return $this->pobject;
  872. }
  873. /**
  874. * @return string the name of the method to call to process the element
  875. */
  876. public function get_processing_method() {
  877. return $this->pmethod;
  878. }
  879. /**
  880. * @return string the name of the path start event listener
  881. */
  882. public function get_start_method() {
  883. return $this->smethod;
  884. }
  885. /**
  886. * @return string the name of the path end event listener
  887. */
  888. public function get_end_method() {
  889. return $this->emethod;
  890. }
  891. /**
  892. * @return mixed the element data
  893. */
  894. public function get_tags() {
  895. return $this->tags;
  896. }
  897. /// end of public API //////////////////////////////////////////////////////
  898. /**
  899. * Makes sure the given name is a valid element name
  900. *
  901. * Note it may look as if we used exceptions for code flow control here. That's not the case
  902. * as we actually validate the code, not the user data. And the code is supposed to be
  903. * correct.
  904. *
  905. * @param string @name the element given name
  906. * @throws convert_path_exception
  907. * @return void
  908. */
  909. protected function validate_name($name) {
  910. // Validate various name constraints, throwing exception if needed
  911. if (empty($name)) {
  912. throw new convert_path_exception('convert_path_emptyname', $name);
  913. }
  914. if (preg_replace('/\s/', '', $name) != $name) {
  915. throw new convert_path_exception('convert_path_whitespace', $name);
  916. }
  917. if (preg_replace('/[^\x30-\x39\x41-\x5a\x5f\x61-\x7a]/', '', $name) != $name) {
  918. throw new convert_path_exception('convert_path_notasciiname', $name);
  919. }
  920. }
  921. /**
  922. * Makes sure that the given object is a valid processing object
  923. *
  924. * The processing object must be an object providing at least element's processing method
  925. * or path-reached-end event listener or path-reached-start listener method.
  926. *
  927. * Note it may look as if we used exceptions for code flow control here. That's not the case
  928. * as we actually validate the code, not the user data. And the code is supposed to be
  929. * correct.
  930. *
  931. * @param object $pobject
  932. * @throws convert_path_exception
  933. * @return void
  934. */
  935. protected function validate_pobject($pobject) {
  936. if (!is_object($pobject)) {
  937. throw new convert_path_exception('convert_path_no_object', get_class($pobject));
  938. }
  939. if (!method_exists($pobject, $this->get_processing_method()) and
  940. !method_exists($pobject, $this->get_end_method()) and
  941. !method_exists($pobject, $this->get_start_method())) {
  942. throw new convert_path_exception('convert_path_missing_method', get_class($pobject));
  943. }
  944. }
  945. }
  946. /**
  947. * Exception being thrown by {@link convert_path} methods
  948. */
  949. class convert_path_exception extends moodle_exception {
  950. /**
  951. * Constructor
  952. *
  953. * @param string $errorcode key for the corresponding error string
  954. * @param mixed $a extra words and phrases that might be required by the error string
  955. * @param string $debuginfo optional debugging information
  956. */
  957. public function __construct($errorcode, $a = null, $debuginfo = null) {
  958. parent::__construct($errorcode, '', '', $a, $debuginfo);
  959. }
  960. }
  961. /**
  962. * The class responsible for files migration
  963. *
  964. * The files in Moodle 1.9 backup are stored in moddata, user_files, group_files,
  965. * course_files and site_files folders.
  966. */
  967. class moodle1_file_manager implements loggable {
  968. /** @var moodle1_converter instance we serve to */
  969. public $converter;
  970. /** @var int context id of the files being migrated */
  971. public $contextid;
  972. /** @var string component name of the files being migrated */
  973. public $component;
  974. /** @var string file area of the files being migrated */
  975. public $filearea;
  976. /** @var int item id of the files being migrated */
  977. public $itemid = 0;
  978. /** @var int user id */
  979. public $userid;
  980. /** @var string the root of the converter temp directory */
  981. protected $basepath;
  982. /** @var array of file ids that were migrated by this instance */
  983. protected $fileids = array();
  984. /**
  985. * Constructor optionally accepting some default values for the migrated files
  986. *
  987. * @param moodle1_converter $converter the converter instance we serve to
  988. * @param int $contextid initial context id of the files being migrated
  989. * @param string $component initial component name of the files being migrated
  990. * @param string $filearea initial file area of the files being migrated
  991. * @param int $itemid initial item id of the files being migrated
  992. * @param int $userid initial user id of the files being migrated
  993. */
  994. public function __construct(moodle1_converter $converter, $contextid = null, $component = null, $filearea = null, $itemid = 0, $userid = null) {
  995. // set the initial destination of the migrated files
  996. $this->converter = $converter;
  997. $this->contextid = $contextid;
  998. $this->component = $component;
  999. $this->filearea = $filearea;
  1000. $this->itemid = $itemid;
  1001. $this->userid = $userid;
  1002. // set other useful bits
  1003. $this->basepath = $converter->get_tempdir_path();
  1004. }
  1005. /**
  1006. * Migrates one given file stored on disk
  1007. *
  1008. * @param string $sourcepath the path to the source local file within the backup archive {@example 'moddata/foobar/file.ext'}
  1009. * @param string $filepath the file path of the migrated file, defaults to the root directory '/' {@example '/sub/dir/'}
  1010. * @param string $filename the name of the migrated file, defaults to the same as the source file has
  1011. * @param int $sortorder the sortorder of the file (main files have sortorder set to 1)
  1012. * @param int $timecreated override the timestamp of when the migrated file should appear as created
  1013. * @param int $timemodified override the timestamp of when the migrated file should appear as modified
  1014. * @return int id of the migrated file
  1015. */
  1016. public function migrate_file($sourcepath, $filepath = '/', $filename = null, $sortorder = 0, $timecreated = null, $timemodified = null) {
  1017. // Normalise Windows paths a bit.
  1018. $sourcepath = str_replace('\\', '/', $sourcepath);
  1019. // PARAM_PATH must not be used on full OS path!
  1020. if ($sourcepath !== clean_param($sourcepath, PARAM_PATH)) {
  1021. throw new moodle1_convert_exception('file_invalid_path', $sourcepath);
  1022. }
  1023. $sourcefullpath = $this->basepath.'/'.$sourcepath;
  1024. if (!is_readable($sourcefullpath)) {
  1025. throw new moodle1_convert_exception('file_not_readable', $sourcefullpath);
  1026. }
  1027. // sanitize filepath
  1028. if (empty($filepath)) {
  1029. $filepath = '/';
  1030. }
  1031. if (substr($filepath, -1) !== '/') {
  1032. $filepath .= '/';
  1033. }
  1034. $filepath = clean_param($filepath, PARAM_PATH);
  1035. if (core_text::strlen($filepath) > 255) {
  1036. throw new moodle1_convert_exception('file_path_longer_than_255_chars');
  1037. }
  1038. if (is_null($filename)) {
  1039. $filename = basename($sourcefullpath);
  1040. }
  1041. $filename = clean_param($filename, PARAM_FILE);
  1042. if ($filename === '') {
  1043. throw new moodle1_convert_exception('unsupported_chars_in_filename');
  1044. }
  1045. if (is_null($timecreated)) {
  1046. $timecreated = filectime($sourcefullpath);
  1047. }
  1048. if (is_null($timemodified)) {
  1049. $timemodified = filemtime($sourcefullpath);
  1050. }
  1051. $filerecord = $this->make_file_record(array(
  1052. 'filepath' => $filepath,
  1053. 'filename' => $filename,
  1054. 'sortorder' => $sortorder,
  1055. 'mimetype' => mimeinfo('type', $sourcefullpath),
  1056. 'timecreated' => $timecreated,
  1057. 'timemodified' => $timemodified,
  1058. ));
  1059. list($filerecord['contenthash'], $filerecord['filesize'], $newfile) = $this->add_file_to_pool($sourcefullpath);
  1060. $this->stash_file($filerecord);
  1061. return $filerecord['id'];
  1062. }
  1063. /**
  1064. * Migrates all files in the given directory
  1065. *
  1066. * @param string $rootpath path within the backup archive to the root directory containing the files {@example 'course_files'}
  1067. * @param string $relpath relative path used during the recursion - do not provide when calling this!
  1068. * @return array ids of the migrated files, empty array if the $rootpath not found
  1069. */
  1070. public function migrate_directory($rootpath, $relpath='/') {
  1071. // Check the trailing slash in the $rootpath
  1072. if (substr($rootpath, -1) === '/') {
  1073. debugging('moodle1_file_manager::migrate_directory() expects $rootpath without the trailing slash', DEBUG_DEVELOPER);
  1074. $rootpath = substr($rootpath, 0, strlen($rootpath) - 1);
  1075. }
  1076. if (!file_exists($this->basepath.'/'.$rootpath.$relpath)) {
  1077. return array();
  1078. }
  1079. $fileids = array();
  1080. // make the fake file record for the directory itself
  1081. $filerecord = $this->make_file_record(array('filepath' => $relpath, 'filename' => '.'));
  1082. $this->stash_file($filerecord);
  1083. $fileids[] = $filerecord['id'];
  1084. $items = new DirectoryIterator($this->basepath.'/'.$rootpath.$relpath);
  1085. foreach ($items as $item) {
  1086. if ($item->isDot()) {
  1087. continue;
  1088. }
  1089. if ($item->isLink()) {
  1090. throw new moodle1_convert_exception('unexpected_symlink');
  1091. }
  1092. if ($item->isFile()) {
  1093. $fileids[] = $this->migrate_file(substr($item->getPathname(), strlen($this->basepath.'/')),
  1094. $relpath, $item->getFilename(), 0, $item->getCTime(), $item->getMTime());
  1095. } else {
  1096. $dirname = clean_param($item->getFilename(), PARAM_PATH);
  1097. if ($dirname === '') {
  1098. throw new moodle1_convert_exception('unsupported_chars_in_filename');
  1099. }
  1100. // migrate subdirectories recursively
  1101. $fileids = array_merge($fileids, $this->migrate_directory($rootpath, $relpath.$item->getFilename().'/'));
  1102. }
  1103. }
  1104. return $fileids;
  1105. }
  1106. /**
  1107. * Returns the list of all file ids migrated by this instance so far
  1108. *
  1109. * @return array of int
  1110. */
  1111. public function get_fileids() {
  1112. return $this->fileids;
  1113. }
  1114. /**
  1115. * Explicitly clear the list of file ids migrated by this instance so far
  1116. */
  1117. public function reset_fileids() {
  1118. $this->fileids = array();
  1119. }
  1120. /**
  1121. * Log a message using the converter's logging mechanism
  1122. *
  1123. * @param string $message message text
  1124. * @param int $level message level {@example backup::LOG_WARNING}
  1125. * @param null|mixed $a additional information
  1126. * @param null|int $depth the message depth
  1127. * @param bool $display whether the message should be sent to the output, too
  1128. */
  1129. public function log($message, $level, $a = null, $depth = null, $display = false) {
  1130. $this->converter->log($message, $level, $a, $depth, $display);
  1131. }
  1132. /// internal implementation details ////////////////////////////////////////
  1133. /**
  1134. * Prepares a fake record from the files table
  1135. *
  1136. * @param array $fileinfo explicit file data
  1137. * @return array
  1138. */
  1139. protected function make_file_record(array $fileinfo) {
  1140. $defaultrecord = array(
  1141. 'contenthash' => 'da39a3ee5e6b4b0d3255bfef95601890afd80709', // sha1 of an empty file
  1142. 'contextid' => $this->contextid,
  1143. 'component' => $this->component,
  1144. 'filearea' => $this->filearea,
  1145. 'itemid' => $this->itemid,
  1146. 'filepath' => null,
  1147. 'filename' => null,
  1148. 'filesize' => 0,
  1149. 'userid' => $this->userid,
  1150. 'mimetype' => null,
  1151. 'status' => 0,
  1152. 'timecreated' => $now = time(),
  1153. 'timemodified' => $now,
  1154. 'source' => null,
  1155. 'author' => null,
  1156. 'license' => null,
  1157. 'sortorder' => 0,
  1158. );
  1159. if (!array_key_exists('id', $fileinfo)) {
  1160. $defaultrecord['id'] = $this->converter->get_nextid();
  1161. }
  1162. // override the default values with the explicit data provided and return
  1163. return array_merge($defaultrecord, $fileinfo);
  1164. }
  1165. /**
  1166. * Copies the given file to the pool directory
  1167. *
  1168. * Returns an array containing SHA1 hash of the file contents, the file size
  1169. * and a flag indicating whether the file was actually added to the pool or whether
  1170. * it was already there.
  1171. *
  1172. * @param string $pathname the full path to the file
  1173. * @return array with keys (string)contenthash, (int)filesize, (bool)newfile
  1174. */
  1175. protected function add_file_to_pool($pathname) {
  1176. if (!is_readable($pathname)) {
  1177. throw new moodle1_convert_exception('file_not_readable');
  1178. }
  1179. $contenthash = sha1_file($pathname);
  1180. $filesize = filesize($pathname);
  1181. $hashpath = $this->converter->get_workdir_path().'/files/'.substr($contenthash, 0, 2);
  1182. $hashfile = "$hashpath/$contenthash";
  1183. if (file_exists($hashfile)) {
  1184. if (filesize($hashfile) !== $filesize) {
  1185. // congratulations! you have found two files with different size and the same
  1186. // content hash. or, something were wrong (which is more likely)
  1187. throw new moodle1_convert_exception('same_hash_different_size');
  1188. }
  1189. $newfile = false;

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