See Release Notes
Long Term Support Release
Differences Between: [Versions 310 and 401] [Versions 311 and 401] [Versions 39 and 401] [Versions 400 and 401] [Versions 401 and 402] [Versions 401 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 * Core file system class definition. 19 * 20 * @package core_files 21 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 defined('MOODLE_INTERNAL') || die(); 26 27 /** 28 * File system class used for low level access to real files in filedir. 29 * 30 * @package core_files 31 * @category files 32 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk> 33 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 34 */ 35 abstract class file_system { 36 37 /** 38 * Output the content of the specified stored file. 39 * 40 * Note, this is different to get_content() as it uses the built-in php 41 * readfile function which is more efficient. 42 * 43 * @param stored_file $file The file to serve. 44 * @return void 45 */ 46 public function readfile(stored_file $file) { 47 if ($this->is_file_readable_locally_by_storedfile($file, false)) { 48 $path = $this->get_local_path_from_storedfile($file, false); 49 } else { 50 $path = $this->get_remote_path_from_storedfile($file); 51 } 52 if (readfile_allow_large($path, $file->get_filesize()) === false) { 53 throw new file_exception('storedfilecannotreadfile', $file->get_filename()); 54 } 55 } 56 57 /** 58 * Get the full path on disk for the specified stored file. 59 * 60 * Note: This must return a consistent path for the file's contenthash 61 * and the path _will_ be in a standard local format. 62 * Streamable paths will not work. 63 * A local copy of the file _will_ be fetched if $fetchifnotfound is tree. 64 * 65 * The $fetchifnotfound allows you to determine the expected path of the file. 66 * 67 * @param stored_file $file The file to serve. 68 * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found. 69 * @return string full path to pool file with file content 70 */ 71 public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) { 72 return $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound); 73 } 74 75 /** 76 * Get a remote filepath for the specified stored file. 77 * 78 * This is typically either the same as the local filepath, or it is a streamable resource. 79 * 80 * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers. 81 * 82 * @param stored_file $file The file to serve. 83 * @return string full path to pool file with file content 84 */ 85 public function get_remote_path_from_storedfile(stored_file $file) { 86 return $this->get_remote_path_from_hash($file->get_contenthash(), false); 87 } 88 89 /** 90 * Get the full path for the specified hash, including the path to the filedir. 91 * 92 * Note: This must return a consistent path for the file's contenthash 93 * and the path _will_ be in a standard local format. 94 * Streamable paths will not work. 95 * A local copy of the file _will_ be fetched if $fetchifnotfound is tree. 96 * 97 * The $fetchifnotfound allows you to determine the expected path of the file. 98 * 99 * @param string $contenthash The content hash 100 * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found. 101 * @return string The full path to the content file 102 */ 103 abstract protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false); 104 105 /** 106 * Get the full path for the specified hash, including the path to the filedir. 107 * 108 * This is typically either the same as the local filepath, or it is a streamable resource. 109 * 110 * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers. 111 * 112 * @param string $contenthash The content hash 113 * @return string The full path to the content file 114 */ 115 abstract protected function get_remote_path_from_hash($contenthash); 116 117 /** 118 * Determine whether the file is present on the file system somewhere. 119 * A local copy of the file _will_ be fetched if $fetchifnotfound is tree. 120 * 121 * The $fetchifnotfound allows you to determine the expected path of the file. 122 * 123 * @param stored_file $file The file to ensure is available. 124 * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found. 125 * @return bool 126 */ 127 public function is_file_readable_locally_by_storedfile(stored_file $file, $fetchifnotfound = false) { 128 if (!$file->get_filesize()) { 129 // Files with empty size are either directories or empty. 130 // We handle these virtually. 131 return true; 132 } 133 134 // Check to see if the file is currently readable. 135 $path = $this->get_local_path_from_storedfile($file, $fetchifnotfound); 136 if (is_readable($path)) { 137 return true; 138 } 139 140 return false; 141 } 142 143 /** 144 * Determine whether the file is present on the local file system somewhere. 145 * 146 * @param stored_file $file The file to ensure is available. 147 * @return bool 148 */ 149 public function is_file_readable_remotely_by_storedfile(stored_file $file) { 150 if (!$file->get_filesize()) { 151 // Files with empty size are either directories or empty. 152 // We handle these virtually. 153 return true; 154 } 155 156 $path = $this->get_remote_path_from_storedfile($file, false); 157 if (is_readable($path)) { 158 return true; 159 } 160 161 return false; 162 } 163 164 /** 165 * Determine whether the file is present on the file system somewhere given 166 * the contenthash. 167 * 168 * @param string $contenthash The contenthash of the file to check. 169 * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found. 170 * @return bool 171 */ 172 public function is_file_readable_locally_by_hash($contenthash, $fetchifnotfound = false) { 173 if ($contenthash === file_storage::hash_from_string('')) { 174 // Files with empty size are either directories or empty. 175 // We handle these virtually. 176 return true; 177 } 178 179 // This is called by file_storage::content_exists(), and in turn by the repository system. 180 $path = $this->get_local_path_from_hash($contenthash, $fetchifnotfound); 181 182 // Note - it is not possible to perform a content recovery safely from a hash alone. 183 return is_readable($path); 184 } 185 186 /** 187 * Determine whether the file is present locally on the file system somewhere given 188 * the contenthash. 189 * 190 * @param string $contenthash The contenthash of the file to check. 191 * @return bool 192 */ 193 public function is_file_readable_remotely_by_hash($contenthash) { 194 if ($contenthash === file_storage::hash_from_string('')) { 195 // Files with empty size are either directories or empty. 196 // We handle these virtually. 197 return true; 198 } 199 200 $path = $this->get_remote_path_from_hash($contenthash, false); 201 202 // Note - it is not possible to perform a content recovery safely from a hash alone. 203 return is_readable($path); 204 } 205 206 /** 207 * Copy content of file to given pathname. 208 * 209 * @param stored_file $file The file to be copied 210 * @param string $target real path to the new file 211 * @return bool success 212 */ 213 abstract public function copy_content_from_storedfile(stored_file $file, $target); 214 215 /** 216 * Remove the file with the specified contenthash. 217 * 218 * Note, if overriding this function, you _must_ check that the file is 219 * no longer in use - see {check_file_usage}. 220 * 221 * DO NOT call directly - reserved for core!! 222 * 223 * @param string $contenthash 224 */ 225 abstract public function remove_file($contenthash); 226 227 /** 228 * Check whether a file is removable. 229 * 230 * This must be called prior to file removal. 231 * 232 * @param string $contenthash 233 * @return bool 234 */ 235 protected static function is_file_removable($contenthash) { 236 global $DB; 237 238 if ($contenthash === file_storage::hash_from_string('')) { 239 // No need to delete files without content. 240 return false; 241 } 242 243 // Note: This section is critical - in theory file could be reused at the same time, if this 244 // happens we can still recover the file from trash. 245 // Technically this is the responsibility of the file_storage API, but as this method is public, we go belt-and-braces. 246 if ($DB->record_exists('files', array('contenthash' => $contenthash))) { 247 // File content is still used. 248 return false; 249 } 250 251 return true; 252 } 253 254 /** 255 * Get the content of the specified stored file. 256 * 257 * Generally you will probably want to use readfile() to serve content, 258 * and where possible you should see if you can use 259 * get_content_file_handle and work with the file stream instead. 260 * 261 * @param stored_file $file The file to retrieve 262 * @return string The full file content 263 */ 264 public function get_content(stored_file $file) { 265 if (!$file->get_filesize()) { 266 // Directories are empty. Empty files are not worth fetching. 267 return ''; 268 } 269 270 $source = $this->get_remote_path_from_storedfile($file); 271 return file_get_contents($source); 272 } 273 274 /** 275 * List contents of archive. 276 * 277 * @param stored_file $file The archive to inspect 278 * @param file_packer $packer file packer instance 279 * @return array of file infos 280 */ 281 public function list_files($file, file_packer $packer) { 282 $archivefile = $this->get_local_path_from_storedfile($file, true); 283 return $packer->list_files($archivefile); 284 } 285 286 /** 287 * Extract file to given file path (real OS filesystem), existing files are overwritten. 288 * 289 * @param stored_file $file The archive to inspect 290 * @param file_packer $packer File packer instance 291 * @param string $pathname Target directory 292 * @param file_progress $progress progress indicator callback or null if not required 293 * @return array|bool List of processed files; false if error 294 */ 295 public function extract_to_pathname(stored_file $file, file_packer $packer, $pathname, file_progress $progress = null) { 296 $archivefile = $this->get_local_path_from_storedfile($file, true); 297 return $packer->extract_to_pathname($archivefile, $pathname, null, $progress); 298 } 299 300 /** 301 * Extract file to given file path (real OS filesystem), existing files are overwritten. 302 * 303 * @param stored_file $file The archive to inspect 304 * @param file_packer $packer file packer instance 305 * @param int $contextid context ID 306 * @param string $component component 307 * @param string $filearea file area 308 * @param int $itemid item ID 309 * @param string $pathbase path base 310 * @param int $userid user ID 311 * @param file_progress $progress Progress indicator callback or null if not required 312 * @return array|bool list of processed files; false if error 313 */ 314 public function extract_to_storage(stored_file $file, file_packer $packer, $contextid, 315 $component, $filearea, $itemid, $pathbase, $userid = null, file_progress $progress = null) { 316 317 // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here. 318 $archivefile = $this->get_local_path_from_storedfile($file, true); 319 return $packer->extract_to_storage($archivefile, $contextid, 320 $component, $filearea, $itemid, $pathbase, $userid, $progress); 321 } 322 323 /** 324 * Add file/directory into archive. 325 * 326 * @param stored_file $file The file to archive 327 * @param file_archive $filearch file archive instance 328 * @param string $archivepath pathname in archive 329 * @return bool success 330 */ 331 public function add_storedfile_to_archive(stored_file $file, file_archive $filearch, $archivepath) { 332 if ($file->is_directory()) { 333 return $filearch->add_directory($archivepath); 334 } else { 335 // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here. 336 return $filearch->add_file_from_pathname($archivepath, $this->get_local_path_from_storedfile($file, true)); 337 } 338 } 339 340 /** 341 * Adds this file path to a curl request (POST only). 342 * 343 * @param stored_file $file The file to add to the curl request 344 * @param curl $curlrequest The curl request object 345 * @param string $key What key to use in the POST request 346 * @return void 347 * This needs the fullpath for the storedfile :/ 348 * Can this be achieved in some other fashion? 349 */ 350 public function add_to_curl_request(stored_file $file, &$curlrequest, $key) { 351 // Note: curl_file_create does not work with remote paths. 352 $path = $this->get_local_path_from_storedfile($file, true); 353 $curlrequest->_tmp_file_post_params[$key] = curl_file_create($path, null, $file->get_filename()); 354 } 355 356 /** 357 * Returns information about image. 358 * Information is determined from the file content 359 * 360 * @param stored_file $file The file to inspect 361 * @return mixed array with width, height and mimetype; false if not an image 362 */ 363 public function get_imageinfo(stored_file $file) { 364 if (!$this->is_image_from_storedfile($file)) { 365 return false; 366 } 367 368 $hash = $file->get_contenthash(); 369 $cache = cache::make('core', 'file_imageinfo'); 370 $info = $cache->get($hash); 371 if ($info !== false) { 372 return $info; 373 } 374 375 // Whilst get_imageinfo_from_path can use remote paths, it must download the entire file first. 376 // It is more efficient to use a local file when possible. 377 $info = $this->get_imageinfo_from_path($this->get_local_path_from_storedfile($file, true)); 378 $cache->set($hash, $info); 379 return $info; 380 } 381 382 /** 383 * Attempt to determine whether the specified file is likely to be an 384 * image. 385 * Since this relies upon the mimetype stored in the files table, there 386 * may be times when this information is not 100% accurate. 387 * 388 * @param stored_file $file The file to check 389 * @return bool 390 */ 391 public function is_image_from_storedfile(stored_file $file) { 392 if (!$file->get_filesize()) { 393 // An empty file cannot be an image. 394 return false; 395 } 396 397 $mimetype = $file->get_mimetype(); 398 if (!preg_match('|^image/|', $mimetype)) { 399 // The mimetype does not include image. 400 return false; 401 } 402 403 // If it looks like an image, and it smells like an image, perhaps it's an image! 404 return true; 405 } 406 407 /** 408 * Returns image information relating to the specified path or URL. 409 * 410 * @param string $path The full path of the image file. 411 * @return array|bool array that containing width, height, and mimetype or false if cannot get the image info. 412 */ 413 protected function get_imageinfo_from_path($path) { 414 $imagemimetype = file_storage::mimetype_from_file($path); 415 $issvgimage = file_is_svg_image_from_mimetype($imagemimetype); 416 417 if (!$issvgimage) { 418 $imageinfo = getimagesize($path); 419 if (!is_array($imageinfo)) { 420 return false; // Nothing to process, the file was not recognised as image by GD. 421 } 422 $image = [ 423 'width' => $imageinfo[0], 424 'height' => $imageinfo[1], 425 'mimetype' => image_type_to_mime_type($imageinfo[2]), 426 ]; 427 } else { 428 // Since SVG file is actually an XML file, GD cannot handle. 429 $svgcontent = @simplexml_load_file($path); 430 if (!$svgcontent) { 431 // Cannot parse the file. 432 return false; 433 } 434 $svgattrs = $svgcontent->attributes(); 435 436 if (!empty($svgattrs->viewBox)) { 437 // We have viewBox. 438 $viewboxval = explode(' ', $svgattrs->viewBox); 439 $width = intval($viewboxval[2]); 440 $height = intval($viewboxval[3]); 441 } else { 442 // Get the width. 443 if (!empty($svgattrs->width) && intval($svgattrs->width) > 0) { 444 $width = intval($svgattrs->width); 445 } else { 446 // Default width. 447 $width = 800; 448 } 449 // Get the height. 450 if (!empty($svgattrs->height) && intval($svgattrs->height) > 0) { 451 $height = intval($svgattrs->height); 452 } else { 453 // Default width. 454 $height = 600; 455 } 456 } 457 458 $image = [ 459 'width' => $width, 460 'height' => $height, 461 'mimetype' => $imagemimetype, 462 ]; 463 } 464 465 if (empty($image['width']) or empty($image['height']) or empty($image['mimetype'])) { 466 // GD can not parse it, sorry. 467 return false; 468 } 469 return $image; 470 } 471 472 /** 473 * Serve file content using X-Sendfile header. 474 * Please make sure that all headers are already sent and the all 475 * access control checks passed. 476 * 477 * This alternate method to xsendfile() allows an alternate file system 478 * to use the full file metadata and avoid extra lookups. 479 * 480 * @param stored_file $file The file to send 481 * @return bool success 482 */ 483 public function xsendfile_file(stored_file $file): bool { 484 return $this->xsendfile($file->get_contenthash()); 485 } 486 487 /** 488 * Serve file content using X-Sendfile header. 489 * Please make sure that all headers are already sent and the all 490 * access control checks passed. 491 * 492 * @param string $contenthash The content hash of the file to be served 493 * @return bool success 494 */ 495 public function xsendfile($contenthash) { 496 global $CFG; 497 require_once($CFG->libdir . "/xsendfilelib.php"); 498 499 return xsendfile($this->get_remote_path_from_hash($contenthash)); 500 } 501 502 /** 503 * Returns true if filesystem is configured to support xsendfile. 504 * 505 * @return bool 506 */ 507 public function supports_xsendfile() { 508 global $CFG; 509 return !empty($CFG->xsendfile); 510 } 511 512 /** 513 * Validate that the content hash matches the content hash of the file on disk. 514 * 515 * @param string $contenthash The current content hash to validate 516 * @param string $pathname The path to the file on disk 517 * @return array The content hash (it might change) and file size 518 */ 519 protected function validate_hash_and_file_size($contenthash, $pathname) { 520 global $CFG; 521 522 if (!is_readable($pathname)) { 523 throw new file_exception('storedfilecannotread', '', $pathname); 524 } 525 526 $filesize = filesize($pathname); 527 if ($filesize === false) { 528 throw new file_exception('storedfilecannotread', '', $pathname); 529 } 530 531 if (is_null($contenthash)) { 532 $contenthash = file_storage::hash_from_path($pathname); 533 } else if ($CFG->debugdeveloper) { 534 $filehash = file_storage::hash_from_path($pathname); 535 if ($filehash === false) { 536 throw new file_exception('storedfilecannotread', '', $pathname); 537 } 538 if ($filehash !== $contenthash) { 539 // Hopefully this never happens, if yes we need to fix calling code. 540 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER); 541 $contenthash = $filehash; 542 } 543 } 544 if ($contenthash === false) { 545 throw new file_exception('storedfilecannotread', '', $pathname); 546 } 547 548 if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) { 549 // Did the file change or is file_storage::hash_from_path() borked for this file? 550 clearstatcache(); 551 $contenthash = file_storage::hash_from_path($pathname); 552 $filesize = filesize($pathname); 553 554 if ($contenthash === false or $filesize === false) { 555 throw new file_exception('storedfilecannotread', '', $pathname); 556 } 557 if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) { 558 // This is very weird... 559 throw new file_exception('storedfilecannotread', '', $pathname); 560 } 561 } 562 563 return [$contenthash, $filesize]; 564 } 565 566 /** 567 * Add the supplied file to the file system. 568 * 569 * Note: If overriding this function, it is advisable to store the file 570 * in the path returned by get_local_path_from_hash as there may be 571 * subsequent uses of the file in the same request. 572 * 573 * @param string $pathname Path to file currently on disk 574 * @param string $contenthash SHA1 hash of content if known (performance only) 575 * @return array (contenthash, filesize, newfile) 576 */ 577 abstract public function add_file_from_path($pathname, $contenthash = null); 578 579 /** 580 * Add a file with the supplied content to the file system. 581 * 582 * Note: If overriding this function, it is advisable to store the file 583 * in the path returned by get_local_path_from_hash as there may be 584 * subsequent uses of the file in the same request. 585 * 586 * @param string $content file content - binary string 587 * @return array (contenthash, filesize, newfile) 588 */ 589 abstract public function add_file_from_string($content); 590 591 /** 592 * Returns file handle - read only mode, no writing allowed into pool files! 593 * 594 * When you want to modify a file, create a new file and delete the old one. 595 * 596 * @param stored_file $file The file to retrieve a handle for 597 * @param int $type Type of file handle (FILE_HANDLE_xx constant) 598 * @return resource file handle 599 */ 600 public function get_content_file_handle(stored_file $file, $type = stored_file::FILE_HANDLE_FOPEN) { 601 if ($type === stored_file::FILE_HANDLE_GZOPEN) { 602 // Local file required for gzopen. 603 $path = $this->get_local_path_from_storedfile($file, true); 604 } else { 605 $path = $this->get_remote_path_from_storedfile($file); 606 } 607 608 return self::get_file_handle_for_path($path, $type); 609 } 610 611 /** 612 * Return a file handle for the specified path. 613 * 614 * This abstraction should be used when overriding get_content_file_handle in a new file system. 615 * 616 * @param string $path The path to the file. This shoudl be any type of path that fopen and gzopen accept. 617 * @param int $type Type of file handle (FILE_HANDLE_xx constant) 618 * @return resource 619 * @throws coding_exception When an unexpected type of file handle is requested 620 */ 621 protected static function get_file_handle_for_path($path, $type = stored_file::FILE_HANDLE_FOPEN) { 622 switch ($type) { 623 case stored_file::FILE_HANDLE_FOPEN: 624 // Binary reading. 625 return fopen($path, 'rb'); 626 case stored_file::FILE_HANDLE_GZOPEN: 627 // Binary reading of file in gz format. 628 return gzopen($path, 'rb'); 629 default: 630 throw new coding_exception('Unexpected file handle type'); 631 } 632 } 633 634 /** 635 * Retrieve the mime information for the specified stored file. 636 * 637 * @param string $contenthash 638 * @param string $filename 639 * @return string The MIME type. 640 */ 641 public function mimetype_from_hash($contenthash, $filename) { 642 $pathname = $this->get_local_path_from_hash($contenthash); 643 $mimetype = file_storage::mimetype($pathname, $filename); 644 645 if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) { 646 // The type is unknown, but the full checks weren't completed because the file isn't locally available. 647 // Ensure we have a local copy and try again. 648 $pathname = $this->get_local_path_from_hash($contenthash, true); 649 $mimetype = file_storage::mimetype_from_file($pathname); 650 } 651 652 return $mimetype; 653 } 654 655 /** 656 * Retrieve the mime information for the specified stored file. 657 * 658 * @param stored_file $file The stored file to retrieve mime information for 659 * @return string The MIME type. 660 */ 661 public function mimetype_from_storedfile($file) { 662 if (!$file->get_filesize()) { 663 // Files with an empty filesize are treated as directories and have no mimetype. 664 return null; 665 } 666 return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename()); 667 } 668 669 /** 670 * Run any periodic tasks which must be performed. 671 */ 672 public function cron() { 673 } 674 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body