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