MDL-68909 h5p: move temporary editor files to draft area
[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';
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      */
57     public const EDITOR_FILEAREA = 'editor';
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;
65     /** @var \file_storage $fs File storage. */
66     protected $fs;
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     }
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         ];
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     }
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         ];
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     }
116     /**
117      * Remove content folder.
118      *
119      * @param array $content Content properties
120      */
121     public function deleteContent($content) {
123         $this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']);
124     }
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     }
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     }
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     }
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     }
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) {
175         global $USER;
177         // Remove old export.
178         $this->deleteExport($filename);
180         $filerecord = [
181             'contextid' => $this->context->id,
182             'component' => self::COMPONENT,
183             'filearea' => self::EXPORT_FILEAREA,
184             'itemid' => 0,
185             'filepath' => '/',
186             'filename' => $filename,
187             'userid' => $USER->id
188         ];
189         $this->fs->create_file_from_pathname($filerecord, $source);
190     }
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     }
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     }
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) {
223         foreach ($files as $type => $assets) {
224             if (empty($assets)) {
225                 continue;
226             }
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             ];
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     }
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 = [];
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         }
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         }
280         return empty($files) ? null : $files;
281     }
283     /**
284      * Remove the aggregated cache files.
285      *
286      * @param array $keys The hash keys of removed files
287      */
288     public function deleteCachedAssets($keys) {
290         if (empty($keys)) {
291             return;
292         }
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     }
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);
319         if (!$itemid) {
320             throw new \file_serving_exception('Could not retrieve the requested file, check your file permissions.');
321         }
323         // Locate file.
324         $file = $this->fs->get_file($this->context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
326         // Return content.
327         return $file->get_content();
328     }
330     /**
331      * Save files uploaded through the editor.
332      *
333      * @param H5peditorFile $file
334      * @param int $contentid
335      *
336      * @return int The id of the saved file.
337      */
338     public function saveFile($file, $contentid) {
339         global $USER;
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         }
351         $record = array(
352             'contextid' => $context,
353             'component' => $component,
354             'filearea' => $filearea,
355             'itemid' => $contentid,
356             'filepath' => '/' . $file->getType() . 's/',
357             'filename' => $file->getName()
358         );
360         $storedfile = $this->fs->create_file_from_pathname($record, $_FILES['file']['tmp_name']);
362         return $storedfile->get_id();
363     }
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
371      * @param \stdClass $tocontent Target Content
372      *
373      * @return void
374      */
375     public function cloneContentFile($file, $fromid, $tocontent): void {
376         // Determine source filearea and itemid.
377         if ($fromid === 'editor') {
378             $sourcefilearea = 'draft';
379             $sourceitemid = 0;
380         } else {
381             $sourcefilearea = self::CONTENT_FILEAREA;
382             $sourceitemid = (int)$fromid;
383         }
385         $filepath = '/' . dirname($file) . '/';
386         $filename = basename($file);
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         }
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         }
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         ];
410         $this->fs->create_file_from_storedfile($record, $sourcefile);
411     }
413     /**
414      * Copy content from one directory to another.
415      * Defaults to cloning content from the current temporary upload folder to the editor path.
416      *
417      * @param string $source path to source directory
418      * @param string $contentid Id of content
419      *
420      */
421     public function moveContentDirectory($source, $contentid = null) {
422         $contentidint = (int)$contentid;
424         if ($source === null) {
425             return;
426         }
428         // Get H5P and content json.
429         $contentsource = $source . '/content';
431         // Move all temporary content files to editor.
432         $it = new \RecursiveIteratorIterator(
433             new \RecursiveDirectoryIterator($contentsource,\RecursiveDirectoryIterator::SKIP_DOTS),
434             \RecursiveIteratorIterator::SELF_FIRST
435         );
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         }
446     }
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());
475             // Return image URL.
476             return $iconurl->out();
477         }
479         return false;
480     }
482     /**
483      * Checks to see if an H5P content has the given file.
484      *
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
489      */
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);
496         return ($contentfile === null ? null : $contentfile->get_id());
497     }
499     /**
500      * Remove content files that are no longer used.
501      *
502      * Used when saving content.
503      *
504      * @param string $file File path and name.
505      * @param int $contentid Content id.
506      *
507      * @return void
508      */
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         }
518     }
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     }
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     }
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     }
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     }
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 {
581         global $DB;
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         }
588         $areafiles = $this->fs->get_area_files($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
589         $this->delete_directory($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']);
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         }
598     }
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 {
610         $this->fs->delete_area_files($contextid, $component, $filearea, $itemid);
611     }
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 {
620         $librarycache = \cache::make('core', 'h5p_library_files');
621         $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS),
622                 \RecursiveIteratorIterator::SELF_FIRST);
624         $root = $options['filepath'];
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                 }
638                 $file = $this->fs->create_file_from_pathname($options, $item->getPathName());
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                 }
645             }
646             $it->next();
647         }
648     }
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);
663         // Read source files.
664         $files = $this->fs->get_directory_files($contextid, self::COMPONENT, $filearea, $itemid, $filepath, true);
666         $librarycache = \cache::make('core', 'h5p_library_files');
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 {
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                 }
684             }
685         }
686     }
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);
707             if ($itemid === false) {
708                 continue;
709             }
711             // Locate file.
712             $file = $this->fs->get_file($context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename);
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     }
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      */
759     public function get_export_file(string $filename) {
760         return $this->fs->get_file($this->context->id, self::COMPONENT, self::EXPORT_FILEAREA, 0, '/', $filename);
761     }
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);
779         $itemid = array_shift($sections);
780         // Get the filepath.
781         $filepath = implode('/', $sections);
782         $filepath = '/' . $filepath . '/';
784         return ['filearea' => $filearea, 'filepath' => $filepath, 'filename' => $filename, 'itemid' => $itemid];
785     }
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     }
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 {
811         global $USER;
813         $component = self::COMPONENT;
814         $context = $this->context->id;
815         if ($filearea === 'draft') {
816             $itemid = 0;
817             $component = 'user';
818             $usercontext = \context_user::instance($USER->id);
819             $context = $usercontext->id;
820         }
822         $filepath = '/'. dirname($file). '/';
823         $filename = basename($file);
825         // Load file.
826         $existingfile = $this->fs->get_file($context, $component, $filearea, $itemid, $filepath, $filename);
827         if (!$existingfile) {
828             return null;
829         }
831         return $existingfile;
832     }
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);
848         // Create file record for content.
849         $record = array(
850             'contextid' => $this->context->id,
851             'component' => $contentid > 0 ? self::COMPONENT : 'user',
852             'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : 'draft',
853             'itemid' => $contentid > 0 ? $contentid : 0,
854             'filepath' => '/' . $foldername . '/',
855             'filename' => $filename
856         );
858         $file = $this->fs->get_file(
859             $record['contextid'], $record['component'],
860             $record['filearea'], $record['itemid'], $record['filepath'],
861             $record['filename']
862         );
864         if ($file) {
865             // Delete it to make sure that it is replaced with correct content.
866             $file->delete();
867         }
869         $this->fs->create_file_from_pathname($record, $sourcefile);
870     }