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