Differences Between: [Versions 310 and 311] [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]
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 // Whilst get_imageinfo_from_path can use remote paths, it must download the entire file first. 369 // It is more efficient to use a local file when possible. 370 return $this->get_imageinfo_from_path($this->get_local_path_from_storedfile($file, true)); 371 } 372 373 /** 374 * Attempt to determine whether the specified file is likely to be an 375 * image. 376 * Since this relies upon the mimetype stored in the files table, there 377 * may be times when this information is not 100% accurate. 378 * 379 * @param stored_file $file The file to check 380 * @return bool 381 */ 382 public function is_image_from_storedfile(stored_file $file) { 383 if (!$file->get_filesize()) { 384 // An empty file cannot be an image. 385 return false; 386 } 387 388 $mimetype = $file->get_mimetype(); 389 if (!preg_match('|^image/|', $mimetype)) { 390 // The mimetype does not include image. 391 return false; 392 } 393 394 // If it looks like an image, and it smells like an image, perhaps it's an image! 395 return true; 396 } 397 398 /** 399 * Returns image information relating to the specified path or URL. 400 * 401 * @param string $path The full path of the image file. 402 * @return array|bool array that containing width, height, and mimetype or false if cannot get the image info. 403 */ 404 protected function get_imageinfo_from_path($path) { 405 $imagemimetype = file_storage::mimetype_from_file($path); 406 $issvgimage = file_is_svg_image_from_mimetype($imagemimetype); 407 408 if (!$issvgimage) { 409 $imageinfo = getimagesize($path); 410 if (!is_array($imageinfo)) { 411 return false; // Nothing to process, the file was not recognised as image by GD. 412 } 413 $image = [ 414 'width' => $imageinfo[0], 415 'height' => $imageinfo[1], 416 'mimetype' => image_type_to_mime_type($imageinfo[2]), 417 ]; 418 } else { 419 // Since SVG file is actually an XML file, GD cannot handle. 420 $svgcontent = @simplexml_load_file($path); 421 if (!$svgcontent) { 422 // Cannot parse the file. 423 return false; 424 } 425 $svgattrs = $svgcontent->attributes(); 426 427 if (!empty($svgattrs->viewBox)) { 428 // We have viewBox. 429 $viewboxval = explode(' ', $svgattrs->viewBox); 430 $width = intval($viewboxval[2]); 431 $height = intval($viewboxval[3]); 432 } else { 433 // Get the width. 434 if (!empty($svgattrs->width) && intval($svgattrs->width) > 0) { 435 $width = intval($svgattrs->width); 436 } else { 437 // Default width. 438 $width = 800; 439 } 440 // Get the height. 441 if (!empty($svgattrs->height) && intval($svgattrs->height) > 0) { 442 $height = intval($svgattrs->height); 443 } else { 444 // Default width. 445 $height = 600; 446 } 447 } 448 449 $image = [ 450 'width' => $width, 451 'height' => $height, 452 'mimetype' => $imagemimetype, 453 ]; 454 } 455 456 if (empty($image['width']) or empty($image['height']) or empty($image['mimetype'])) { 457 // GD can not parse it, sorry. 458 return false; 459 } 460 return $image; 461 } 462 463 /** 464 * Serve file content using X-Sendfile header. 465 * Please make sure that all headers are already sent and the all 466 * access control checks passed. 467 * 468 * This alternate method to xsendfile() allows an alternate file system 469 * to use the full file metadata and avoid extra lookups. 470 * 471 * @param stored_file $file The file to send 472 * @return bool success 473 */ 474 public function xsendfile_file(stored_file $file): bool { 475 return $this->xsendfile($file->get_contenthash()); 476 } 477 478 /** 479 * Serve file content using X-Sendfile header. 480 * Please make sure that all headers are already sent and the all 481 * access control checks passed. 482 * 483 * @param string $contenthash The content hash of the file to be served 484 * @return bool success 485 */ 486 public function xsendfile($contenthash) { 487 global $CFG; 488 require_once($CFG->libdir . "/xsendfilelib.php"); 489 490 return xsendfile($this->get_remote_path_from_hash($contenthash)); 491 } 492 493 /** 494 * Returns true if filesystem is configured to support xsendfile. 495 * 496 * @return bool 497 */ 498 public function supports_xsendfile() { 499 global $CFG; 500 return !empty($CFG->xsendfile); 501 } 502 503 /** 504 * Validate that the content hash matches the content hash of the file on disk. 505 * 506 * @param string $contenthash The current content hash to validate 507 * @param string $pathname The path to the file on disk 508 * @return array The content hash (it might change) and file size 509 */ 510 protected function validate_hash_and_file_size($contenthash, $pathname) { 511 global $CFG; 512 513 if (!is_readable($pathname)) { 514 throw new file_exception('storedfilecannotread', '', $pathname); 515 } 516 517 $filesize = filesize($pathname); 518 if ($filesize === false) { 519 throw new file_exception('storedfilecannotread', '', $pathname); 520 } 521 522 if (is_null($contenthash)) { 523 $contenthash = file_storage::hash_from_path($pathname); 524 } else if ($CFG->debugdeveloper) { 525 $filehash = file_storage::hash_from_path($pathname); 526 if ($filehash === false) { 527 throw new file_exception('storedfilecannotread', '', $pathname); 528 } 529 if ($filehash !== $contenthash) { 530 // Hopefully this never happens, if yes we need to fix calling code. 531 debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER); 532 $contenthash = $filehash; 533 } 534 } 535 if ($contenthash === false) { 536 throw new file_exception('storedfilecannotread', '', $pathname); 537 } 538 539 if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) { 540 // Did the file change or is file_storage::hash_from_path() borked for this file? 541 clearstatcache(); 542 $contenthash = file_storage::hash_from_path($pathname); 543 $filesize = filesize($pathname); 544 545 if ($contenthash === false or $filesize === false) { 546 throw new file_exception('storedfilecannotread', '', $pathname); 547 } 548 if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) { 549 // This is very weird... 550 throw new file_exception('storedfilecannotread', '', $pathname); 551 } 552 } 553 554 return [$contenthash, $filesize]; 555 } 556 557 /** 558 * Add the supplied file to the file system. 559 * 560 * Note: If overriding this function, it is advisable to store the file 561 * in the path returned by get_local_path_from_hash as there may be 562 * subsequent uses of the file in the same request. 563 * 564 * @param string $pathname Path to file currently on disk 565 * @param string $contenthash SHA1 hash of content if known (performance only) 566 * @return array (contenthash, filesize, newfile) 567 */ 568 abstract public function add_file_from_path($pathname, $contenthash = null); 569 570 /** 571 * Add a file with the supplied content to the file system. 572 * 573 * Note: If overriding this function, it is advisable to store the file 574 * in the path returned by get_local_path_from_hash as there may be 575 * subsequent uses of the file in the same request. 576 * 577 * @param string $content file content - binary string 578 * @return array (contenthash, filesize, newfile) 579 */ 580 abstract public function add_file_from_string($content); 581 582 /** 583 * Returns file handle - read only mode, no writing allowed into pool files! 584 * 585 * When you want to modify a file, create a new file and delete the old one. 586 * 587 * @param stored_file $file The file to retrieve a handle for 588 * @param int $type Type of file handle (FILE_HANDLE_xx constant) 589 * @return resource file handle 590 */ 591 public function get_content_file_handle(stored_file $file, $type = stored_file::FILE_HANDLE_FOPEN) { 592 if ($type === stored_file::FILE_HANDLE_GZOPEN) { 593 // Local file required for gzopen. 594 $path = $this->get_local_path_from_storedfile($file, true); 595 } else { 596 $path = $this->get_remote_path_from_storedfile($file); 597 } 598 599 return self::get_file_handle_for_path($path, $type); 600 } 601 602 /** 603 * Return a file handle for the specified path. 604 * 605 * This abstraction should be used when overriding get_content_file_handle in a new file system. 606 * 607 * @param string $path The path to the file. This shoudl be any type of path that fopen and gzopen accept. 608 * @param int $type Type of file handle (FILE_HANDLE_xx constant) 609 * @return resource 610 * @throws coding_exception When an unexpected type of file handle is requested 611 */ 612 protected static function get_file_handle_for_path($path, $type = stored_file::FILE_HANDLE_FOPEN) { 613 switch ($type) { 614 case stored_file::FILE_HANDLE_FOPEN: 615 // Binary reading. 616 return fopen($path, 'rb'); 617 case stored_file::FILE_HANDLE_GZOPEN: 618 // Binary reading of file in gz format. 619 return gzopen($path, 'rb'); 620 default: 621 throw new coding_exception('Unexpected file handle type'); 622 } 623 } 624 625 /** 626 * Retrieve the mime information for the specified stored file. 627 * 628 * @param string $contenthash 629 * @param string $filename 630 * @return string The MIME type. 631 */ 632 public function mimetype_from_hash($contenthash, $filename) { 633 $pathname = $this->get_local_path_from_hash($contenthash); 634 $mimetype = file_storage::mimetype($pathname, $filename); 635 636 if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) { 637 // The type is unknown, but the full checks weren't completed because the file isn't locally available. 638 // Ensure we have a local copy and try again. 639 $pathname = $this->get_local_path_from_hash($contenthash, true); 640 $mimetype = file_storage::mimetype_from_file($pathname); 641 } 642 643 return $mimetype; 644 } 645 646 /** 647 * Retrieve the mime information for the specified stored file. 648 * 649 * @param stored_file $file The stored file to retrieve mime information for 650 * @return string The MIME type. 651 */ 652 public function mimetype_from_storedfile($file) { 653 if (!$file->get_filesize()) { 654 // Files with an empty filesize are treated as directories and have no mimetype. 655 return null; 656 } 657 return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename()); 658 } 659 660 /** 661 * Run any periodic tasks which must be performed. 662 */ 663 public function cron() { 664 } 665 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body