[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/filestorage/ -> file_storage.php (source)

   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  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1