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