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 /** 19 * Core file storage class definition. 20 * 21 * @package core_files 22 * @copyright 2008 Petr Skoda {@link http://skodak.org} 23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 24 */ 25 26 defined('MOODLE_INTERNAL') || die(); 27 28 require_once("$CFG->libdir/filestorage/stored_file.php"); 29 30 /** 31 * File storage class used for low level access to stored files. 32 * 33 * Only owner of file area may use this class to access own files, 34 * for example only code in mod/assignment/* may access assignment 35 * attachments. When some other part of moodle needs to access 36 * files of modules it has to use file_browser class instead or there 37 * has to be some callback API. 38 * 39 * @package core_files 40 * @category files 41 * @copyright 2008 Petr Skoda {@link http://skodak.org} 42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 43 * @since Moodle 2.0 44 */ 45 class file_storage { 46 47 /** @var string tempdir */ 48 private $tempdir; 49 50 /** @var file_system filesystem */ 51 private $filesystem; 52 53 /** 54 * Constructor - do not use directly use {@link get_file_storage()} call instead. 55 */ 56 public function __construct() { 57 // The tempdir must always remain on disk, but shared between all ndoes in a cluster. Its content is not subject 58 // to the file_system abstraction. 59 $this->tempdir = make_temp_directory('filestorage'); 60 61 $this->setup_file_system(); 62 } 63 64 /** 65 * Complete setup procedure for the file_system component. 66 * 67 * @return file_system 68 */ 69 public function setup_file_system() { 70 global $CFG; 71 if ($this->filesystem === null) { 72 require_once($CFG->libdir . '/filestorage/file_system.php'); 73 if (!empty($CFG->alternative_file_system_class)) { 74 $class = $CFG->alternative_file_system_class; 75 } else { 76 // The default file_system is the filedir. 77 require_once($CFG->libdir . '/filestorage/file_system_filedir.php'); 78 $class = file_system_filedir::class; 79 } 80 $this->filesystem = new $class(); 81 } 82 83 return $this->filesystem; 84 } 85 86 /** 87 * Return the file system instance. 88 * 89 * @return file_system 90 */ 91 public function get_file_system() { 92 return $this->filesystem; 93 } 94 95 /** 96 * Calculates sha1 hash of unique full path name information. 97 * 98 * This hash is a unique file identifier - it is used to improve 99 * performance and overcome db index size limits. 100 * 101 * @param int $contextid context ID 102 * @param string $component component 103 * @param string $filearea file area 104 * @param int $itemid item ID 105 * @param string $filepath file path 106 * @param string $filename file name 107 * @return string sha1 hash 108 */ 109 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) { 110 if (substr($filepath, 0, 1) != '/') { 111 $filepath = '/' . $filepath; 112 } 113 if (substr($filepath, - 1) != '/') { 114 $filepath .= '/'; 115 } 116 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename); 117 } 118 119 /** 120 * Does this file exist? 121 * 122 * @param int $contextid context ID 123 * @param string $component component 124 * @param string $filearea file area 125 * @param int $itemid item ID 126 * @param string $filepath file path 127 * @param string $filename file name 128 * @return bool 129 */ 130 public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) { 131 $filepath = clean_param($filepath, PARAM_PATH); 132 $filename = clean_param($filename, PARAM_FILE); 133 134 if ($filename === '') { 135 $filename = '.'; 136 } 137 138 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); 139 return $this->file_exists_by_hash($pathnamehash); 140 } 141 142 /** 143 * Whether or not the file exist 144 * 145 * @param string $pathnamehash path name hash 146 * @return bool 147 */ 148 public function file_exists_by_hash($pathnamehash) { 149 global $DB; 150 151 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash)); 152 } 153 154 /** 155 * Create instance of file class from database record. 156 * 157 * @param stdClass $filerecord record from the files table left join files_reference table 158 * @return stored_file instance of file abstraction class 159 */ 160 public function get_file_instance(stdClass $filerecord) { 161 $storedfile = new stored_file($this, $filerecord); 162 return $storedfile; 163 } 164 165 /** 166 * Get converted document. 167 * 168 * Get an alternate version of the specified document, if it is possible to convert. 169 * 170 * @param stored_file $file the file we want to preview 171 * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension. 172 * @param boolean $forcerefresh If true, the file will be converted every time (not cached). 173 * @return stored_file|bool false if unable to create the conversion, stored file otherwise 174 */ 175 public function get_converted_document(stored_file $file, $format, $forcerefresh = false) { 176 debugging('The get_converted_document function has been deprecated and the unoconv functions been removed. ' 177 . 'The file has not been converted. ' 178 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER); 179 180 return false; 181 } 182 183 /** 184 * Verify the format is supported. 185 * 186 * @param string $format The desired format - e.g. 'pdf'. Formats are specified by file extension. 187 * @return bool - True if the format is supported for input. 188 */ 189 protected function is_format_supported_by_unoconv($format) { 190 debugging('The is_format_supported_by_unoconv function has been deprecated and the unoconv functions been removed. ' 191 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER); 192 193 return false; 194 } 195 196 /** 197 * Check if the installed version of unoconv is supported. 198 * 199 * @return bool true if the present version is supported, false otherwise. 200 */ 201 public static function can_convert_documents() { 202 debugging('The can_convert_documents function has been deprecated and the unoconv functions been removed. ' 203 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER); 204 205 return false; 206 } 207 208 /** 209 * Regenerate the test pdf and send it direct to the browser. 210 */ 211 public static function send_test_pdf() { 212 debugging('The send_test_pdf function has been deprecated and the unoconv functions been removed. ' 213 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER); 214 215 return false; 216 } 217 218 /** 219 * Check if unoconv configured path is correct and working. 220 * 221 * @return \stdClass an object with the test status and the UNOCONVPATH_ constant message. 222 */ 223 public static function test_unoconv_path() { 224 debugging('The test_unoconv_path function has been deprecated and the unoconv functions been removed. ' 225 . 'Please update your code to use the file conversion API instead.', DEBUG_DEVELOPER); 226 227 return false; 228 } 229 230 /** 231 * Returns an image file that represent the given stored file as a preview 232 * 233 * At the moment, only GIF, JPEG, PNG and SVG files are supported to have previews. In the 234 * future, the support for other mimetypes can be added, too (eg. generate an image 235 * preview of PDF, text documents etc). 236 * 237 * @param stored_file $file the file we want to preview 238 * @param string $mode preview mode, eg. 'thumb' 239 * @return stored_file|bool false if unable to create the preview, stored file otherwise 240 */ 241 public function get_file_preview(stored_file $file, $mode) { 242 243 $context = context_system::instance(); 244 $path = '/' . trim($mode, '/') . '/'; 245 $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash()); 246 247 if (!$preview) { 248 $preview = $this->create_file_preview($file, $mode); 249 if (!$preview) { 250 return false; 251 } 252 } 253 254 return $preview; 255 } 256 257 /** 258 * Return an available file name. 259 * 260 * This will return the next available file name in the area, adding/incrementing a suffix 261 * of the file, ie: file.txt > file (1).txt > file (2).txt > etc... 262 * 263 * If the file name passed is available without modification, it is returned as is. 264 * 265 * @param int $contextid context ID. 266 * @param string $component component. 267 * @param string $filearea file area. 268 * @param int $itemid area item ID. 269 * @param string $filepath the file path. 270 * @param string $filename the file name. 271 * @return string available file name. 272 * @throws coding_exception if the file name is invalid. 273 * @since Moodle 2.5 274 */ 275 public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) { 276 global $DB; 277 278 // Do not accept '.' or an empty file name (zero is acceptable). 279 if ($filename == '.' || (empty($filename) && !is_numeric($filename))) { 280 throw new coding_exception('Invalid file name passed', $filename); 281 } 282 283 // The file does not exist, we return the same file name. 284 if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) { 285 return $filename; 286 } 287 288 // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first. 289 $pathinfo = pathinfo($filename); 290 $basename = $pathinfo['filename']; 291 $matches = array(); 292 if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) { 293 $basename = $matches[1]; 294 } 295 296 $filenamelike = $DB->sql_like_escape($basename) . ' (%)'; 297 if (isset($pathinfo['extension'])) { 298 $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']); 299 } 300 301 $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike'); 302 $filenamelen = $DB->sql_length('f.filename'); 303 $sql = "SELECT filename 304 FROM {files} f 305 WHERE 306 f.contextid = :contextid AND 307 f.component = :component AND 308 f.filearea = :filearea AND 309 f.itemid = :itemid AND 310 f.filepath = :filepath AND 311 $filenamelikesql 312 ORDER BY 313 $filenamelen DESC, 314 f.filename DESC"; 315 $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid, 316 'filepath' => $filepath, 'filenamelike' => $filenamelike); 317 $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE); 318 319 // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt' 320 // would both be returned, but only the one only containing digits should be used. 321 $number = 1; 322 foreach ($results as $result) { 323 $resultbasename = pathinfo($result, PATHINFO_FILENAME); 324 $matches = array(); 325 if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) { 326 $number = $matches[2] + 1; 327 break; 328 } 329 } 330 331 // Constructing the new filename. 332 $newfilename = $basename . ' (' . $number . ')'; 333 if (isset($pathinfo['extension'])) { 334 $newfilename .= '.' . $pathinfo['extension']; 335 } 336 337 return $newfilename; 338 } 339 340 /** 341 * Return an available directory name. 342 * 343 * This will return the next available directory name in the area, adding/incrementing a suffix 344 * of the last portion of path, ie: /path/ > /path (1)/ > /path (2)/ > etc... 345 * 346 * If the file path passed is available without modification, it is returned as is. 347 * 348 * @param int $contextid context ID. 349 * @param string $component component. 350 * @param string $filearea file area. 351 * @param int $itemid area item ID. 352 * @param string $suggestedpath the suggested file path. 353 * @return string available file path 354 * @since Moodle 2.5 355 */ 356 public function get_unused_dirname($contextid, $component, $filearea, $itemid, $suggestedpath) { 357 global $DB; 358 359 // Ensure suggestedpath has trailing '/' 360 $suggestedpath = rtrim($suggestedpath, '/'). '/'; 361 362 // The directory does not exist, we return the same file path. 363 if (!$this->file_exists($contextid, $component, $filearea, $itemid, $suggestedpath, '.')) { 364 return $suggestedpath; 365 } 366 367 // Trying to locate a file path using the used pattern. We remove the used pattern from the path first. 368 if (preg_match('~^(/.+) \(([0-9]+)\)/$~', $suggestedpath, $matches)) { 369 $suggestedpath = $matches[1]. '/'; 370 } 371 372 $filepathlike = $DB->sql_like_escape(rtrim($suggestedpath, '/')) . ' (%)/'; 373 374 $filepathlikesql = $DB->sql_like('f.filepath', ':filepathlike'); 375 $filepathlen = $DB->sql_length('f.filepath'); 376 $sql = "SELECT filepath 377 FROM {files} f 378 WHERE 379 f.contextid = :contextid AND 380 f.component = :component AND 381 f.filearea = :filearea AND 382 f.itemid = :itemid AND 383 f.filename = :filename AND 384 $filepathlikesql 385 ORDER BY 386 $filepathlen DESC, 387 f.filepath DESC"; 388 $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid, 389 'filename' => '.', 'filepathlike' => $filepathlike); 390 $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE); 391 392 // Loop over the results to make sure we are working on a valid file path. Because '/path (1)/' and '/path (copy)/' 393 // would both be returned, but only the one only containing digits should be used. 394 $number = 1; 395 foreach ($results as $result) { 396 if (preg_match('~ \(([0-9]+)\)/$~', $result, $matches)) { 397 $number = (int)($matches[1]) + 1; 398 break; 399 } 400 } 401 402 return rtrim($suggestedpath, '/'). ' (' . $number . ')/'; 403 } 404 405 /** 406 * Generates a preview image for the stored file 407 * 408 * @param stored_file $file the file we want to preview 409 * @param string $mode preview mode, eg. 'thumb' 410 * @return stored_file|bool the newly created preview file or false 411 */ 412 protected function create_file_preview(stored_file $file, $mode) { 413 414 $mimetype = $file->get_mimetype(); 415 416 if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') { 417 // make a preview of the image 418 $data = $this->create_imagefile_preview($file, $mode); 419 } else if ($mimetype === 'image/svg+xml') { 420 // If we have an SVG image, then return the original (scalable) file. 421 return $file; 422 } else { 423 // unable to create the preview of this mimetype yet 424 return false; 425 } 426 427 if (empty($data)) { 428 return false; 429 } 430 431 $context = context_system::instance(); 432 $record = array( 433 'contextid' => $context->id, 434 'component' => 'core', 435 'filearea' => 'preview', 436 'itemid' => 0, 437 'filepath' => '/' . trim($mode, '/') . '/', 438 'filename' => $file->get_contenthash(), 439 ); 440 441 $imageinfo = getimagesizefromstring($data); 442 if ($imageinfo) { 443 $record['mimetype'] = $imageinfo['mime']; 444 } 445 446 return $this->create_file_from_string($record, $data); 447 } 448 449 /** 450 * Generates a preview for the stored image file 451 * 452 * @param stored_file $file the image we want to preview 453 * @param string $mode preview mode, eg. 'thumb' 454 * @return string|bool false if a problem occurs, the thumbnail image data otherwise 455 */ 456 protected function create_imagefile_preview(stored_file $file, $mode) { 457 global $CFG; 458 require_once($CFG->libdir.'/gdlib.php'); 459 460 if ($mode === 'tinyicon') { 461 $data = $file->generate_image_thumbnail(24, 24); 462 463 } else if ($mode === 'thumb') { 464 $data = $file->generate_image_thumbnail(90, 90); 465 466 } else if ($mode === 'bigthumb') { 467 $data = $file->generate_image_thumbnail(250, 250); 468 469 } else { 470 throw new file_exception('storedfileproblem', 'Invalid preview mode requested'); 471 } 472 473 return $data; 474 } 475 476 /** 477 * Fetch file using local file id. 478 * 479 * Please do not rely on file ids, it is usually easier to use 480 * pathname hashes instead. 481 * 482 * @param int $fileid file ID 483 * @return stored_file|bool stored_file instance if exists, false if not 484 */ 485 public function get_file_by_id($fileid) { 486 global $DB; 487 488 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 489 FROM {files} f 490 LEFT JOIN {files_reference} r 491 ON f.referencefileid = r.id 492 WHERE f.id = ?"; 493 if ($filerecord = $DB->get_record_sql($sql, array($fileid))) { 494 return $this->get_file_instance($filerecord); 495 } else { 496 return false; 497 } 498 } 499 500 /** 501 * Fetch file using local file full pathname hash 502 * 503 * @param string $pathnamehash path name hash 504 * @return stored_file|bool stored_file instance if exists, false if not 505 */ 506 public function get_file_by_hash($pathnamehash) { 507 global $DB; 508 509 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 510 FROM {files} f 511 LEFT JOIN {files_reference} r 512 ON f.referencefileid = r.id 513 WHERE f.pathnamehash = ?"; 514 if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) { 515 return $this->get_file_instance($filerecord); 516 } else { 517 return false; 518 } 519 } 520 521 /** 522 * Fetch locally stored file. 523 * 524 * @param int $contextid context ID 525 * @param string $component component 526 * @param string $filearea file area 527 * @param int $itemid item ID 528 * @param string $filepath file path 529 * @param string $filename file name 530 * @return stored_file|bool stored_file instance if exists, false if not 531 */ 532 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) { 533 $filepath = clean_param($filepath, PARAM_PATH); 534 $filename = clean_param($filename, PARAM_FILE); 535 536 if ($filename === '') { 537 $filename = '.'; 538 } 539 540 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); 541 return $this->get_file_by_hash($pathnamehash); 542 } 543 544 /** 545 * Are there any files (or directories) 546 * 547 * @param int $contextid context ID 548 * @param string $component component 549 * @param string $filearea file area 550 * @param bool|int $itemid item id or false if all items 551 * @param bool $ignoredirs whether or not ignore directories 552 * @return bool empty 553 */ 554 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) { 555 global $DB; 556 557 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea); 558 $where = "contextid = :contextid AND component = :component AND filearea = :filearea"; 559 560 if ($itemid !== false) { 561 $params['itemid'] = $itemid; 562 $where .= " AND itemid = :itemid"; 563 } 564 565 if ($ignoredirs) { 566 $sql = "SELECT 'x' 567 FROM {files} 568 WHERE $where AND filename <> '.'"; 569 } else { 570 $sql = "SELECT 'x' 571 FROM {files} 572 WHERE $where AND (filename <> '.' OR filepath <> '/')"; 573 } 574 575 return !$DB->record_exists_sql($sql, $params); 576 } 577 578 /** 579 * Returns all files belonging to given repository 580 * 581 * @param int $repositoryid 582 * @param string $sort A fragment of SQL to use for sorting 583 */ 584 public function get_external_files($repositoryid, $sort = '') { 585 global $DB; 586 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 587 FROM {files} f 588 LEFT JOIN {files_reference} r 589 ON f.referencefileid = r.id 590 WHERE r.repositoryid = ?"; 591 if (!empty($sort)) { 592 $sql .= " ORDER BY {$sort}"; 593 } 594 595 $result = array(); 596 $filerecords = $DB->get_records_sql($sql, array($repositoryid)); 597 foreach ($filerecords as $filerecord) { 598 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 599 } 600 return $result; 601 } 602 603 /** 604 * Returns all area files (optionally limited by itemid) 605 * 606 * @param int $contextid context ID 607 * @param string $component component 608 * @param mixed $filearea file area/s, you cannot specify multiple fileareas as well as an itemid 609 * @param int|int[]|false $itemid item ID(s) or all files if not specified 610 * @param string $sort A fragment of SQL to use for sorting 611 * @param bool $includedirs whether or not include directories 612 * @param int $updatedsince return files updated since this time 613 * @param int $limitfrom return a subset of records, starting at this point (optional). 614 * @param int $limitnum return a subset comprising this many records in total (optional, required if $limitfrom is set). 615 * @return stored_file[] array of stored_files indexed by pathanmehash 616 */ 617 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", 618 $includedirs = true, $updatedsince = 0, $limitfrom = 0, $limitnum = 0) { 619 global $DB; 620 621 list($areasql, $conditions) = $DB->get_in_or_equal($filearea, SQL_PARAMS_NAMED); 622 $conditions['contextid'] = $contextid; 623 $conditions['component'] = $component; 624 625 if ($itemid !== false && is_array($filearea)) { 626 throw new coding_exception('You cannot specify multiple fileareas as well as an itemid.'); 627 } else if ($itemid !== false) { 628 $itemids = is_array($itemid) ? $itemid : [$itemid]; 629 list($itemidinorequalsql, $itemidconditions) = $DB->get_in_or_equal($itemids, SQL_PARAMS_NAMED); 630 $itemidsql = " AND f.itemid {$itemidinorequalsql}"; 631 $conditions = array_merge($conditions, $itemidconditions); 632 } else { 633 $itemidsql = ''; 634 } 635 636 $updatedsincesql = ''; 637 if (!empty($updatedsince)) { 638 $conditions['time'] = $updatedsince; 639 $updatedsincesql = 'AND f.timemodified > :time'; 640 } 641 642 $includedirssql = ''; 643 if (!$includedirs) { 644 $includedirssql = 'AND f.filename != :dot'; 645 $conditions['dot'] = '.'; 646 } 647 648 if ($limitfrom && !$limitnum) { 649 throw new coding_exception('If specifying $limitfrom you must also specify $limitnum'); 650 } 651 652 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 653 FROM {files} f 654 LEFT JOIN {files_reference} r 655 ON f.referencefileid = r.id 656 WHERE f.contextid = :contextid 657 AND f.component = :component 658 AND f.filearea $areasql 659 $includedirssql 660 $updatedsincesql 661 $itemidsql"; 662 if (!empty($sort)) { 663 $sql .= " ORDER BY {$sort}"; 664 } 665 666 $result = array(); 667 $filerecords = $DB->get_records_sql($sql, $conditions, $limitfrom, $limitnum); 668 foreach ($filerecords as $filerecord) { 669 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 670 } 671 return $result; 672 } 673 674 /** 675 * Returns the file area item ids and their updatetime for a user's draft uploads, sorted by updatetime DESC. 676 * 677 * @param int $userid user id 678 * @param int $updatedsince only return draft areas updated since this time 679 * @param int $lastnum only return the last specified numbers 680 * @return array 681 */ 682 public function get_user_draft_items(int $userid, int $updatedsince = 0, int $lastnum = 0): array { 683 global $DB; 684 685 $params = [ 686 'component' => 'user', 687 'filearea' => 'draft', 688 'contextid' => context_user::instance($userid)->id, 689 ]; 690 691 $updatedsincesql = ''; 692 if ($updatedsince) { 693 $updatedsincesql = 'AND f.timemodified > :time'; 694 $params['time'] = $updatedsince; 695 } 696 $sql = "SELECT itemid, 697 MAX(f.timemodified) AS timemodified 698 FROM {files} f 699 WHERE component = :component 700 AND filearea = :filearea 701 AND contextid = :contextid 702 $updatedsincesql 703 GROUP BY itemid 704 ORDER BY MAX(f.timemodified) DESC"; 705 706 return $DB->get_records_sql($sql, $params, 0, $lastnum); 707 } 708 709 /** 710 * Returns array based tree structure of area files 711 * 712 * @param int $contextid context ID 713 * @param string $component component 714 * @param string $filearea file area 715 * @param int $itemid item ID 716 * @return array each dir represented by dirname, subdirs, files and dirfile array elements 717 */ 718 public function get_area_tree($contextid, $component, $filearea, $itemid) { 719 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array()); 720 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true); 721 // first create directory structure 722 foreach ($files as $hash=>$dir) { 723 if (!$dir->is_directory()) { 724 continue; 725 } 726 unset($files[$hash]); 727 if ($dir->get_filepath() === '/') { 728 $result['dirfile'] = $dir; 729 continue; 730 } 731 $parts = explode('/', trim($dir->get_filepath(),'/')); 732 $pointer =& $result; 733 foreach ($parts as $part) { 734 if ($part === '') { 735 continue; 736 } 737 if (!isset($pointer['subdirs'][$part])) { 738 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array()); 739 } 740 $pointer =& $pointer['subdirs'][$part]; 741 } 742 $pointer['dirfile'] = $dir; 743 unset($pointer); 744 } 745 foreach ($files as $hash=>$file) { 746 $parts = explode('/', trim($file->get_filepath(),'/')); 747 $pointer =& $result; 748 foreach ($parts as $part) { 749 if ($part === '') { 750 continue; 751 } 752 $pointer =& $pointer['subdirs'][$part]; 753 } 754 $pointer['files'][$file->get_filename()] = $file; 755 unset($pointer); 756 } 757 $result = $this->sort_area_tree($result); 758 return $result; 759 } 760 761 /** 762 * Sorts the result of {@link file_storage::get_area_tree()}. 763 * 764 * @param array $tree Array of results provided by {@link file_storage::get_area_tree()} 765 * @return array of sorted results 766 */ 767 protected function sort_area_tree($tree) { 768 foreach ($tree as $key => &$value) { 769 if ($key == 'subdirs') { 770 core_collator::ksort($value, core_collator::SORT_NATURAL); 771 foreach ($value as $subdirname => &$subtree) { 772 $subtree = $this->sort_area_tree($subtree); 773 } 774 } else if ($key == 'files') { 775 core_collator::ksort($value, core_collator::SORT_NATURAL); 776 } 777 } 778 return $tree; 779 } 780 781 /** 782 * Returns all files and optionally directories 783 * 784 * @param int $contextid context ID 785 * @param string $component component 786 * @param string $filearea file area 787 * @param int $itemid item ID 788 * @param int $filepath directory path 789 * @param bool $recursive include all subdirectories 790 * @param bool $includedirs include files and directories 791 * @param string $sort A fragment of SQL to use for sorting 792 * @return array of stored_files indexed by pathanmehash 793 */ 794 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") { 795 global $DB; 796 797 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) { 798 return array(); 799 } 800 801 $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : ''; 802 803 if ($recursive) { 804 805 $dirs = $includedirs ? "" : "AND filename <> '.'"; 806 $length = core_text::strlen($filepath); 807 808 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 809 FROM {files} f 810 LEFT JOIN {files_reference} r 811 ON f.referencefileid = r.id 812 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid 813 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath 814 AND f.id <> :dirid 815 $dirs 816 $orderby"; 817 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id()); 818 819 $files = array(); 820 $dirs = array(); 821 $filerecords = $DB->get_records_sql($sql, $params); 822 foreach ($filerecords as $filerecord) { 823 if ($filerecord->filename == '.') { 824 $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 825 } else { 826 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 827 } 828 } 829 $result = array_merge($dirs, $files); 830 831 } else { 832 $result = array(); 833 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id()); 834 835 $length = core_text::strlen($filepath); 836 837 if ($includedirs) { 838 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 839 FROM {files} f 840 LEFT JOIN {files_reference} r 841 ON f.referencefileid = r.id 842 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea 843 AND f.itemid = :itemid AND f.filename = '.' 844 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath 845 AND f.id <> :dirid 846 $orderby"; 847 $reqlevel = substr_count($filepath, '/') + 1; 848 $filerecords = $DB->get_records_sql($sql, $params); 849 foreach ($filerecords as $filerecord) { 850 if (substr_count($filerecord->filepath, '/') !== $reqlevel) { 851 continue; 852 } 853 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 854 } 855 } 856 857 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 858 FROM {files} f 859 LEFT JOIN {files_reference} r 860 ON f.referencefileid = r.id 861 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid 862 AND f.filepath = :filepath AND f.filename <> '.' 863 $orderby"; 864 865 $filerecords = $DB->get_records_sql($sql, $params); 866 foreach ($filerecords as $filerecord) { 867 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 868 } 869 } 870 871 return $result; 872 } 873 874 /** 875 * Delete all area files (optionally limited by itemid). 876 * 877 * @param int $contextid context ID 878 * @param string $component component 879 * @param string $filearea file area or all areas in context if not specified 880 * @param int $itemid item ID or all files if not specified 881 * @return bool success 882 */ 883 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) { 884 global $DB; 885 886 $conditions = array('contextid'=>$contextid); 887 if ($component !== false) { 888 $conditions['component'] = $component; 889 } 890 if ($filearea !== false) { 891 $conditions['filearea'] = $filearea; 892 } 893 if ($itemid !== false) { 894 $conditions['itemid'] = $itemid; 895 } 896 897 $filerecords = $DB->get_records('files', $conditions); 898 foreach ($filerecords as $filerecord) { 899 $this->get_file_instance($filerecord)->delete(); 900 } 901 902 return true; // BC only 903 } 904 905 /** 906 * Delete all the files from certain areas where itemid is limited by an 907 * arbitrary bit of SQL. 908 * 909 * @param int $contextid the id of the context the files belong to. Must be given. 910 * @param string $component the owning component. Must be given. 911 * @param string $filearea the file area name. Must be given. 912 * @param string $itemidstest an SQL fragment that the itemid must match. Used 913 * in the query like WHERE itemid $itemidstest. Must used named parameters, 914 * and may not used named parameters called contextid, component or filearea. 915 * @param array $params any query params used by $itemidstest. 916 */ 917 public function delete_area_files_select($contextid, $component, 918 $filearea, $itemidstest, array $params = null) { 919 global $DB; 920 921 $where = "contextid = :contextid 922 AND component = :component 923 AND filearea = :filearea 924 AND itemid $itemidstest"; 925 $params['contextid'] = $contextid; 926 $params['component'] = $component; 927 $params['filearea'] = $filearea; 928 929 $filerecords = $DB->get_recordset_select('files', $where, $params); 930 foreach ($filerecords as $filerecord) { 931 $this->get_file_instance($filerecord)->delete(); 932 } 933 $filerecords->close(); 934 } 935 936 /** 937 * Delete all files associated with the given component. 938 * 939 * @param string $component the component owning the file 940 */ 941 public function delete_component_files($component) { 942 global $DB; 943 944 $filerecords = $DB->get_recordset('files', array('component' => $component)); 945 foreach ($filerecords as $filerecord) { 946 $this->get_file_instance($filerecord)->delete(); 947 } 948 $filerecords->close(); 949 } 950 951 /** 952 * Move all the files in a file area from one context to another. 953 * 954 * @param int $oldcontextid the context the files are being moved from. 955 * @param int $newcontextid the context the files are being moved to. 956 * @param string $component the plugin that these files belong to. 957 * @param string $filearea the name of the file area. 958 * @param int $itemid file item ID 959 * @return int the number of files moved, for information. 960 */ 961 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) { 962 // Note, this code is based on some code that Petr wrote in 963 // forum_move_attachments in mod/forum/lib.php. I moved it here because 964 // I needed it in the question code too. 965 $count = 0; 966 967 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false); 968 foreach ($oldfiles as $oldfile) { 969 $filerecord = new stdClass(); 970 $filerecord->contextid = $newcontextid; 971 $this->create_file_from_storedfile($filerecord, $oldfile); 972 $count += 1; 973 } 974 975 if ($count) { 976 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid); 977 } 978 979 return $count; 980 } 981 982 /** 983 * Recursively creates directory. 984 * 985 * @param int $contextid context ID 986 * @param string $component component 987 * @param string $filearea file area 988 * @param int $itemid item ID 989 * @param string $filepath file path 990 * @param int $userid the user ID 991 * @return bool success 992 */ 993 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) { 994 global $DB; 995 996 // validate all parameters, we do not want any rubbish stored in database, right? 997 if (!is_number($contextid) or $contextid < 1) { 998 throw new file_exception('storedfileproblem', 'Invalid contextid'); 999 } 1000 1001 $component = clean_param($component, PARAM_COMPONENT); 1002 if (empty($component)) { 1003 throw new file_exception('storedfileproblem', 'Invalid component'); 1004 } 1005 1006 $filearea = clean_param($filearea, PARAM_AREA); 1007 if (empty($filearea)) { 1008 throw new file_exception('storedfileproblem', 'Invalid filearea'); 1009 } 1010 1011 if (!is_number($itemid) or $itemid < 0) { 1012 throw new file_exception('storedfileproblem', 'Invalid itemid'); 1013 } 1014 1015 $filepath = clean_param($filepath, PARAM_PATH); 1016 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) { 1017 // path must start and end with '/' 1018 throw new file_exception('storedfileproblem', 'Invalid file path'); 1019 } 1020 1021 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.'); 1022 1023 if ($dir_info = $this->get_file_by_hash($pathnamehash)) { 1024 return $dir_info; 1025 } 1026 1027 static $contenthash = null; 1028 if (!$contenthash) { 1029 $this->add_string_to_pool(''); 1030 $contenthash = self::hash_from_string(''); 1031 } 1032 1033 $now = time(); 1034 1035 $dir_record = new stdClass(); 1036 $dir_record->contextid = $contextid; 1037 $dir_record->component = $component; 1038 $dir_record->filearea = $filearea; 1039 $dir_record->itemid = $itemid; 1040 $dir_record->filepath = $filepath; 1041 $dir_record->filename = '.'; 1042 $dir_record->contenthash = $contenthash; 1043 $dir_record->filesize = 0; 1044 1045 $dir_record->timecreated = $now; 1046 $dir_record->timemodified = $now; 1047 $dir_record->mimetype = null; 1048 $dir_record->userid = $userid; 1049 1050 $dir_record->pathnamehash = $pathnamehash; 1051 1052 $DB->insert_record('files', $dir_record); 1053 $dir_info = $this->get_file_by_hash($pathnamehash); 1054 1055 if ($filepath !== '/') { 1056 //recurse to parent dirs 1057 $filepath = trim($filepath, '/'); 1058 $filepath = explode('/', $filepath); 1059 array_pop($filepath); 1060 $filepath = implode('/', $filepath); 1061 $filepath = ($filepath === '') ? '/' : "/$filepath/"; 1062 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid); 1063 } 1064 1065 return $dir_info; 1066 } 1067 1068 /** 1069 * Add new file record to database and handle callbacks. 1070 * 1071 * @param stdClass $newrecord 1072 */ 1073 protected function create_file($newrecord) { 1074 global $DB; 1075 $newrecord->id = $DB->insert_record('files', $newrecord); 1076 1077 if ($newrecord->filename !== '.') { 1078 // Callback for file created. 1079 if ($pluginsfunction = get_plugins_with_function('after_file_created')) { 1080 foreach ($pluginsfunction as $plugintype => $plugins) { 1081 foreach ($plugins as $pluginfunction) { 1082 $pluginfunction($newrecord); 1083 } 1084 } 1085 } 1086 } 1087 } 1088 1089 /** 1090 * Add new local file based on existing local file. 1091 * 1092 * @param stdClass|array $filerecord object or array describing changes 1093 * @param stored_file|int $fileorid id or stored_file instance of the existing local file 1094 * @return stored_file instance of newly created file 1095 */ 1096 public function create_file_from_storedfile($filerecord, $fileorid) { 1097 global $DB; 1098 1099 if ($fileorid instanceof stored_file) { 1100 $fid = $fileorid->get_id(); 1101 } else { 1102 $fid = $fileorid; 1103 } 1104 1105 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record! 1106 1107 unset($filerecord['id']); 1108 unset($filerecord['filesize']); 1109 unset($filerecord['contenthash']); 1110 unset($filerecord['pathnamehash']); 1111 1112 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 1113 FROM {files} f 1114 LEFT JOIN {files_reference} r 1115 ON f.referencefileid = r.id 1116 WHERE f.id = ?"; 1117 1118 if (!$newrecord = $DB->get_record_sql($sql, array($fid))) { 1119 throw new file_exception('storedfileproblem', 'File does not exist'); 1120 } 1121 1122 unset($newrecord->id); 1123 1124 foreach ($filerecord as $key => $value) { 1125 // validate all parameters, we do not want any rubbish stored in database, right? 1126 if ($key == 'contextid' and (!is_number($value) or $value < 1)) { 1127 throw new file_exception('storedfileproblem', 'Invalid contextid'); 1128 } 1129 1130 if ($key == 'component') { 1131 $value = clean_param($value, PARAM_COMPONENT); 1132 if (empty($value)) { 1133 throw new file_exception('storedfileproblem', 'Invalid component'); 1134 } 1135 } 1136 1137 if ($key == 'filearea') { 1138 $value = clean_param($value, PARAM_AREA); 1139 if (empty($value)) { 1140 throw new file_exception('storedfileproblem', 'Invalid filearea'); 1141 } 1142 } 1143 1144 if ($key == 'itemid' and (!is_number($value) or $value < 0)) { 1145 throw new file_exception('storedfileproblem', 'Invalid itemid'); 1146 } 1147 1148 1149 if ($key == 'filepath') { 1150 $value = clean_param($value, PARAM_PATH); 1151 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) { 1152 // path must start and end with '/' 1153 throw new file_exception('storedfileproblem', 'Invalid file path'); 1154 } 1155 } 1156 1157 if ($key == 'filename') { 1158 $value = clean_param($value, PARAM_FILE); 1159 if ($value === '') { 1160 // path must start and end with '/' 1161 throw new file_exception('storedfileproblem', 'Invalid file name'); 1162 } 1163 } 1164 1165 if ($key === 'timecreated' or $key === 'timemodified') { 1166 if (!is_number($value)) { 1167 throw new file_exception('storedfileproblem', 'Invalid file '.$key); 1168 } 1169 if ($value < 0) { 1170 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1171 $value = 0; 1172 } 1173 } 1174 1175 if ($key == 'referencefileid' or $key == 'referencelastsync') { 1176 $value = clean_param($value, PARAM_INT); 1177 } 1178 1179 $newrecord->$key = $value; 1180 } 1181 1182 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename); 1183 1184 if ($newrecord->filename === '.') { 1185 // special case - only this function supports directories ;-) 1186 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid); 1187 // update the existing directory with the new data 1188 $newrecord->id = $directory->get_id(); 1189 $DB->update_record('files', $newrecord); 1190 return $this->get_file_instance($newrecord); 1191 } 1192 1193 // note: referencefileid is copied from the original file so that 1194 // creating a new file from an existing alias creates new alias implicitly. 1195 // here we just check the database consistency. 1196 if (!empty($newrecord->repositoryid)) { 1197 // It is OK if the current reference does not exist. It may have been altered by a repository plugin when the files 1198 // where saved from a draft area. 1199 $newrecord->referencefileid = $this->get_or_create_referencefileid($newrecord->repositoryid, $newrecord->reference); 1200 } 1201 1202 try { 1203 $this->create_file($newrecord); 1204 } catch (dml_exception $e) { 1205 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, 1206 $newrecord->filepath, $newrecord->filename, $e->debuginfo); 1207 } 1208 1209 1210 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid); 1211 1212 return $this->get_file_instance($newrecord); 1213 } 1214 1215 /** 1216 * Add new local file. 1217 * 1218 * @param stdClass|array $filerecord object or array describing file 1219 * @param string $url the URL to the file 1220 * @param array $options {@link download_file_content()} options 1221 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems 1222 * @return stored_file 1223 */ 1224 public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) { 1225 1226 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. 1227 $filerecord = (object)$filerecord; // We support arrays too. 1228 1229 $headers = isset($options['headers']) ? $options['headers'] : null; 1230 $postdata = isset($options['postdata']) ? $options['postdata'] : null; 1231 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false; 1232 $timeout = isset($options['timeout']) ? $options['timeout'] : 300; 1233 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20; 1234 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false; 1235 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false; 1236 1237 if (!isset($filerecord->filename)) { 1238 $parts = explode('/', $url); 1239 $filename = array_pop($parts); 1240 $filerecord->filename = clean_param($filename, PARAM_FILE); 1241 } 1242 $source = !empty($filerecord->source) ? $filerecord->source : $url; 1243 $filerecord->source = clean_param($source, PARAM_URL); 1244 1245 if ($usetempfile) { 1246 check_dir_exists($this->tempdir); 1247 $tmpfile = tempnam($this->tempdir, 'newfromurl'); 1248 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout); 1249 if ($content === false) { 1250 throw new file_exception('storedfileproblem', 'Cannot fetch file from URL'); 1251 } 1252 try { 1253 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile); 1254 @unlink($tmpfile); 1255 return $newfile; 1256 } catch (Exception $e) { 1257 @unlink($tmpfile); 1258 throw $e; 1259 } 1260 1261 } else { 1262 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout); 1263 if ($content === false) { 1264 throw new file_exception('storedfileproblem', 'Cannot fetch file from URL'); 1265 } 1266 return $this->create_file_from_string($filerecord, $content); 1267 } 1268 } 1269 1270 /** 1271 * Add new local file. 1272 * 1273 * @param stdClass|array $filerecord object or array describing file 1274 * @param string $pathname path to file or content of file 1275 * @return stored_file 1276 */ 1277 public function create_file_from_pathname($filerecord, $pathname) { 1278 global $DB; 1279 1280 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. 1281 $filerecord = (object)$filerecord; // We support arrays too. 1282 1283 // validate all parameters, we do not want any rubbish stored in database, right? 1284 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) { 1285 throw new file_exception('storedfileproblem', 'Invalid contextid'); 1286 } 1287 1288 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT); 1289 if (empty($filerecord->component)) { 1290 throw new file_exception('storedfileproblem', 'Invalid component'); 1291 } 1292 1293 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA); 1294 if (empty($filerecord->filearea)) { 1295 throw new file_exception('storedfileproblem', 'Invalid filearea'); 1296 } 1297 1298 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) { 1299 throw new file_exception('storedfileproblem', 'Invalid itemid'); 1300 } 1301 1302 if (!empty($filerecord->sortorder)) { 1303 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) { 1304 $filerecord->sortorder = 0; 1305 } 1306 } else { 1307 $filerecord->sortorder = 0; 1308 } 1309 1310 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); 1311 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { 1312 // path must start and end with '/' 1313 throw new file_exception('storedfileproblem', 'Invalid file path'); 1314 } 1315 1316 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE); 1317 if ($filerecord->filename === '') { 1318 // filename must not be empty 1319 throw new file_exception('storedfileproblem', 'Invalid file name'); 1320 } 1321 1322 $now = time(); 1323 if (isset($filerecord->timecreated)) { 1324 if (!is_number($filerecord->timecreated)) { 1325 throw new file_exception('storedfileproblem', 'Invalid file timecreated'); 1326 } 1327 if ($filerecord->timecreated < 0) { 1328 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1329 $filerecord->timecreated = 0; 1330 } 1331 } else { 1332 $filerecord->timecreated = $now; 1333 } 1334 1335 if (isset($filerecord->timemodified)) { 1336 if (!is_number($filerecord->timemodified)) { 1337 throw new file_exception('storedfileproblem', 'Invalid file timemodified'); 1338 } 1339 if ($filerecord->timemodified < 0) { 1340 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1341 $filerecord->timemodified = 0; 1342 } 1343 } else { 1344 $filerecord->timemodified = $now; 1345 } 1346 1347 $newrecord = new stdClass(); 1348 1349 $newrecord->contextid = $filerecord->contextid; 1350 $newrecord->component = $filerecord->component; 1351 $newrecord->filearea = $filerecord->filearea; 1352 $newrecord->itemid = $filerecord->itemid; 1353 $newrecord->filepath = $filerecord->filepath; 1354 $newrecord->filename = $filerecord->filename; 1355 1356 $newrecord->timecreated = $filerecord->timecreated; 1357 $newrecord->timemodified = $filerecord->timemodified; 1358 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype; 1359 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; 1360 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source; 1361 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author; 1362 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license; 1363 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status; 1364 $newrecord->sortorder = $filerecord->sortorder; 1365 1366 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname, null, $newrecord); 1367 1368 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename); 1369 1370 try { 1371 $this->create_file($newrecord); 1372 } catch (dml_exception $e) { 1373 if ($newfile) { 1374 $this->filesystem->remove_file($newrecord->contenthash); 1375 } 1376 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, 1377 $newrecord->filepath, $newrecord->filename, $e->debuginfo); 1378 } 1379 1380 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid); 1381 1382 return $this->get_file_instance($newrecord); 1383 } 1384 1385 /** 1386 * Add new local file. 1387 * 1388 * @param stdClass|array $filerecord object or array describing file 1389 * @param string $content content of file 1390 * @return stored_file 1391 */ 1392 public function create_file_from_string($filerecord, $content) { 1393 global $DB; 1394 1395 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. 1396 $filerecord = (object)$filerecord; // We support arrays too. 1397 1398 // validate all parameters, we do not want any rubbish stored in database, right? 1399 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) { 1400 throw new file_exception('storedfileproblem', 'Invalid contextid'); 1401 } 1402 1403 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT); 1404 if (empty($filerecord->component)) { 1405 throw new file_exception('storedfileproblem', 'Invalid component'); 1406 } 1407 1408 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA); 1409 if (empty($filerecord->filearea)) { 1410 throw new file_exception('storedfileproblem', 'Invalid filearea'); 1411 } 1412 1413 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) { 1414 throw new file_exception('storedfileproblem', 'Invalid itemid'); 1415 } 1416 1417 if (!empty($filerecord->sortorder)) { 1418 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) { 1419 $filerecord->sortorder = 0; 1420 } 1421 } else { 1422 $filerecord->sortorder = 0; 1423 } 1424 1425 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); 1426 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { 1427 // path must start and end with '/' 1428 throw new file_exception('storedfileproblem', 'Invalid file path'); 1429 } 1430 1431 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE); 1432 if ($filerecord->filename === '') { 1433 // path must start and end with '/' 1434 throw new file_exception('storedfileproblem', 'Invalid file name'); 1435 } 1436 1437 $now = time(); 1438 if (isset($filerecord->timecreated)) { 1439 if (!is_number($filerecord->timecreated)) { 1440 throw new file_exception('storedfileproblem', 'Invalid file timecreated'); 1441 } 1442 if ($filerecord->timecreated < 0) { 1443 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1444 $filerecord->timecreated = 0; 1445 } 1446 } else { 1447 $filerecord->timecreated = $now; 1448 } 1449 1450 if (isset($filerecord->timemodified)) { 1451 if (!is_number($filerecord->timemodified)) { 1452 throw new file_exception('storedfileproblem', 'Invalid file timemodified'); 1453 } 1454 if ($filerecord->timemodified < 0) { 1455 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1456 $filerecord->timemodified = 0; 1457 } 1458 } else { 1459 $filerecord->timemodified = $now; 1460 } 1461 1462 $newrecord = new stdClass(); 1463 1464 $newrecord->contextid = $filerecord->contextid; 1465 $newrecord->component = $filerecord->component; 1466 $newrecord->filearea = $filerecord->filearea; 1467 $newrecord->itemid = $filerecord->itemid; 1468 $newrecord->filepath = $filerecord->filepath; 1469 $newrecord->filename = $filerecord->filename; 1470 1471 $newrecord->timecreated = $filerecord->timecreated; 1472 $newrecord->timemodified = $filerecord->timemodified; 1473 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; 1474 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source; 1475 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author; 1476 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license; 1477 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status; 1478 $newrecord->sortorder = $filerecord->sortorder; 1479 1480 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content, $newrecord); 1481 if (empty($filerecord->mimetype)) { 1482 $newrecord->mimetype = $this->filesystem->mimetype_from_hash($newrecord->contenthash, $newrecord->filename); 1483 } else { 1484 $newrecord->mimetype = $filerecord->mimetype; 1485 } 1486 1487 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename); 1488 1489 try { 1490 $this->create_file($newrecord); 1491 } catch (dml_exception $e) { 1492 if ($newfile) { 1493 $this->filesystem->remove_file($newrecord->contenthash); 1494 } 1495 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, 1496 $newrecord->filepath, $newrecord->filename, $e->debuginfo); 1497 } 1498 1499 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid); 1500 1501 return $this->get_file_instance($newrecord); 1502 } 1503 1504 /** 1505 * Synchronise stored file from file. 1506 * 1507 * @param stored_file $file Stored file to synchronise. 1508 * @param string $path Path to the file to synchronise from. 1509 * @param stdClass $filerecord The file record from the database. 1510 */ 1511 public function synchronise_stored_file_from_file(stored_file $file, $path, $filerecord) { 1512 list($contenthash, $filesize) = $this->add_file_to_pool($path, null, $filerecord); 1513 $file->set_synchronized($contenthash, $filesize); 1514 } 1515 1516 /** 1517 * Synchronise stored file from string. 1518 * 1519 * @param stored_file $file Stored file to synchronise. 1520 * @param string $content File content. 1521 * @param stdClass $filerecord The file record from the database. 1522 */ 1523 public function synchronise_stored_file_from_string(stored_file $file, $content, $filerecord) { 1524 list($contenthash, $filesize) = $this->add_string_to_pool($content, $filerecord); 1525 $file->set_synchronized($contenthash, $filesize); 1526 } 1527 1528 /** 1529 * Create a new alias/shortcut file from file reference information 1530 * 1531 * @param stdClass|array $filerecord object or array describing the new file 1532 * @param int $repositoryid the id of the repository that provides the original file 1533 * @param string $reference the information required by the repository to locate the original file 1534 * @param array $options options for creating the new file 1535 * @return stored_file 1536 */ 1537 public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) { 1538 global $DB; 1539 1540 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects. 1541 $filerecord = (object)$filerecord; // We support arrays too. 1542 1543 // validate all parameters, we do not want any rubbish stored in database, right? 1544 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) { 1545 throw new file_exception('storedfileproblem', 'Invalid contextid'); 1546 } 1547 1548 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT); 1549 if (empty($filerecord->component)) { 1550 throw new file_exception('storedfileproblem', 'Invalid component'); 1551 } 1552 1553 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA); 1554 if (empty($filerecord->filearea)) { 1555 throw new file_exception('storedfileproblem', 'Invalid filearea'); 1556 } 1557 1558 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) { 1559 throw new file_exception('storedfileproblem', 'Invalid itemid'); 1560 } 1561 1562 if (!empty($filerecord->sortorder)) { 1563 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) { 1564 $filerecord->sortorder = 0; 1565 } 1566 } else { 1567 $filerecord->sortorder = 0; 1568 } 1569 1570 $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype; 1571 $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid; 1572 $filerecord->source = empty($filerecord->source) ? null : $filerecord->source; 1573 $filerecord->author = empty($filerecord->author) ? null : $filerecord->author; 1574 $filerecord->license = empty($filerecord->license) ? null : $filerecord->license; 1575 $filerecord->status = empty($filerecord->status) ? 0 : $filerecord->status; 1576 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH); 1577 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) { 1578 // Path must start and end with '/'. 1579 throw new file_exception('storedfileproblem', 'Invalid file path'); 1580 } 1581 1582 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE); 1583 if ($filerecord->filename === '') { 1584 // Path must start and end with '/'. 1585 throw new file_exception('storedfileproblem', 'Invalid file name'); 1586 } 1587 1588 $now = time(); 1589 if (isset($filerecord->timecreated)) { 1590 if (!is_number($filerecord->timecreated)) { 1591 throw new file_exception('storedfileproblem', 'Invalid file timecreated'); 1592 } 1593 if ($filerecord->timecreated < 0) { 1594 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1595 $filerecord->timecreated = 0; 1596 } 1597 } else { 1598 $filerecord->timecreated = $now; 1599 } 1600 1601 if (isset($filerecord->timemodified)) { 1602 if (!is_number($filerecord->timemodified)) { 1603 throw new file_exception('storedfileproblem', 'Invalid file timemodified'); 1604 } 1605 if ($filerecord->timemodified < 0) { 1606 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak) 1607 $filerecord->timemodified = 0; 1608 } 1609 } else { 1610 $filerecord->timemodified = $now; 1611 } 1612 1613 $transaction = $DB->start_delegated_transaction(); 1614 1615 try { 1616 $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference); 1617 } catch (Exception $e) { 1618 throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage()); 1619 } 1620 1621 $existingfile = null; 1622 if (isset($filerecord->contenthash)) { 1623 $existingfile = $DB->get_record('files', array('contenthash' => $filerecord->contenthash), '*', IGNORE_MULTIPLE); 1624 } 1625 if (!empty($existingfile)) { 1626 // There is an existing file already available. 1627 if (empty($filerecord->filesize)) { 1628 $filerecord->filesize = $existingfile->filesize; 1629 } else { 1630 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT); 1631 } 1632 } else { 1633 // Attempt to get the result of last synchronisation for this reference. 1634 $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid), 1635 'id, contenthash, filesize', IGNORE_MULTIPLE); 1636 if ($lastcontent) { 1637 $filerecord->contenthash = $lastcontent->contenthash; 1638 $filerecord->filesize = $lastcontent->filesize; 1639 } else { 1640 // External file doesn't have content in moodle. 1641 // So we create an empty file for it. 1642 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null, $filerecord); 1643 } 1644 } 1645 1646 $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename); 1647 1648 try { 1649 $filerecord->id = $DB->insert_record('files', $filerecord); 1650 } catch (dml_exception $e) { 1651 if (!empty($newfile)) { 1652 $this->filesystem->remove_file($filerecord->contenthash); 1653 } 1654 throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, 1655 $filerecord->filepath, $filerecord->filename, $e->debuginfo); 1656 } 1657 1658 $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid); 1659 1660 $transaction->allow_commit(); 1661 1662 // this will retrieve all reference information from DB as well 1663 return $this->get_file_by_id($filerecord->id); 1664 } 1665 1666 /** 1667 * Creates new image file from existing. 1668 * 1669 * @param stdClass|array $filerecord object or array describing new file 1670 * @param int|stored_file $fid file id or stored file object 1671 * @param int $newwidth in pixels 1672 * @param int $newheight in pixels 1673 * @param bool $keepaspectratio whether or not keep aspect ratio 1674 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png 1675 * @return stored_file 1676 */ 1677 public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) { 1678 if (!function_exists('imagecreatefromstring')) { 1679 //Most likely the GD php extension isn't installed 1680 //image conversion cannot succeed 1681 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.'); 1682 } 1683 1684 if ($fid instanceof stored_file) { 1685 $fid = $fid->get_id(); 1686 } 1687 1688 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record! 1689 1690 if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data. 1691 throw new file_exception('storedfileproblem', 'File does not exist'); 1692 } 1693 1694 if (!$imageinfo = $file->get_imageinfo()) { 1695 throw new file_exception('storedfileproblem', 'File is not an image'); 1696 } 1697 1698 if (!isset($filerecord['filename'])) { 1699 $filerecord['filename'] = $file->get_filename(); 1700 } 1701 1702 if (!isset($filerecord['mimetype'])) { 1703 $filerecord['mimetype'] = $imageinfo['mimetype']; 1704 } 1705 1706 $width = $imageinfo['width']; 1707 $height = $imageinfo['height']; 1708 1709 if ($keepaspectratio) { 1710 if (0 >= $newwidth and 0 >= $newheight) { 1711 // no sizes specified 1712 $newwidth = $width; 1713 $newheight = $height; 1714 1715 } else if (0 < $newwidth and 0 < $newheight) { 1716 $xheight = ($newwidth*($height/$width)); 1717 if ($xheight < $newheight) { 1718 $newheight = (int)$xheight; 1719 } else { 1720 $newwidth = (int)($newheight*($width/$height)); 1721 } 1722 1723 } else if (0 < $newwidth) { 1724 $newheight = (int)($newwidth*($height/$width)); 1725 1726 } else { //0 < $newheight 1727 $newwidth = (int)($newheight*($width/$height)); 1728 } 1729 1730 } else { 1731 if (0 >= $newwidth) { 1732 $newwidth = $width; 1733 } 1734 if (0 >= $newheight) { 1735 $newheight = $height; 1736 } 1737 } 1738 1739 // The original image. 1740 $img = imagecreatefromstring($file->get_content()); 1741 1742 // A new true color image where we will copy our original image. 1743 $newimg = imagecreatetruecolor($newwidth, $newheight); 1744 1745 // Determine if the file supports transparency. 1746 $hasalpha = $filerecord['mimetype'] == 'image/png' || $filerecord['mimetype'] == 'image/gif'; 1747 1748 // Maintain transparency. 1749 if ($hasalpha) { 1750 imagealphablending($newimg, true); 1751 1752 // Get the current transparent index for the original image. 1753 $colour = imagecolortransparent($img); 1754 if ($colour == -1) { 1755 // Set a transparent colour index if there's none. 1756 $colour = imagecolorallocatealpha($newimg, 255, 255, 255, 127); 1757 // Save full alpha channel. 1758 imagesavealpha($newimg, true); 1759 } 1760 imagecolortransparent($newimg, $colour); 1761 imagefill($newimg, 0, 0, $colour); 1762 } 1763 1764 // Process the image to be output. 1765 if ($height != $newheight or $width != $newwidth) { 1766 // Resample if the dimensions differ from the original. 1767 if (!imagecopyresampled($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) { 1768 // weird 1769 throw new file_exception('storedfileproblem', 'Can not resize image'); 1770 } 1771 imagedestroy($img); 1772 $img = $newimg; 1773 1774 } else if ($hasalpha) { 1775 // Just copy to the new image with the alpha channel. 1776 if (!imagecopy($newimg, $img, 0, 0, 0, 0, $width, $height)) { 1777 // Weird. 1778 throw new file_exception('storedfileproblem', 'Can not copy image'); 1779 } 1780 imagedestroy($img); 1781 $img = $newimg; 1782 1783 } else { 1784 // No particular processing needed for the original image. 1785 imagedestroy($newimg); 1786 } 1787 1788 ob_start(); 1789 switch ($filerecord['mimetype']) { 1790 case 'image/gif': 1791 imagegif($img); 1792 break; 1793 1794 case 'image/jpeg': 1795 if (is_null($quality)) { 1796 imagejpeg($img); 1797 } else { 1798 imagejpeg($img, NULL, $quality); 1799 } 1800 break; 1801 1802 case 'image/png': 1803 $quality = (int)$quality; 1804 1805 // Woah nelly! Because PNG quality is in the range 0 - 9 compared to JPEG quality, 1806 // the latter of which can go to 100, we need to make sure that quality here is 1807 // in a safe range or PHP WILL CRASH AND DIE. You have been warned. 1808 $quality = $quality > 9 ? (int)(max(1.0, (float)$quality / 100.0) * 9.0) : $quality; 1809 imagepng($img, null, $quality, PNG_NO_FILTER); 1810 break; 1811 1812 default: 1813 throw new file_exception('storedfileproblem', 'Unsupported mime type'); 1814 } 1815 1816 $content = ob_get_contents(); 1817 ob_end_clean(); 1818 imagedestroy($img); 1819 1820 if (!$content) { 1821 throw new file_exception('storedfileproblem', 'Can not convert image'); 1822 } 1823 1824 return $this->create_file_from_string($filerecord, $content); 1825 } 1826 1827 /** 1828 * Add file content to sha1 pool. 1829 * 1830 * @param string $pathname path to file 1831 * @param string|null $contenthash sha1 hash of content if known (performance only) 1832 * @param stdClass|null $newrecord New file record 1833 * @return array (contenthash, filesize, newfile) 1834 */ 1835 public function add_file_to_pool($pathname, $contenthash = null, $newrecord = null) { 1836 $this->call_before_file_created_plugin_functions($newrecord, $pathname); 1837 return $this->filesystem->add_file_from_path($pathname, $contenthash); 1838 } 1839 1840 /** 1841 * Add string content to sha1 pool. 1842 * 1843 * @param string $content file content - binary string 1844 * @return array (contenthash, filesize, newfile) 1845 */ 1846 public function add_string_to_pool($content, $newrecord = null) { 1847 $this->call_before_file_created_plugin_functions($newrecord, null, $content); 1848 return $this->filesystem->add_file_from_string($content); 1849 } 1850 1851 /** 1852 * before_file_created hook. 1853 * 1854 * @param stdClass|null $newrecord New file record. 1855 * @param string|null $pathname Path to file. 1856 * @param string|null $content File content. 1857 */ 1858 protected function call_before_file_created_plugin_functions($newrecord, $pathname = null, $content = null) { 1859 $pluginsfunction = get_plugins_with_function('before_file_created'); 1860 foreach ($pluginsfunction as $plugintype => $plugins) { 1861 foreach ($plugins as $pluginfunction) { 1862 $pluginfunction($newrecord, ['pathname' => $pathname, 'content' => $content]); 1863 } 1864 } 1865 } 1866 1867 /** 1868 * Serve file content using X-Sendfile header. 1869 * Please make sure that all headers are already sent and the all 1870 * access control checks passed. 1871 * 1872 * This alternate method to xsendfile() allows an alternate file system 1873 * to use the full file metadata and avoid extra lookups. 1874 * 1875 * @param stored_file $file The file to send 1876 * @return bool success 1877 */ 1878 public function xsendfile_file(stored_file $file): bool { 1879 return $this->filesystem->xsendfile_file($file); 1880 } 1881 1882 /** 1883 * Serve file content using X-Sendfile header. 1884 * Please make sure that all headers are already sent 1885 * and the all access control checks passed. 1886 * 1887 * @param string $contenthash sah1 hash of the file content to be served 1888 * @return bool success 1889 */ 1890 public function xsendfile($contenthash) { 1891 return $this->filesystem->xsendfile($contenthash); 1892 } 1893 1894 /** 1895 * Returns true if filesystem is configured to support xsendfile. 1896 * 1897 * @return bool 1898 */ 1899 public function supports_xsendfile() { 1900 return $this->filesystem->supports_xsendfile(); 1901 } 1902 1903 /** 1904 * Content exists 1905 * 1906 * @param string $contenthash 1907 * @return bool 1908 * @deprecated since 3.3 1909 */ 1910 public function content_exists($contenthash) { 1911 debugging('The content_exists function has been deprecated and should no longer be used.', DEBUG_DEVELOPER); 1912 1913 return false; 1914 } 1915 1916 /** 1917 * Tries to recover missing content of file from trash. 1918 * 1919 * @param stored_file $file stored_file instance 1920 * @return bool success 1921 * @deprecated since 3.3 1922 */ 1923 public function try_content_recovery($file) { 1924 debugging('The try_content_recovery function has been deprecated and should no longer be used.', DEBUG_DEVELOPER); 1925 1926 return false; 1927 } 1928 1929 /** 1930 * When user referring to a moodle file, we build the reference field 1931 * 1932 * @param array $params 1933 * @return string 1934 */ 1935 public static function pack_reference($params) { 1936 $params = (array)$params; 1937 $reference = array(); 1938 $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT); 1939 $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT); 1940 $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT); 1941 $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA); 1942 $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH); 1943 $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE); 1944 return base64_encode(serialize($reference)); 1945 } 1946 1947 /** 1948 * Unpack reference field 1949 * 1950 * @param string $str 1951 * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()} 1952 * @throws file_reference_exception if the $str does not have the expected format 1953 * @return array 1954 */ 1955 public static function unpack_reference($str, $cleanparams = false) { 1956 $decoded = base64_decode($str, true); 1957 if ($decoded === false) { 1958 throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format'); 1959 } 1960 $params = unserialize_array($decoded); 1961 if ($params === false) { 1962 throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value'); 1963 } 1964 if (is_array($params) && $cleanparams) { 1965 $params = array( 1966 'component' => is_null($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT), 1967 'filearea' => is_null($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA), 1968 'itemid' => is_null($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT), 1969 'filename' => is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE), 1970 'filepath' => is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH), 1971 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT) 1972 ); 1973 } 1974 return $params; 1975 } 1976 1977 /** 1978 * Search through the server files. 1979 * 1980 * The query parameter will be used in conjuction with the SQL directive 1981 * LIKE, so include '%' in it if you need to. This search will always ignore 1982 * user files and directories. Note that the search is case insensitive. 1983 * 1984 * This query can quickly become inefficient so use it sparignly. 1985 * 1986 * @param string $query The string used with SQL LIKE. 1987 * @param integer $from The offset to start the search at. 1988 * @param integer $limit The maximum number of results. 1989 * @param boolean $count When true this methods returns the number of results availabe, 1990 * disregarding the parameters $from and $limit. 1991 * @return int|array Integer when count, otherwise array of stored_file objects. 1992 */ 1993 public function search_server_files($query, $from = 0, $limit = 20, $count = false) { 1994 global $DB; 1995 $params = array( 1996 'contextlevel' => CONTEXT_USER, 1997 'directory' => '.', 1998 'query' => $query 1999 ); 2000 2001 if ($count) { 2002 $select = 'COUNT(1)'; 2003 } else { 2004 $select = self::instance_sql_fields('f', 'r'); 2005 } 2006 $like = $DB->sql_like('f.filename', ':query', false); 2007 2008 $sql = "SELECT $select 2009 FROM {files} f 2010 LEFT JOIN {files_reference} r 2011 ON f.referencefileid = r.id 2012 JOIN {context} c 2013 ON f.contextid = c.id 2014 WHERE c.contextlevel <> :contextlevel 2015 AND f.filename <> :directory 2016 AND " . $like . ""; 2017 2018 if ($count) { 2019 return $DB->count_records_sql($sql, $params); 2020 } 2021 2022 $sql .= " ORDER BY f.filename"; 2023 2024 $result = array(); 2025 $filerecords = $DB->get_recordset_sql($sql, $params, $from, $limit); 2026 foreach ($filerecords as $filerecord) { 2027 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 2028 } 2029 $filerecords->close(); 2030 2031 return $result; 2032 } 2033 2034 /** 2035 * Returns all aliases that refer to some stored_file via the given reference 2036 * 2037 * All repositories that provide access to a stored_file are expected to use 2038 * {@link self::pack_reference()}. This method can't be used if the given reference 2039 * does not use this format or if you are looking for references to an external file 2040 * (for example it can't be used to search for all aliases that refer to a given 2041 * Dropbox or Box.net file). 2042 * 2043 * Aliases in user draft areas are excluded from the returned list. 2044 * 2045 * @param string $reference identification of the referenced file 2046 * @return array of stored_file indexed by its pathnamehash 2047 */ 2048 public function search_references($reference) { 2049 global $DB; 2050 2051 if (is_null($reference)) { 2052 throw new coding_exception('NULL is not a valid reference to an external file'); 2053 } 2054 2055 // Give {@link self::unpack_reference()} a chance to throw exception if the 2056 // reference is not in a valid format. 2057 self::unpack_reference($reference); 2058 2059 $referencehash = sha1($reference); 2060 2061 $sql = "SELECT ".self::instance_sql_fields('f', 'r')." 2062 FROM {files} f 2063 JOIN {files_reference} r ON f.referencefileid = r.id 2064 JOIN {repository_instances} ri ON r.repositoryid = ri.id 2065 WHERE r.referencehash = ? 2066 AND (f.component <> ? OR f.filearea <> ?)"; 2067 2068 $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft')); 2069 $files = array(); 2070 foreach ($rs as $filerecord) { 2071 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord); 2072 } 2073 $rs->close(); 2074 2075 return $files; 2076 } 2077 2078 /** 2079 * Returns the number of aliases that refer to some stored_file via the given reference 2080 * 2081 * All repositories that provide access to a stored_file are expected to use 2082 * {@link self::pack_reference()}. This method can't be used if the given reference 2083 * does not use this format or if you are looking for references to an external file 2084 * (for example it can't be used to count aliases that refer to a given Dropbox or 2085 * Box.net file). 2086 * 2087 * Aliases in user draft areas are not counted. 2088 * 2089 * @param string $reference identification of the referenced file 2090 * @return int 2091 */ 2092 public function search_references_count($reference) { 2093 global $DB; 2094 2095 if (is_null($reference)) { 2096 throw new coding_exception('NULL is not a valid reference to an external file'); 2097 } 2098 2099 // Give {@link self::unpack_reference()} a chance to throw exception if the 2100 // reference is not in a valid format. 2101 self::unpack_reference($reference); 2102 2103 $referencehash = sha1($reference); 2104 2105 $sql = "SELECT COUNT(f.id) 2106 FROM {files} f 2107 JOIN {files_reference} r ON f.referencefileid = r.id 2108 JOIN {repository_instances} ri ON r.repositoryid = ri.id 2109 WHERE r.referencehash = ? 2110 AND (f.component <> ? OR f.filearea <> ?)"; 2111 2112 return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft')); 2113 } 2114 2115 /** 2116 * Returns all aliases that link to the given stored_file 2117 * 2118 * Aliases in user draft areas are excluded from the returned list. 2119 * 2120 * @param stored_file $storedfile 2121 * @return array of stored_file 2122 */ 2123 public function get_references_by_storedfile(stored_file $storedfile) { 2124 global $DB; 2125 2126 $params = array(); 2127 $params['contextid'] = $storedfile->get_contextid(); 2128 $params['component'] = $storedfile->get_component(); 2129 $params['filearea'] = $storedfile->get_filearea(); 2130 $params['itemid'] = $storedfile->get_itemid(); 2131 $params['filename'] = $storedfile->get_filename(); 2132 $params['filepath'] = $storedfile->get_filepath(); 2133 2134 return $this->search_references(self::pack_reference($params)); 2135 } 2136 2137 /** 2138 * Returns the number of aliases that link to the given stored_file 2139 * 2140 * Aliases in user draft areas are not counted. 2141 * 2142 * @param stored_file $storedfile 2143 * @return int 2144 */ 2145 public function get_references_count_by_storedfile(stored_file $storedfile) { 2146 global $DB; 2147 2148 $params = array(); 2149 $params['contextid'] = $storedfile->get_contextid(); 2150 $params['component'] = $storedfile->get_component(); 2151 $params['filearea'] = $storedfile->get_filearea(); 2152 $params['itemid'] = $storedfile->get_itemid(); 2153 $params['filename'] = $storedfile->get_filename(); 2154 $params['filepath'] = $storedfile->get_filepath(); 2155 2156 return $this->search_references_count(self::pack_reference($params)); 2157 } 2158 2159 /** 2160 * Updates all files that are referencing this file with the new contenthash 2161 * and filesize 2162 * 2163 * @param stored_file $storedfile 2164 */ 2165 public function update_references_to_storedfile(stored_file $storedfile) { 2166 global $CFG, $DB; 2167 $params = array(); 2168 $params['contextid'] = $storedfile->get_contextid(); 2169 $params['component'] = $storedfile->get_component(); 2170 $params['filearea'] = $storedfile->get_filearea(); 2171 $params['itemid'] = $storedfile->get_itemid(); 2172 $params['filename'] = $storedfile->get_filename(); 2173 $params['filepath'] = $storedfile->get_filepath(); 2174 $reference = self::pack_reference($params); 2175 $referencehash = sha1($reference); 2176 2177 $sql = "SELECT repositoryid, id FROM {files_reference} 2178 WHERE referencehash = ?"; 2179 $rs = $DB->get_recordset_sql($sql, array($referencehash)); 2180 2181 $now = time(); 2182 foreach ($rs as $record) { 2183 $this->update_references($record->id, $now, null, 2184 $storedfile->get_contenthash(), $storedfile->get_filesize(), 0, $storedfile->get_timemodified()); 2185 } 2186 $rs->close(); 2187 } 2188 2189 /** 2190 * Convert file alias to local file 2191 * 2192 * @throws moodle_exception if file could not be downloaded 2193 * 2194 * @param stored_file $storedfile a stored_file instances 2195 * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit) 2196 * @return stored_file stored_file 2197 */ 2198 public function import_external_file(stored_file $storedfile, $maxbytes = 0) { 2199 global $CFG; 2200 $storedfile->import_external_file_contents($maxbytes); 2201 $storedfile->delete_reference(); 2202 return $storedfile; 2203 } 2204 2205 /** 2206 * Return mimetype by given file pathname. 2207 * 2208 * If file has a known extension, we return the mimetype based on extension. 2209 * Otherwise (when possible) we try to get the mimetype from file contents. 2210 * 2211 * @param string $fullpath Full path to the file on disk 2212 * @param string $filename Correct file name with extension, if omitted will be taken from $path 2213 * @return string 2214 */ 2215 public static function mimetype($fullpath, $filename = null) { 2216 if (empty($filename)) { 2217 $filename = $fullpath; 2218 } 2219 2220 // The mimeinfo function determines the mimetype purely based on the file extension. 2221 $type = mimeinfo('type', $filename); 2222 2223 if ($type === 'document/unknown') { 2224 // The type is unknown. Inspect the file now. 2225 $type = self::mimetype_from_file($fullpath); 2226 } 2227 return $type; 2228 } 2229 2230 /** 2231 * Inspect a file on disk for it's mimetype. 2232 * 2233 * @param string $fullpath Path to file on disk 2234 * @return string The mimetype 2235 */ 2236 public static function mimetype_from_file($fullpath) { 2237 if (file_exists($fullpath)) { 2238 // The type is unknown. Attempt to look up the file type now. 2239 $finfo = new finfo(FILEINFO_MIME_TYPE); 2240 2241 // See https://bugs.php.net/bug.php?id=79045 - finfo isn't consistent with returned type, normalize into value 2242 // that is used internally by the {@see core_filetypes} class and the {@see mimeinfo_from_type} call below. 2243 $mimetype = $finfo->file($fullpath); 2244 if ($mimetype === 'image/svg') { 2245 $mimetype = 'image/svg+xml'; 2246 } 2247 2248 return mimeinfo_from_type('type', $mimetype); 2249 } 2250 2251 return 'document/unknown'; 2252 } 2253 2254 /** 2255 * Cron cleanup job. 2256 */ 2257 public function cron() { 2258 global $CFG, $DB; 2259 require_once($CFG->libdir.'/cronlib.php'); 2260 2261 // find out all stale draft areas (older than 4 days) and purge them 2262 // those are identified by time stamp of the /. root dir 2263 mtrace('Deleting old draft files... ', ''); 2264 cron_trace_time_and_memory(); 2265 $old = time() - 60*60*24*4; 2266 $sql = "SELECT * 2267 FROM {files} 2268 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.' 2269 AND timecreated < :old"; 2270 $rs = $DB->get_recordset_sql($sql, array('old'=>$old)); 2271 foreach ($rs as $dir) { 2272 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid); 2273 } 2274 $rs->close(); 2275 mtrace('done.'); 2276 2277 // Remove orphaned files: 2278 // * preview files in the core preview filearea without the existing original file. 2279 // * document converted files in core documentconversion filearea without the existing original file. 2280 mtrace('Deleting orphaned preview, and document conversion files... ', ''); 2281 cron_trace_time_and_memory(); 2282 $sql = "SELECT p.* 2283 FROM {files} p 2284 LEFT JOIN {files} o ON (p.filename = o.contenthash) 2285 WHERE p.contextid = ? 2286 AND p.component = 'core' 2287 AND (p.filearea = 'preview' OR p.filearea = 'documentconversion') 2288 AND p.itemid = 0 2289 AND o.id IS NULL"; 2290 $syscontext = context_system::instance(); 2291 $rs = $DB->get_recordset_sql($sql, array($syscontext->id)); 2292 foreach ($rs as $orphan) { 2293 $file = $this->get_file_instance($orphan); 2294 if (!$file->is_directory()) { 2295 $file->delete(); 2296 } 2297 } 2298 $rs->close(); 2299 mtrace('done.'); 2300 2301 // remove trash pool files once a day 2302 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php 2303 $filescleanupperiod = empty($CFG->filescleanupperiod) ? 86400 : $CFG->filescleanupperiod; 2304 if (empty($CFG->fileslastcleanup) || ($CFG->fileslastcleanup < time() - $filescleanupperiod)) { 2305 require_once($CFG->libdir.'/filelib.php'); 2306 // Delete files that are associated with a context that no longer exists. 2307 mtrace('Cleaning up files from deleted contexts... ', ''); 2308 cron_trace_time_and_memory(); 2309 $sql = "SELECT DISTINCT f.contextid 2310 FROM {files} f 2311 LEFT OUTER JOIN {context} c ON f.contextid = c.id 2312 WHERE c.id IS NULL"; 2313 $rs = $DB->get_recordset_sql($sql); 2314 if ($rs->valid()) { 2315 $fs = get_file_storage(); 2316 foreach ($rs as $ctx) { 2317 $fs->delete_area_files($ctx->contextid); 2318 } 2319 } 2320 $rs->close(); 2321 mtrace('done.'); 2322 2323 mtrace('Call filesystem cron tasks.', ''); 2324 cron_trace_time_and_memory(); 2325 $this->filesystem->cron(); 2326 mtrace('done.'); 2327 } 2328 } 2329 2330 /** 2331 * Get the sql formated fields for a file instance to be created from a 2332 * {files} and {files_refernece} join. 2333 * 2334 * @param string $filesprefix the table prefix for the {files} table 2335 * @param string $filesreferenceprefix the table prefix for the {files_reference} table 2336 * @return string the sql to go after a SELECT 2337 */ 2338 private static function instance_sql_fields($filesprefix, $filesreferenceprefix) { 2339 // Note, these fieldnames MUST NOT overlap between the two tables, 2340 // else problems like MDL-33172 occur. 2341 $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea', 2342 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source', 2343 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid'); 2344 2345 $referencefields = array('repositoryid' => 'repositoryid', 2346 'reference' => 'reference', 2347 'lastsync' => 'referencelastsync'); 2348 2349 // id is specifically named to prevent overlaping between the two tables. 2350 $fields = array(); 2351 $fields[] = $filesprefix.'.id AS id'; 2352 foreach ($filefields as $field) { 2353 $fields[] = "{$filesprefix}.{$field}"; 2354 } 2355 2356 foreach ($referencefields as $field => $alias) { 2357 $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}"; 2358 } 2359 2360 return implode(', ', $fields); 2361 } 2362 2363 /** 2364 * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference 2365 * 2366 * If the record already exists, its id is returned. If there is no such record yet, 2367 * new one is created (using the lastsync provided, too) and its id is returned. 2368 * 2369 * @param int $repositoryid 2370 * @param string $reference 2371 * @param int $lastsync 2372 * @param int $lifetime argument not used any more 2373 * @return int 2374 */ 2375 private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) { 2376 global $DB; 2377 2378 $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING); 2379 2380 if ($id !== false) { 2381 // bah, that was easy 2382 return $id; 2383 } 2384 2385 // no such record yet, create one 2386 try { 2387 $id = $DB->insert_record('files_reference', array( 2388 'repositoryid' => $repositoryid, 2389 'reference' => $reference, 2390 'referencehash' => sha1($reference), 2391 'lastsync' => $lastsync)); 2392 } catch (dml_exception $e) { 2393 // if inserting the new record failed, chances are that the race condition has just 2394 // occured and the unique index did not allow to create the second record with the same 2395 // repositoryid + reference combo 2396 $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST); 2397 } 2398 2399 return $id; 2400 } 2401 2402 /** 2403 * Returns the id of the record in {files_reference} that matches the passed parameters 2404 * 2405 * Depending on the required strictness, false can be returned. The behaviour is consistent 2406 * with standard DML methods. 2407 * 2408 * @param int $repositoryid 2409 * @param string $reference 2410 * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST} 2411 * @return int|bool 2412 */ 2413 private function get_referencefileid($repositoryid, $reference, $strictness) { 2414 global $DB; 2415 2416 return $DB->get_field('files_reference', 'id', 2417 array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness); 2418 } 2419 2420 /** 2421 * Updates a reference to the external resource and all files that use it 2422 * 2423 * This function is called after synchronisation of an external file and updates the 2424 * contenthash, filesize and status of all files that reference this external file 2425 * as well as time last synchronised. 2426 * 2427 * @param int $referencefileid 2428 * @param int $lastsync 2429 * @param int $lifetime argument not used any more, liefetime is returned by repository 2430 * @param string $contenthash 2431 * @param int $filesize 2432 * @param int $status 0 if ok or 666 if source is missing 2433 * @param int $timemodified last time modified of the source, if known 2434 */ 2435 public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status, $timemodified = null) { 2436 global $DB; 2437 $referencefileid = clean_param($referencefileid, PARAM_INT); 2438 $lastsync = clean_param($lastsync, PARAM_INT); 2439 validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED); 2440 $filesize = clean_param($filesize, PARAM_INT); 2441 $status = clean_param($status, PARAM_INT); 2442 $params = array('contenthash' => $contenthash, 2443 'filesize' => $filesize, 2444 'status' => $status, 2445 'referencefileid' => $referencefileid, 2446 'timemodified' => $timemodified); 2447 $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize, 2448 status = :status ' . ($timemodified ? ', timemodified = :timemodified' : '') . ' 2449 WHERE referencefileid = :referencefileid', $params); 2450 $data = array('id' => $referencefileid, 'lastsync' => $lastsync); 2451 $DB->update_record('files_reference', (object)$data); 2452 } 2453 2454 /** 2455 * Calculate and return the contenthash of the supplied file. 2456 * 2457 * @param string $filepath The path to the file on disk 2458 * @return string The file's content hash 2459 */ 2460 public static function hash_from_path($filepath) { 2461 return sha1_file($filepath); 2462 } 2463 2464 /** 2465 * Calculate and return the contenthash of the supplied content. 2466 * 2467 * @param string $content The file content 2468 * @return string The file's content hash 2469 */ 2470 public static function hash_from_string($content) { 2471 return sha1($content ?? ''); 2472 } 2473 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body