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