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