[ Index ]

PHP Cross Reference of moodle-2.8

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1