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