See Release Notes
Long Term Support Release
Differences Between: [Versions 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]
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 27 use H5peditorFile; 28 use stored_file; 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'; 49 /** The icon filename */ 50 public const ICON_FILENAME = 'icon.svg'; 51 /** The editor file area */ 52 public const EDITOR_FILEAREA = 'editor'; 53 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; 59 60 /** @var \file_storage $fs File storage. */ 61 protected $fs; 62 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 } 71 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 ]; 85 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 } 90 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 ]; 105 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 } 110 111 /** 112 * Remove content folder. 113 * 114 * @param array $content Content properties 115 */ 116 public function deleteContent($content) { 117 118 $this->delete_directory($this->context->id, self::COMPONENT, self::CONTENT_FILEAREA, $content['id']); 119 } 120 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 } 130 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 } 140 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 } 150 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 } 162 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; 171 172 // Remove old export. 173 $this->deleteExport($filename); 174 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 } 186 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 } 198 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 } 208 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) { 217 218 foreach ($files as $type => $assets) { 219 if (empty($assets)) { 220 continue; 221 } 222 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 ]; 234 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 } 245 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 = []; 254 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 } 264 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 } 274 275 return empty($files) ? null : $files; 276 } 277 278 /** 279 * Remove the aggregated cache files. 280 * 281 * @param array $keys The hash keys of removed files 282 */ 283 public function deleteCachedAssets($keys) { 284 285 if (empty($keys)) { 286 return; 287 } 288 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 } 299 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); 313 314 if (!$itemid) { 315 throw new \file_serving_exception('Could not retrieve the requested file, check your file permissions.'); 316 } 317 318 // Locate file. 319 $file = $this->fs->get_file($this->context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename); 320 321 // Return content. 322 return $file->get_content(); 323 } 324 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 global $USER; 335 336 $context = $this->context->id; 337 $component = self::COMPONENT; 338 $filearea = self::CONTENT_FILEAREA; 339 if ($contentid === 0) { 340 $usercontext = \context_user::instance($USER->id); 341 $context = $usercontext->id; 342 $component = 'user'; 343 $filearea = 'draft'; 344 } 345 346 $record = array( 347 'contextid' => $context, 348 'component' => $component, 349 'filearea' => $filearea, 350 'itemid' => $contentid, 351 'filepath' => '/' . $file->getType() . 's/', 352 'filename' => $file->getName() 353 ); 354 355 $storedfile = $this->fs->create_file_from_pathname($record, $_FILES['file']['tmp_name']); 356 357 return $storedfile->get_id(); 358 } 359 360 /** 361 * Copy a file from another content or editor tmp dir. 362 * Used when copy pasting content in H5P. 363 * 364 * @param string $file path + name 365 * @param string|int $fromid Content ID or 'editor' string 366 * @param \stdClass $tocontent Target Content 367 * 368 * @return void 369 */ 370 public function cloneContentFile($file, $fromid, $tocontent): void { 371 // Determine source filearea and itemid. 372 if ($fromid === 'editor') { 373 $sourcefilearea = 'draft'; 374 $sourceitemid = 0; 375 } else { 376 $sourcefilearea = self::CONTENT_FILEAREA; 377 $sourceitemid = (int)$fromid; 378 } 379 380 $filepath = '/' . dirname($file) . '/'; 381 $filename = basename($file); 382 383 // Check to see if source exists. 384 $sourcefile = $this->get_file($sourcefilearea, $sourceitemid, $file); 385 if ($sourcefile === null) { 386 return; // Nothing to copy from. 387 } 388 389 // Check to make sure that file doesn't exist already in target. 390 $targetfile = $this->get_file(self::CONTENT_FILEAREA, $tocontent->id, $file); 391 if ( $targetfile !== null) { 392 return; // File exists, no need to copy. 393 } 394 395 // Create new file record. 396 $record = [ 397 'contextid' => $this->context->id, 398 'component' => self::COMPONENT, 399 'filearea' => self::CONTENT_FILEAREA, 400 'itemid' => $tocontent->id, 401 'filepath' => $filepath, 402 'filename' => $filename, 403 ]; 404 405 $this->fs->create_file_from_storedfile($record, $sourcefile); 406 } 407 408 /** 409 * Copy content from one directory to another. 410 * Defaults to cloning content from the current temporary upload folder to the editor path. 411 * 412 * @param string $source path to source directory 413 * @param string $contentid Id of content 414 * 415 */ 416 public function moveContentDirectory($source, $contentid = null) { 417 $contentidint = (int)$contentid; 418 419 if ($source === null) { 420 return; 421 } 422 423 // Get H5P and content json. 424 $contentsource = $source . '/content'; 425 426 // Move all temporary content files to editor. 427 $it = new \RecursiveIteratorIterator( 428 new \RecursiveDirectoryIterator($contentsource,\RecursiveDirectoryIterator::SKIP_DOTS), 429 \RecursiveIteratorIterator::SELF_FIRST 430 ); 431 432 $it->rewind(); 433 while ($it->valid()) { 434 $item = $it->current(); 435 $pathname = $it->getPathname(); 436 if (!$item->isDir() && !($item->getFilename() === 'content.json')) { 437 $this->move_file($pathname, $contentidint); 438 } 439 $it->next(); 440 } 441 } 442 443 /** 444 * Get the file URL or given library and then return it. 445 * 446 * @param int $itemid 447 * @param string $machinename 448 * @param int $majorversion 449 * @param int $minorversion 450 * @return string url or false if the file doesn't exist 451 */ 452 public function get_icon_url(int $itemid, string $machinename, int $majorversion, int $minorversion) { 453 $filepath = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/'; 454 if ($file = $this->fs->get_file( 455 $this->context->id, 456 self::COMPONENT, 457 self::LIBRARY_FILEAREA, 458 $itemid, 459 $filepath, 460 self::ICON_FILENAME) 461 ) { 462 $iconurl = \moodle_url::make_pluginfile_url( 463 $this->context->id, 464 self::COMPONENT, 465 self::LIBRARY_FILEAREA, 466 $itemid, 467 $filepath, 468 $file->get_filename()); 469 470 // Return image URL. 471 return $iconurl->out(); 472 } 473 474 return false; 475 } 476 477 /** 478 * Checks to see if an H5P content has the given file. 479 * 480 * @param string $file File path and name. 481 * @param int $content Content id. 482 * 483 * @return int|null File ID or NULL if not found 484 */ 485 public function getContentFile($file, $content): ?int { 486 if (is_object($content)) { 487 $content = $content->id; 488 } 489 $contentfile = $this->get_file(self::CONTENT_FILEAREA, $content, $file); 490 491 return ($contentfile === null ? null : $contentfile->get_id()); 492 } 493 494 /** 495 * Remove content files that are no longer used. 496 * 497 * Used when saving content. 498 * 499 * @param string $file File path and name. 500 * @param int $contentid Content id. 501 * 502 * @return void 503 */ 504 public function removeContentFile($file, $contentid): void { 505 // Although the interface defines $contentid as int, object given in \H5peditor::processParameters. 506 if (is_object($contentid)) { 507 $contentid = $contentid->id; 508 } 509 $existingfile = $this->get_file(self::CONTENT_FILEAREA, $contentid, $file); 510 if ($existingfile !== null) { 511 $existingfile->delete(); 512 } 513 } 514 515 /** 516 * Check if server setup has write permission to 517 * the required folders 518 * 519 * @return bool True if server has the proper write access 520 */ 521 public function hasWriteAccess() { 522 // Moodle has access to the files table which is where all of the folders are stored. 523 return true; 524 } 525 526 /** 527 * Check if the library has a presave.js in the root folder 528 * 529 * @param string $libraryname 530 * @param string $developmentpath 531 * @return bool 532 */ 533 public function hasPresave($libraryname, $developmentpath = null) { 534 return false; 535 } 536 537 /** 538 * Check if upgrades script exist for library. 539 * 540 * @param string $machinename 541 * @param int $majorversion 542 * @param int $minorversion 543 * @return string Relative path 544 */ 545 public function getUpgradeScript($machinename, $majorversion, $minorversion) { 546 $path = '/' . "{$machinename}-{$majorversion}.{$minorversion}" . '/'; 547 $file = 'upgrade.js'; 548 $itemid = $this->get_itemid_for_file(self::LIBRARY_FILEAREA, $path, $file); 549 if ($this->fs->get_file($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $itemid, $path, $file)) { 550 return '/' . self::LIBRARY_FILEAREA . $path. $file; 551 } else { 552 return null; 553 } 554 } 555 556 /** 557 * Store the given stream into the given file. 558 * 559 * @param string $path 560 * @param string $file 561 * @param resource $stream 562 * @return bool|int 563 */ 564 public function saveFileFromZip($path, $file, $stream) { 565 $fullpath = $path . '/' . $file; 566 check_dir_exists(pathinfo($fullpath, PATHINFO_DIRNAME)); 567 return file_put_contents($fullpath, $stream); 568 } 569 570 /** 571 * Deletes a library from the file system. 572 * 573 * @param array $library Library details 574 */ 575 public function delete_library(array $library): void { 576 577 // A library ID of false would result in all library files being deleted, which we don't want. Return instead. 578 if ($library['libraryId'] === false) { 579 return; 580 } 581 582 $this->delete_directory($this->context->id, self::COMPONENT, self::LIBRARY_FILEAREA, $library['libraryId']); 583 } 584 585 /** 586 * Remove an H5P directory from the filesystem. 587 * 588 * @param int $contextid context ID 589 * @param string $component component 590 * @param string $filearea file area or all areas in context if not specified 591 * @param int $itemid item ID or all files if not specified 592 */ 593 private function delete_directory(int $contextid, string $component, string $filearea, int $itemid): void { 594 595 $this->fs->delete_area_files($contextid, $component, $filearea, $itemid); 596 } 597 598 /** 599 * Copy an H5P directory from the temporary directory into the file system. 600 * 601 * @param string $source Temporary location for files. 602 * @param array $options File system information. 603 */ 604 private function copy_directory(string $source, array $options): void { 605 $it = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), 606 \RecursiveIteratorIterator::SELF_FIRST); 607 608 $root = $options['filepath']; 609 610 $it->rewind(); 611 while ($it->valid()) { 612 $item = $it->current(); 613 $subpath = $it->getSubPath(); 614 if (!$item->isDir()) { 615 $options['filename'] = $it->getFilename(); 616 if (!$subpath == '') { 617 $options['filepath'] = $root . $subpath . '/'; 618 } else { 619 $options['filepath'] = $root; 620 } 621 622 $this->fs->create_file_from_pathname($options, $item->getPathName()); 623 } 624 $it->next(); 625 } 626 } 627 628 /** 629 * Copies files from storage to temporary folder. 630 * 631 * @param string $target Path to temporary folder 632 * @param int $contextid context where the files are found 633 * @param string $filearea file area 634 * @param string $filepath file path 635 * @param int $itemid Optional item ID 636 */ 637 private function export_file_tree(string $target, int $contextid, string $filearea, string $filepath, int $itemid = 0): void { 638 // Make sure target folder exists. 639 check_dir_exists($target); 640 641 // Read source files. 642 $files = $this->fs->get_directory_files($contextid, self::COMPONENT, $filearea, $itemid, $filepath, true); 643 644 foreach ($files as $file) { 645 $path = $target . str_replace($filepath, DIRECTORY_SEPARATOR, $file->get_filepath()); 646 if ($file->is_directory()) { 647 check_dir_exists(rtrim($path)); 648 } else { 649 $file->copy_content_to($path . $file->get_filename()); 650 } 651 } 652 } 653 654 /** 655 * Adds all files of a type into one file. 656 * 657 * @param array $assets A list of files. 658 * @param string $type The type of files in assets. Either 'scripts' or 'styles' 659 * @param \context $context Context 660 * @return string All of the file content in one string. 661 */ 662 private function concatenate_files(array $assets, string $type, \context $context): string { 663 $content = ''; 664 foreach ($assets as $asset) { 665 // Find location of asset. 666 list( 667 'filearea' => $filearea, 668 'filepath' => $filepath, 669 'filename' => $filename, 670 'itemid' => $itemid 671 ) = $this->get_file_elements_from_filepath($asset->path); 672 673 if ($itemid === false) { 674 continue; 675 } 676 677 // Locate file. 678 $file = $this->fs->get_file($context->id, self::COMPONENT, $filearea, $itemid, $filepath, $filename); 679 680 // Get file content and concatenate. 681 if ($type === 'scripts') { 682 $content .= $file->get_content() . ";\n"; 683 } else { 684 // Rewrite relative URLs used inside stylesheets. 685 $content .= preg_replace_callback( 686 '/url\([\'"]?([^"\')]+)[\'"]?\)/i', 687 function ($matches) use ($filearea, $filepath, $itemid) { 688 if (preg_match("/^(data:|([a-z0-9]+:)?\/)/i", $matches[1]) === 1) { 689 return $matches[0]; // Not relative, skip. 690 } 691 // Find "../" in matches[1]. 692 // If it exists, we have to remove "../". 693 // And switch the last folder in the filepath for the first folder in $matches[1]. 694 // For instance: 695 // $filepath: /H5P.Question-1.4/styles/ 696 // $matches[1]: ../images/plus-one.svg 697 // We want to avoid this: H5P.Question-1.4/styles/ITEMID/../images/minus-one.svg 698 // We want this: H5P.Question-1.4/images/ITEMID/minus-one.svg. 699 if (preg_match('/\.\.\//', $matches[1], $pathmatches)) { 700 $path = preg_split('/\//', $filepath, -1, PREG_SPLIT_NO_EMPTY); 701 $pathfilename = preg_split('/\//', $matches[1], -1, PREG_SPLIT_NO_EMPTY); 702 // Remove the first element: ../. 703 array_shift($pathfilename); 704 // Replace pathfilename into the filepath. 705 $path[count($path) - 1] = $pathfilename[0]; 706 $filepath = '/' . implode('/', $path) . '/'; 707 // Remove the element used to replace. 708 array_shift($pathfilename); 709 $matches[1] = implode('/', $pathfilename); 710 } 711 return 'url("../' . $filearea . $filepath . $itemid . '/' . $matches[1] . '")'; 712 }, 713 $file->get_content()) . "\n"; 714 } 715 } 716 return $content; 717 } 718 719 /** 720 * Get files ready for export. 721 * 722 * @param string $filename File name to retrieve. 723 * @return bool|\stored_file Stored file instance if exists, false if not 724 */ 725 public function get_export_file(string $filename) { 726 return $this->fs->get_file($this->context->id, self::COMPONENT, self::EXPORT_FILEAREA, 0, '/', $filename); 727 } 728 729 /** 730 * Converts a relative system file path into Moodle File API elements. 731 * 732 * @param string $filepath The system filepath to get information from. 733 * @return array File information. 734 */ 735 private function get_file_elements_from_filepath(string $filepath): array { 736 $sections = explode('/', $filepath); 737 // Get the filename. 738 $filename = array_pop($sections); 739 // Discard first element. 740 if (empty($sections[0])) { 741 array_shift($sections); 742 } 743 // Get the filearea. 744 $filearea = array_shift($sections); 745 $itemid = array_shift($sections); 746 // Get the filepath. 747 $filepath = implode('/', $sections); 748 $filepath = '/' . $filepath . '/'; 749 750 return ['filearea' => $filearea, 'filepath' => $filepath, 'filename' => $filename, 'itemid' => $itemid]; 751 } 752 753 /** 754 * Returns the item id given the other necessary variables. 755 * 756 * @param string $filearea The file area. 757 * @param string $filepath The file path. 758 * @param string $filename The file name. 759 * @return mixed the specified value false if not found. 760 */ 761 private function get_itemid_for_file(string $filearea, string $filepath, string $filename) { 762 global $DB; 763 return $DB->get_field('files', 'itemid', ['component' => self::COMPONENT, 'filearea' => $filearea, 'filepath' => $filepath, 764 'filename' => $filename]); 765 } 766 767 /** 768 * Helper to make it easy to load content files. 769 * 770 * @param string $filearea File area where the file is saved. 771 * @param int $itemid Content instance or content id. 772 * @param string $file File path and name. 773 * 774 * @return stored_file|null 775 */ 776 private function get_file(string $filearea, int $itemid, string $file): ?stored_file { 777 global $USER; 778 779 $component = self::COMPONENT; 780 $context = $this->context->id; 781 if ($filearea === 'draft') { 782 $itemid = 0; 783 $component = 'user'; 784 $usercontext = \context_user::instance($USER->id); 785 $context = $usercontext->id; 786 } 787 788 $filepath = '/'. dirname($file). '/'; 789 $filename = basename($file); 790 791 // Load file. 792 $existingfile = $this->fs->get_file($context, $component, $filearea, $itemid, $filepath, $filename); 793 if (!$existingfile) { 794 return null; 795 } 796 797 return $existingfile; 798 } 799 800 /** 801 * Move a single file 802 * 803 * @param string $sourcefile Path to source file 804 * @param int $contentid Content id or 0 if the file is in the editor file area 805 * 806 * @return void 807 */ 808 private function move_file(string $sourcefile, int $contentid): void { 809 $pathparts = pathinfo($sourcefile); 810 $filename = $pathparts['basename']; 811 $filepath = $pathparts['dirname']; 812 $foldername = basename($filepath); 813 814 // Create file record for content. 815 $record = array( 816 'contextid' => $this->context->id, 817 'component' => $contentid > 0 ? self::COMPONENT : 'user', 818 'filearea' => $contentid > 0 ? self::CONTENT_FILEAREA : 'draft', 819 'itemid' => $contentid > 0 ? $contentid : 0, 820 'filepath' => '/' . $foldername . '/', 821 'filename' => $filename 822 ); 823 824 $file = $this->fs->get_file( 825 $record['contextid'], $record['component'], 826 $record['filearea'], $record['itemid'], $record['filepath'], 827 $record['filename'] 828 ); 829 830 if ($file) { 831 // Delete it to make sure that it is replaced with correct content. 832 $file->delete(); 833 } 834 835 $this->fs->create_file_from_pathname($record, $sourcefile); 836 } 837 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body