MDL-68909 h5p: move temporary editor files to draft area
[moodle.git] / h5p / classes / file_storage.php
CommitLineData
e55cc513
AG
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/**
18 * Class \core_h5p\file_storage.
19 *
20 * @package core_h5p
21 * @copyright 2019 Victor Deniz <victor@moodle.com>, base on code by Joubel AS
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25namespace core_h5p;
26
6da050d7
VDF
27use H5peditorFile;
28use stored_file;
e55cc513
AG
29
30/**
31 * Class to handle storage and export of H5P Content.
32 *
33 * @package core_h5p
34 * @copyright 2019 Victor Deniz <victor@moodle.com>
35 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
36 */
37class file_storage implements \H5PFileStorage {
38
39 /** The component for H5P. */
40 public const COMPONENT = 'core_h5p';
41 /** The library file area. */
42 public const LIBRARY_FILEAREA = 'libraries';
43 /** The content file area */
44 public const CONTENT_FILEAREA = 'content';
45 /** The cached assest file area. */
46 public const CACHED_ASSETS_FILEAREA = 'cachedassets';
47 /** The export file area */
48 public const EXPORT_FILEAREA = 'export';
d3ee08db
AA
49 /** The icon filename */
50 public const ICON_FILENAME = 'icon.svg';
67a11150
SA
51
52 /**
53 * The editor file area.
54 * @deprecated since Moodle 3.10 MDL-68909. Please do not use this constant any more.
55 * @todo MDL-69530 This will be deleted in Moodle 4.2.
56 */
6da050d7 57 public const EDITOR_FILEAREA = 'editor';
e55cc513
AG
58
59 /**
60 * @var \context $context Currently we use the system context everywhere.
61 * Don't feel forced to keep it this way in the future.
62 */
63 protected $context;
64
65 /** @var \file_storage $fs File storage. */
66 protected $fs;
67
68 /**
69 * Initial setup for file_storage.
70 */
71 public function __construct() {
72 // Currently everything uses the system context.
73 $this->context = \context_system::instance();
74 $this->fs = get_file_storage();
75 }
76
77 /**
78 * Stores a H5P library in the Moodle filesystem.
79 *
80 * @param array $library Library properties.
81 */
82 public function saveLibrary($library) {
83 $options = [
84 'contextid' => $this->context->id,
85 'component' => self::COMPONENT,
86 'filearea' => self::LIBRARY_FILEAREA,
87 'filepath' => '/' . \H5PCore::libraryToString($library, true) . '/',
88 'itemid' => $library['libraryId']
89 ];
90
91 // Easiest approach: delete the existing library version and copy the new one.
92 $this->delete_library($library);
93 $this->copy_directory($library['uploadDirectory'], $options);
94 }
95
96 /**
97 * Store the content folder.
98 *
99 * @param string $source Path on file system to content directory.
100 * @param array $content Content properties
101 */
102 public function saveContent($source, $content) {
103 $options = [
104 'contextid' => $this->context->id,
105 'component' => self::COMPONENT,
106 'filearea' => self::CONTENT_FILEAREA,
107 'itemid' => $content['id'],
108 'filepath' => '/',
109 ];
110
111 $this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
112 // Copy content directory into Moodle filesystem.
113 $this->copy_directory($source, $options);
114 }
115
116 /**
117 * Remove content folder.
118 *
119 * @param array $content Content properties
120 */
121 public function deleteContent($content) {
122
123 $this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
124 }
125
126 /**
127 * Creates a stored copy of the content folder.
128 *
129 * @param string $id Identifier of content to clone.
130 * @param int $newid The cloned content's identifier
131 */
132 public function cloneContent($id, $newid) {
133 // Not implemented in Moodle.
134 }
135
136 /**
137 * Get path to a new unique tmp folder.
138 * Please note this needs to not be a directory.
139 *
140 * @return string Path
141 */
142 public function getTmpPath(): string {
143 return make_request_directory() . '/' . uniqid('h5p-');
144 }
145
146 /**
147 * Fetch content folder and save in target directory.
148 *
149 * @param int $id Content identifier
150 * @param string $target Where the content folder will be saved
151 */
152 public function exportContent($id, $target) {
153 $this->export_file_tree($target, $this->context->id, self::CONTENT_FILEAREA, '/', $id);
154 }
155
156 /**
157 * Fetch library folder and save in target directory.
158 *
159 * @param array $library Library properties
160 * @param string $target Where the library folder will be saved
161 */
162 public function exportLibrary($library, $target) {
163 $folder = \H5PCore::libraryToString($library, true);
164 $this->export_file_tree($target . '/' . $folder, $this->context->id, self::LIBRARY_FILEAREA,
165 '/' . $folder . '/', $library['libraryId']);
166 }
167
168 /**
169 * Save export in file system
170 *
171 * @param string $source Path on file system to temporary export file.
172 * @param string $filename Name of export file.
173 */
174 public function saveExport($source, $filename) {
6da050d7
VDF
175 global $USER;
176
177 // Remove old export.
178 $this->deleteExport($filename);
179
e55cc513
AG
180 $filerecord = [
181 'contextid' => $this->context->id,
182 'component' => self::COMPONENT,
183 'filearea' => self::EXPORT_FILEAREA,
184 'itemid' => 0,
185 'filepath' => '/',
6da050d7
VDF
186 'filename' => $filename,
187 'userid' => $USER->id
e55cc513
AG
188 ];
189 $this->fs->create_file_from_pathname($filerecord, $source);
190 }
191
192 /**
193 * Removes given export file
194 *
195 * @param string $filename filename of the export to delete.
196 */
197 public function deleteExport($filename) {
198 $file = $this->get_export_file($filename);
199 if ($file) {
200 $file->delete();
201 }
202 }
203
204 /**
205 * Check if the given export file exists
206 *
207 * @param string $filename The export file to check.
208 * @return boolean True if the export file exists.
209 */
210 public function hasExport($filename) {
211 return !!$this->get_export_file($filename);
212 }
213
214 /**
215 * Will concatenate all JavaScrips and Stylesheets into two files in order
216 * to improve page performance.
217 *
218 * @param array $files A set of all the assets required for content to display
219 * @param string $key Hashed key for cached asset
220 */
221 public function cacheAssets(&$files, $key) {
222
223 foreach ($files as $type => $assets) {
224 if (empty($assets)) {
225 continue;
226 }
227
228 // Create new file for cached assets.
229 $ext = ($type === 'scripts' ? 'js' : 'css');
230 $filename = $key . '.' . $ext;
231 $fileinfo = [
232 'contextid' => $this->context->id,
233 'component' => self::COMPONENT,
234 'filearea' => self::CACHED_ASSETS_FILEAREA,
235 'itemid' => 0,
236 'filepath' => '/',
237 'filename' => $filename
238 ];
239
240 // Store concatenated content.
241 $this->fs->create_file_from_string($fileinfo, $this->concatenate_files($assets, $type, $this->context));
242 $files[$type] = [
243 (object) [
244 'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . $filename,
245 'version' => ''
246 ]
247 ];
248 }
249 }
250
251 /**
252 * Will check if there are cache assets available for content.
253 *
254 * @param string $key Hashed key for cached asset
255 * @return array
256 */
257 public function getCachedAssets($key) {
258 $files = [];
259
260 $js = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/', "{$key}.js");
261 if ($js && $js->get_filesize() > 0) {
262 $files['scripts'] = [
263 (object) [
264 'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . "{$key}.js",
265 'version' => ''
266 ]
267 ];
268 }
269
270 $css = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/', "{$key}.css");
271 if ($css && $css->get_filesize() > 0) {
272 $files['styles'] = [
273 (object) [
274 'path' => '/' . self::CACHED_ASSETS_FILEAREA . '/' . "{$key}.css",
275 'version' => ''
276 ]
277 ];
278 }
279
280 return empty($files) ? null : $files;
281 }
282
283 /**
284 * Remove the aggregated cache files.
285 *
286 * @param array $keys The hash keys of removed files
287 */
288 public function deleteCachedAssets($keys) {
289
290 if (empty($keys)) {
291 return;
292 }
293
294 foreach ($keys as $hash) {
295 foreach (['js', 'css'] as $type) {
296 $cachedasset = $this->fs->get_file($this->context->id, self::COMPONENT, self::CACHED_ASSETS_FILEAREA, 0, '/',
297 "{$hash}.{$type}");
298 if ($cachedasset) {
299 $cachedasset->delete();
300 }
301 }
302 }
303 }
304
305 /**
306 * Read file content of given file and then return it.
307 *
308 * @param string $filepath
309 * @return string contents
310 */
311 public function getContent($filepath) {
312 list(
313 'filearea' => $filearea,
314 'filepath' => $filepath,
315 'filename' => $filename,
316 'itemid' => $itemid
317 ) = $this->get_file_elements_from_filepath($filepath);
318
319 if (!$itemid) {
320 throw new \file_serving_exception('Could not retrieve the requested file, check your file permissions.');
321 }
322
323 // Locate file.
324 $file = $this->fs->get_file($this->context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
325
326 // Return content.
327 return $file->get_content();
328 }
329
330 /**
331 * Save files uploaded through the editor.
e55cc513 332 *
6da050d7 333 * @param H5peditorFile $file
e55cc513 334 * @param int $contentid
6da050d7
VDF
335 *
336 * @return int The id of the saved file.
e55cc513
AG
337 */
338 public function saveFile($file, $contentid) {
67a11150
SA
339 global $USER;
340
341 $context = $this->context->id;
342 $component = self::COMPONENT;
343 $filearea = self::CONTENT_FILEAREA;
344 if ($contentid === 0) {
345 $usercontext = \context_user::instance($USER->id);
346 $context = $usercontext->id;
347 $component = 'user';
348 $filearea = 'draft';
349 }
350
6da050d7 351 $record = array(
67a11150
SA
352 'contextid' => $context,
353 'component' => $component,
354 'filearea' => $filearea,
6da050d7
VDF
355 'itemid' => $contentid,
356 'filepath' => '/' . $file->getType() . 's/',
357 'filename' => $file->getName()
358 );
359
360 $storedfile = $this->fs->create_file_from_pathname($record, $_FILES['file']['tmp_name']);
361
362 return $storedfile->get_id();
e55cc513
AG
363 }
364
365 /**
366 * Copy a file from another content or editor tmp dir.
367 * Used when copy pasting content in H5P.
368 *
369 * @param string $file path + name
370 * @param string|int $fromid Content ID or 'editor' string
6da050d7
VDF
371 * @param \stdClass $tocontent Target Content
372 *
373 * @return void
e55cc513 374 */
6da050d7
VDF
375 public function cloneContentFile($file, $fromid, $tocontent): void {
376 // Determine source filearea and itemid.
67a11150
SA
377 if ($fromid === 'editor') {
378 $sourcefilearea = 'draft';
6da050d7
VDF
379 $sourceitemid = 0;
380 } else {
381 $sourcefilearea = self::CONTENT_FILEAREA;
382 $sourceitemid = (int)$fromid;
383 }
384
385 $filepath = '/' . dirname($file) . '/';
386 $filename = basename($file);
387
388 // Check to see if source exists.
389 $sourcefile = $this->get_file($sourcefilearea, $sourceitemid, $file);
390 if ($sourcefile === null) {
391 return; // Nothing to copy from.
392 }
393
394 // Check to make sure that file doesn't exist already in target.
395 $targetfile = $this->get_file(self::CONTENT_FILEAREA, $tocontent->id, $file);
396 if ( $targetfile !== null) {
397 return; // File exists, no need to copy.
398 }
399
400 // Create new file record.
401 $record = [
402 'contextid' => $this->context->id,
403 'component' => self::COMPONENT,
404 'filearea' => self::CONTENT_FILEAREA,
405 'itemid' => $tocontent->id,
406 'filepath' => $filepath,
407 'filename' => $filename,
408 ];
409
410 $this->fs->create_file_from_storedfile($record, $sourcefile);
e55cc513
AG
411 }
412
413 /**
6da050d7
VDF
414 * Copy content from one directory to another.
415 * Defaults to cloning content from the current temporary upload folder to the editor path.
e55cc513
AG
416 *
417 * @param string $source path to source directory
418 * @param string $contentid Id of content
419 *
e55cc513
AG
420 */
421 public function moveContentDirectory($source, $contentid = null) {
6da050d7
VDF
422 $contentidint = (int)$contentid;
423
424 if ($source === null) {
425 return;
426 }
427
428 // Get H5P and content json.
429 $contentsource = $source . '/content';
430
431 // Move all temporary content files to editor.
432 $it = new \RecursiveIteratorIterator(
433 new \RecursiveDirectoryIterator($contentsource,\RecursiveDirectoryIterator::SKIP_DOTS),
434 \RecursiveIteratorIterator::SELF_FIRST
435 );
436
437 $it->rewind();
438 while ($it->valid()) {
439 $item = $it->current();
440 $pathname = $it->getPathname();
441 if (!$item->isDir() && !($item->getFilename() === 'content.json')) {
442 $this->move_file($pathname, $contentidint);
443 }
444 $it->next();
445 }
e55cc513
AG
446 }
447
d3ee08db
AA
448 /**
449 * Get the file URL or given library and then return it.
450 *
451 * @param int $itemid
452 * @param string $machinename
453 * @param int $majorversion
454 * @param int $minorversion
455 * @return string url or false if the file doesn't exist
456 */
457 public function get_icon_url(int $itemid, string $machinename, int $majorversion, int $minorversion) {
458 $filepath = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
459 if ($file = $this->fs->get_file(
460 $this->context->id,
461 self::COMPONENT,
462 self::LIBRARY_FILEAREA,
463 $itemid,
464 $filepath,
465 self::ICON_FILENAME)
466 ) {
467 $iconurl = \moodle_url::make_pluginfile_url(
468 $this->context->id,
469 self::COMPONENT,
470 self::LIBRARY_FILEAREA,
471 $itemid,
472 $filepath,
473 $file->get_filename());
474
475 // Return image URL.
476 return $iconurl->out();
477 }
478
479 return false;
480 }
481
e55cc513 482 /**
6da050d7 483 * Checks to see if an H5P content has the given file.
e55cc513 484 *
6da050d7
VDF
485 * @param string $file File path and name.
486 * @param int $content Content id.
487 *
488 * @return int|null File ID or NULL if not found
e55cc513 489 */
6da050d7
VDF
490 public function getContentFile($file, $content): ?int {
491 if (is_object($content)) {
492 $content = $content->id;
493 }
494 $contentfile = $this->get_file(self::CONTENT_FILEAREA, $content, $file);
495
496 return ($contentfile === null ? null : $contentfile->get_id());
e55cc513
AG
497 }
498
499 /**
500 * Remove content files that are no longer used.
6da050d7 501 *
e55cc513
AG
502 * Used when saving content.
503 *
6da050d7
VDF
504 * @param string $file File path and name.
505 * @param int $contentid Content id.
506 *
507 * @return void
e55cc513 508 */
6da050d7
VDF
509 public function removeContentFile($file, $contentid): void {
510 // Although the interface defines $contentid as int, object given in \H5peditor::processParameters.
511 if (is_object($contentid)) {
512 $contentid = $contentid->id;
513 }
514 $existingfile = $this->get_file(self::CONTENT_FILEAREA, $contentid, $file);
515 if ($existingfile !== null) {
516 $existingfile->delete();
517 }
e55cc513
AG
518 }
519
520 /**
521 * Check if server setup has write permission to
522 * the required folders
523 *
524 * @return bool True if server has the proper write access
525 */
526 public function hasWriteAccess() {
527 // Moodle has access to the files table which is where all of the folders are stored.
528 return true;
529 }
530
531 /**
532 * Check if the library has a presave.js in the root folder
533 *
534 * @param string $libraryname
535 * @param string $developmentpath
536 * @return bool
537 */
538 public function hasPresave($libraryname, $developmentpath = null) {
539 return false;
540 }
541
542 /**
543 * Check if upgrades script exist for library.
544 *
545 * @param string $machinename
546 * @param int $majorversion
547 * @param int $minorversion
548 * @return string Relative path
549 */
550 public function getUpgradeScript($machinename, $majorversion, $minorversion) {
551 $path = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/';
552 $file = 'upgrade.js';
553 $itemid = $this->get_itemid_for_file(self::LIBRARY_FILEAREA, $path, $file);
554 if ($this->fs->get_file($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $itemid, $path, $file)) {
555 return '/' . self::LIBRARY_FILEAREA . $path. $file;
556 } else {
557 return null;
558 }
559 }
560
561 /**
562 * Store the given stream into the given file.
563 *
564 * @param string $path
565 * @param string $file
566 * @param resource $stream
567 * @return bool|int
568 */
569 public function saveFileFromZip($path, $file, $stream) {
570 $fullpath = $path . '/' . $file;
571 check_dir_exists(pathinfo($fullpath, PATHINFO_DIRNAME));
572 return file_put_contents($fullpath, $stream);
573 }
574
575 /**
576 * Deletes a library from the file system.
577 *
578 * @param array $library Library details
579 */
580 public function delete_library(array $library): void {
ef8dff05 581 global $DB;
e55cc513
AG
582
583 // A library ID of false would result in all library files being deleted, which we don't want. Return instead.
584 if ($library['libraryId'] === false) {
585 return;
586 }
587
ef8dff05 588 $areafiles = $this->fs->get_area_files($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
e55cc513 589 $this->delete_directory($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
ef8dff05
JT
590 $librarycache = \cache::make('core', 'h5p_library_files');
591 foreach ($areafiles as $file) {
592 if (!$DB->record_exists('files', array('contenthash' => $file->get_contenthash(),
593 'component' => self::COMPONENT,
594 'filearea' => self::LIBRARY_FILEAREA))) {
595 $librarycache->delete($file->get_contenthash());
596 }
597 }
e55cc513
AG
598 }
599
600 /**
601 * Remove an H5P directory from the filesystem.
602 *
603 * @param int $contextid context ID
604 * @param string $component component
605 * @param string $filearea file area or all areas in context if not specified
606 * @param int $itemid item ID or all files if not specified
607 */
608 private function delete_directory(int $contextid, string $component, string $filearea, int $itemid): void {
609
610 $this->fs->delete_area_files($contextid, $component, $filearea, $itemid);
611 }
612
613 /**
614 * Copy an H5P directory from the temporary directory into the file system.
615 *
616 * @param string $source Temporary location for files.
617 * @param array $options File system information.
618 */
619 private function copy_directory(string $source, array $options): void {
ef8dff05 620 $librarycache = \cache::make('core', 'h5p_library_files');
e55cc513
AG
621 $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
622 \RecursiveIteratorIterator::SELF_FIRST);
623
624 $root = $options['filepath'];
625
626 $it->rewind();
627 while ($it->valid()) {
628 $item = $it->current();
629 $subpath = $it->getSubPath();
630 if (!$item->isDir()) {
631 $options['filename'] = $it->getFilename();
632 if (!$subpath == '') {
633 $options['filepath'] = $root . $subpath . '/';
634 } else {
635 $options['filepath'] = $root;
636 }
637
ef8dff05
JT
638 $file = $this->fs->create_file_from_pathname($options, $item->getPathName());
639
640 if ($options['filearea'] == self::LIBRARY_FILEAREA) {
641 if (!$librarycache->has($file->get_contenthash())) {
642 $librarycache->set($file->get_contenthash(), file_get_contents($item->getPathName()));
643 }
644 }
e55cc513
AG
645 }
646 $it->next();
647 }
648 }
649
650 /**
651 * Copies files from storage to temporary folder.
652 *
653 * @param string $target Path to temporary folder
654 * @param int $contextid context where the files are found
655 * @param string $filearea file area
656 * @param string $filepath file path
657 * @param int $itemid Optional item ID
658 */
659 private function export_file_tree(string $target, int $contextid, string $filearea, string $filepath, int $itemid = 0): void {
660 // Make sure target folder exists.
661 check_dir_exists($target);
662
663 // Read source files.
664 $files = $this->fs->get_directory_files($contextid, self::COMPONENT, $filearea, $itemid, $filepath, true);
665
ef8dff05
JT
666 $librarycache = \cache::make('core', 'h5p_library_files');
667
e55cc513
AG
668 foreach ($files as $file) {
669 $path = $target . str_replace($filepath, DIRECTORY_SEPARATOR, $file->get_filepath());
670 if ($file->is_directory()) {
671 check_dir_exists(rtrim($path));
672 } else {
ef8dff05
JT
673 if ($filearea == self::LIBRARY_FILEAREA) {
674 $cachedfile = $librarycache->get($file->get_contenthash());
675 if (empty($cachedfile)) {
676 $file->copy_content_to($path . $file->get_filename());
677 $librarycache->set($file->get_contenthash(), file_get_contents($path . $file->get_filename()));
678 } else {
679 file_put_contents($path . $file->get_filename(), $cachedfile);
680 }
681 } else {
682 $file->copy_content_to($path . $file->get_filename());
683 }
e55cc513
AG
684 }
685 }
686 }
687
688 /**
689 * Adds all files of a type into one file.
690 *
691 * @param array $assets A list of files.
692 * @param string $type The type of files in assets. Either 'scripts' or 'styles'
693 * @param \context $context Context
694 * @return string All of the file content in one string.
695 */
696 private function concatenate_files(array $assets, string $type, \context $context): string {
697 $content = '';
698 foreach ($assets as $asset) {
699 // Find location of asset.
700 list(
701 'filearea' => $filearea,
702 'filepath' => $filepath,
703 'filename' => $filename,
704 'itemid' => $itemid
705 ) = $this->get_file_elements_from_filepath($asset->path);
706
707 if ($itemid === false) {
708 continue;
709 }
710
711 // Locate file.
712 $file = $this->fs->get_file($context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
713
714 // Get file content and concatenate.
715 if ($type === 'scripts') {
716 $content .= $file->get_content() . ";\n";
717 } else {
718 // Rewrite relative URLs used inside stylesheets.
719 $content .= preg_replace_callback(
720 '/url\([\'"]?([^"\')]+)[\'"]?\)/i',
721 function ($matches) use ($filearea, $filepath, $itemid) {
722 if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) {
723 return $matches[0]; // Not relative, skip.
724 }
725 // Find "../" in matches[1].
726 // If it exists, we have to remove "../".
727 // And switch the last folder in the filepath for the first folder in $matches[1].
728 // For instance:
729 // $filepath: /H5P.Question-1.4/styles/
730 // $matches[1]: ../images/plus-one.svg
731 // We want to avoid this: H5P.Question-1.4/styles/ITEMID/../images/minus-one.svg
732 // We want this: H5P.Question-1.4/images/ITEMID/minus-one.svg.
733 if (preg_match('/\.\.\//', $matches[1], $pathmatches)) {
734 $path = preg_split('/\//', $filepath, -1, PREG_SPLIT_NO_EMPTY);
735 $pathfilename = preg_split('/\//', $matches[1], -1, PREG_SPLIT_NO_EMPTY);
736 // Remove the first element: ../.
737 array_shift($pathfilename);
738 // Replace pathfilename into the filepath.
739 $path[count($path) - 1] = $pathfilename[0];
740 $filepath = '/' . implode('/', $path) . '/';
741 // Remove the element used to replace.
742 array_shift($pathfilename);
743 $matches[1] = implode('/', $pathfilename);
744 }
745 return 'url("../' . $filearea . $filepath . $itemid . '/' . $matches[1] . '")';
746 },
747 $file->get_content()) . "\n";
748 }
749 }
750 return $content;
751 }
752
753 /**
754 * Get files ready for export.
755 *
756 * @param string $filename File name to retrieve.
757 * @return bool|\stored_file Stored file instance if exists, false if not
758 */
6da050d7 759 public function get_export_file(string $filename) {
e55cc513
AG
760 return $this->fs->get_file($this->context->id, self::COMPONENT, self::EXPORT_FILEAREA, 0, '/', $filename);
761 }
762
763 /**
764 * Converts a relative system file path into Moodle File API elements.
765 *
766 * @param string $filepath The system filepath to get information from.
767 * @return array File information.
768 */
769 private function get_file_elements_from_filepath(string $filepath): array {
770 $sections = explode('/', $filepath);
771 // Get the filename.
772 $filename = array_pop($sections);
773 // Discard first element.
774 if (empty($sections[0])) {
775 array_shift($sections);
776 }
777 // Get the filearea.
778 $filearea = array_shift($sections);
9e67f5e3 779 $itemid = array_shift($sections);
e55cc513
AG
780 // Get the filepath.
781 $filepath = implode('/', $sections);
782 $filepath = '/' . $filepath . '/';
783
9e67f5e3 784 return ['filearea' => $filearea, 'filepath' => $filepath, 'filename' => $filename, 'itemid' => $itemid];
e55cc513
AG
785 }
786
787 /**
788 * Returns the item id given the other necessary variables.
789 *
790 * @param string $filearea The file area.
791 * @param string $filepath The file path.
792 * @param string $filename The file name.
793 * @return mixed the specified value false if not found.
794 */
795 private function get_itemid_for_file(string $filearea, string $filepath, string $filename) {
796 global $DB;
797 return $DB->get_field('files', 'itemid', ['component' => self::COMPONENT, 'filearea' => $filearea, 'filepath' => $filepath,
798 'filename' => $filename]);
799 }
6da050d7
VDF
800
801 /**
802 * Helper to make it easy to load content files.
803 *
804 * @param string $filearea File area where the file is saved.
805 * @param int $itemid Content instance or content id.
806 * @param string $file File path and name.
807 *
808 * @return stored_file|null
809 */
810 private function get_file(string $filearea, int $itemid, string $file): ?stored_file {
67a11150
SA
811 global $USER;
812
813 $component = self::COMPONENT;
814 $context = $this->context->id;
815 if ($filearea === 'draft') {
6da050d7 816 $itemid = 0;
67a11150
SA
817 $component = 'user';
818 $usercontext = \context_user::instance($USER->id);
819 $context = $usercontext->id;
6da050d7
VDF
820 }
821
822 $filepath = '/'. dirname($file). '/';
823 $filename = basename($file);
824
825 // Load file.
67a11150 826 $existingfile = $this->fs->get_file($context, $component, $filearea, $itemid, $filepath, $filename);
6da050d7
VDF
827 if (!$existingfile) {
828 return null;
829 }
830
831 return $existingfile;
832 }
833
834 /**
835 * Move a single file
836 *
837 * @param string $sourcefile Path to source file
838 * @param int $contentid Content id or 0 if the file is in the editor file area
839 *
840 * @return void
841 */
842 private function move_file(string $sourcefile, int $contentid): void {
843 $pathparts = pathinfo($sourcefile);
844 $filename = $pathparts['basename'];
845 $filepath = $pathparts['dirname'];
846 $foldername = basename($filepath);
847
848 // Create file record for content.
849 $record = array(
850 'contextid' => $this->context->id,
67a11150
SA
851 'component' => $contentid > 0 ? self::COMPONENT : 'user',
852 'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : 'draft',
6da050d7
VDF
853 'itemid' => $contentid > 0 ? $contentid : 0,
854 'filepath' => '/' . $foldername . '/',
855 'filename' => $filename
856 );
857
858 $file = $this->fs->get_file(
859 $record['contextid'], $record['component'],
860 $record['filearea'], $record['itemid'], $record['filepath'],
861 $record['filename']
862 );
863
864 if ($file) {
865 // Delete it to make sure that it is replaced with correct content.
866 $file->delete();
867 }
868
869 $this->fs->create_file_from_pathname($record, $sourcefile);
870 }
9e67f5e3 871}