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