Commit | Line | Data |
---|---|---|
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 | ||
25 | namespace core_h5p; | |
26 | ||
6da050d7 VDF |
27 | use H5peditorFile; |
28 | use 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 | */ | |
37 | class 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 | } |