Merge branch 'MDL-27857-assignment-portfolio_21_STABLE' of git://github.com/mudrd8mz...
[moodle.git] / lib / filestorage / file_storage.php
blob4c97d64f2f25fea352c581e6bea1a79e462b6f87
1 <?php
3 // This file is part of Moodle - http://moodle.org/
4 //
5 // Moodle is free software: you can redistribute it and/or modify
6 // it under the terms of the GNU General Public License as published by
7 // the Free Software Foundation, either version 3 of the License, or
8 // (at your option) any later version.
9 //
10 // Moodle is distributed in the hope that it will be useful,
11 // but WITHOUT ANY WARRANTY; without even the implied warranty of
12 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 // GNU General Public License for more details.
15 // You should have received a copy of the GNU General Public License
16 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
19 /**
20 * Core file storage class definition.
22 * @package core
23 * @subpackage filestorage
24 * @copyright 2008 Petr Skoda {@link http://skodak.org}
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
30 require_once("$CFG->libdir/filestorage/stored_file.php");
32 /**
33 * File storage class used for low level access to stored files.
35 * Only owner of file area may use this class to access own files,
36 * for example only code in mod/assignment/* may access assignment
37 * attachments. When some other part of moodle needs to access
38 * files of modules it has to use file_browser class instead or there
39 * has to be some callback API.
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
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;
57 /**
58 * Constructor - do not use directly use @see get_file_storage() call instead.
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
66 public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
67 $this->filedir = $filedir;
68 $this->trashdir = $trashdir;
69 $this->tempdir = $tempdir;
70 $this->dirpermissions = $dirpermissions;
71 $this->filepermissions = $filepermissions;
73 // make sure the file pool directory exists
74 if (!is_dir($this->filedir)) {
75 if (!mkdir($this->filedir, $this->dirpermissions, true)) {
76 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
78 // place warning file in file pool root
79 if (!file_exists($this->filedir.'/warning.txt')) {
80 file_put_contents($this->filedir.'/warning.txt',
81 '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 // make sure the file pool directory exists
85 if (!is_dir($this->trashdir)) {
86 if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
87 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
92 /**
93 * Calculates sha1 hash of unique full path name information.
95 * This hash is a unique file identifier - it is used to improve
96 * performance and overcome db index size limits.
98 * @param int $contextid
99 * @param string $component
100 * @param string $filearea
101 * @param int $itemid
102 * @param string $filepath
103 * @param string $filename
104 * @return string sha1 hash
106 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
111 * Does this file exist?
113 * @param int $contextid
114 * @param string $component
115 * @param string $filearea
116 * @param int $itemid
117 * @param string $filepath
118 * @param string $filename
119 * @return bool
121 public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
122 $filepath = clean_param($filepath, PARAM_PATH);
123 $filename = clean_param($filename, PARAM_FILE);
125 if ($filename === '') {
126 $filename = '.';
129 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
130 return $this->file_exists_by_hash($pathnamehash);
134 * Does this file exist?
136 * @param string $pathnamehash
137 * @return bool
139 public function file_exists_by_hash($pathnamehash) {
140 global $DB;
142 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
146 * Create instance of file class from database record.
148 * @param stdClass $file_record record from the files table
149 * @return stored_file instance of file abstraction class
151 public function get_file_instance(stdClass $file_record) {
152 return new stored_file($this, $file_record, $this->filedir);
156 * Fetch file using local file id.
158 * Please do not rely on file ids, it is usually easier to use
159 * pathname hashes instead.
161 * @param int $fileid
162 * @return stored_file instance if exists, false if not
164 public function get_file_by_id($fileid) {
165 global $DB;
167 if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
168 return $this->get_file_instance($file_record);
169 } else {
170 return false;
175 * Fetch file using local file full pathname hash
177 * @param string $pathnamehash
178 * @return stored_file instance if exists, false if not
180 public function get_file_by_hash($pathnamehash) {
181 global $DB;
183 if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
184 return $this->get_file_instance($file_record);
185 } else {
186 return false;
191 * Fetch locally stored file.
193 * @param int $contextid
194 * @param string $component
195 * @param string $filearea
196 * @param int $itemid
197 * @param string $filepath
198 * @param string $filename
199 * @return stored_file instance if exists, false if not
201 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
202 $filepath = clean_param($filepath, PARAM_PATH);
203 $filename = clean_param($filename, PARAM_FILE);
205 if ($filename === '') {
206 $filename = '.';
209 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
210 return $this->get_file_by_hash($pathnamehash);
214 * Are there any files (or directories)
215 * @param int $contextid
216 * @param string $component
217 * @param string $filearea
218 * @param bool|int $itemid tem id or false if all items
219 * @param bool $ignoredirs
220 * @return bool empty
222 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
223 global $DB;
225 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
226 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
228 if ($itemid !== false) {
229 $params['itemid'] = $itemid;
230 $where .= " AND itemid = :itemid";
233 if ($ignoredirs) {
234 $sql = "SELECT 'x'
235 FROM {files}
236 WHERE $where AND filename <> '.'";
237 } else {
238 $sql = "SELECT 'x'
239 FROM {files}
240 WHERE $where AND (filename <> '.' OR filepath <> '/')";
243 return !$DB->record_exists_sql($sql, $params);
247 * Returns all area files (optionally limited by itemid)
249 * @param int $contextid
250 * @param string $component
251 * @param string $filearea
252 * @param int $itemid (all files if not specified)
253 * @param string $sort
254 * @param bool $includedirs
255 * @return array of stored_files indexed by pathanmehash
257 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
258 global $DB;
260 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
261 if ($itemid !== false) {
262 $conditions['itemid'] = $itemid;
265 $result = array();
266 $file_records = $DB->get_records('files', $conditions, $sort);
267 foreach ($file_records as $file_record) {
268 if (!$includedirs and $file_record->filename === '.') {
269 continue;
271 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
273 return $result;
277 * Returns array based tree structure of area files
279 * @param int $contextid
280 * @param string $component
281 * @param string $filearea
282 * @param int $itemid
283 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
285 public function get_area_tree($contextid, $component, $filearea, $itemid) {
286 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
287 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
288 // first create directory structure
289 foreach ($files as $hash=>$dir) {
290 if (!$dir->is_directory()) {
291 continue;
293 unset($files[$hash]);
294 if ($dir->get_filepath() === '/') {
295 $result['dirfile'] = $dir;
296 continue;
298 $parts = explode('/', trim($dir->get_filepath(),'/'));
299 $pointer =& $result;
300 foreach ($parts as $part) {
301 if ($part === '') {
302 continue;
304 if (!isset($pointer['subdirs'][$part])) {
305 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
307 $pointer =& $pointer['subdirs'][$part];
309 $pointer['dirfile'] = $dir;
310 unset($pointer);
312 foreach ($files as $hash=>$file) {
313 $parts = explode('/', trim($file->get_filepath(),'/'));
314 $pointer =& $result;
315 foreach ($parts as $part) {
316 if ($part === '') {
317 continue;
319 $pointer =& $pointer['subdirs'][$part];
321 $pointer['files'][$file->get_filename()] = $file;
322 unset($pointer);
324 return $result;
328 * Returns all files and optionally directories
330 * @param int $contextid
331 * @param string $component
332 * @param string $filearea
333 * @param int $itemid
334 * @param int $filepath directory path
335 * @param bool $recursive include all subdirectories
336 * @param bool $includedirs include files and directories
337 * @param string $sort
338 * @return array of stored_files indexed by pathanmehash
340 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
341 global $DB;
343 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
344 return array();
347 if ($recursive) {
349 $dirs = $includedirs ? "" : "AND filename <> '.'";
350 $length = textlib_get_instance()->strlen($filepath);
352 $sql = "SELECT *
353 FROM {files}
354 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
355 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
356 AND id <> :dirid
357 $dirs
358 ORDER BY $sort";
359 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
361 $files = array();
362 $dirs = array();
363 $file_records = $DB->get_records_sql($sql, $params);
364 foreach ($file_records as $file_record) {
365 if ($file_record->filename == '.') {
366 $dirs[$file_record->pathnamehash] = $this->get_file_instance($file_record);
367 } else {
368 $files[$file_record->pathnamehash] = $this->get_file_instance($file_record);
371 $result = array_merge($dirs, $files);
373 } else {
374 $result = array();
375 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
377 $length = textlib_get_instance()->strlen($filepath);
379 if ($includedirs) {
380 $sql = "SELECT *
381 FROM {files}
382 WHERE contextid = :contextid AND component = :component AND filearea = :filearea
383 AND itemid = :itemid AND filename = '.'
384 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
385 AND id <> :dirid
386 ORDER BY $sort";
387 $reqlevel = substr_count($filepath, '/') + 1;
388 $file_records = $DB->get_records_sql($sql, $params);
389 foreach ($file_records as $file_record) {
390 if (substr_count($file_record->filepath, '/') !== $reqlevel) {
391 continue;
393 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
397 $sql = "SELECT *
398 FROM {files}
399 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
400 AND filepath = :filepath AND filename <> '.'
401 ORDER BY $sort";
403 $file_records = $DB->get_records_sql($sql, $params);
404 foreach ($file_records as $file_record) {
405 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
409 return $result;
413 * Delete all area files (optionally limited by itemid).
415 * @param int $contextid
416 * @param string $component
417 * @param string $filearea (all areas in context if not specified)
418 * @param int $itemid (all files if not specified)
419 * @return bool success
421 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
422 global $DB;
424 $conditions = array('contextid'=>$contextid);
425 if ($component !== false) {
426 $conditions['component'] = $component;
428 if ($filearea !== false) {
429 $conditions['filearea'] = $filearea;
431 if ($itemid !== false) {
432 $conditions['itemid'] = $itemid;
435 $file_records = $DB->get_records('files', $conditions);
436 foreach ($file_records as $file_record) {
437 $this->get_file_instance($file_record)->delete();
440 return true; // BC only
444 * Delete all the files from certain areas where itemid is limited by an
445 * arbitrary bit of SQL.
447 * @param int $contextid the id of the context the files belong to. Must be given.
448 * @param string $component the owning component. Must be given.
449 * @param string $filearea the file area name. Must be given.
450 * @param string $itemidstest an SQL fragment that the itemid must match. Used
451 * in the query like WHERE itemid $itemidstest. Must used named parameters,
452 * and may not used named parameters called contextid, component or filearea.
453 * @param array $params any query params used by $itemidstest.
455 public function delete_area_files_select($contextid, $component,
456 $filearea, $itemidstest, array $params = null) {
457 global $DB;
459 $where = "contextid = :contextid
460 AND component = :component
461 AND filearea = :filearea
462 AND itemid $itemidstest";
463 $params['contextid'] = $contextid;
464 $params['component'] = $component;
465 $params['filearea'] = $filearea;
467 $file_records = $DB->get_recordset_select('files', $where, $params);
468 foreach ($file_records as $file_record) {
469 $this->get_file_instance($file_record)->delete();
471 $file_records->close();
475 * Move all the files in a file area from one context to another.
476 * @param integer $oldcontextid the context the files are being moved from.
477 * @param integer $newcontextid the context the files are being moved to.
478 * @param string $component the plugin that these files belong to.
479 * @param string $filearea the name of the file area.
480 * @return integer the number of files moved, for information.
482 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
483 // Note, this code is based on some code that Petr wrote in
484 // forum_move_attachments in mod/forum/lib.php. I moved it here because
485 // I needed it in the question code too.
486 $count = 0;
488 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
489 foreach ($oldfiles as $oldfile) {
490 $filerecord = new stdClass();
491 $filerecord->contextid = $newcontextid;
492 $this->create_file_from_storedfile($filerecord, $oldfile);
493 $count += 1;
496 if ($count) {
497 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
500 return $count;
504 * Recursively creates directory.
506 * @param int $contextid
507 * @param string $component
508 * @param string $filearea
509 * @param int $itemid
510 * @param string $filepath
511 * @param string $filename
512 * @return bool success
514 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
515 global $DB;
517 // validate all parameters, we do not want any rubbish stored in database, right?
518 if (!is_number($contextid) or $contextid < 1) {
519 throw new file_exception('storedfileproblem', 'Invalid contextid');
522 if ($component === '' or $component !== clean_param($component, PARAM_ALPHAEXT)) {
523 throw new file_exception('storedfileproblem', 'Invalid component');
526 if ($filearea === '' or $filearea !== clean_param($filearea, PARAM_ALPHAEXT)) {
527 throw new file_exception('storedfileproblem', 'Invalid filearea');
530 if (!is_number($itemid) or $itemid < 0) {
531 throw new file_exception('storedfileproblem', 'Invalid itemid');
534 $filepath = clean_param($filepath, PARAM_PATH);
535 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
536 // path must start and end with '/'
537 throw new file_exception('storedfileproblem', 'Invalid file path');
540 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
542 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
543 return $dir_info;
546 static $contenthash = null;
547 if (!$contenthash) {
548 $this->add_string_to_pool('');
549 $contenthash = sha1('');
552 $now = time();
554 $dir_record = new stdClass();
555 $dir_record->contextid = $contextid;
556 $dir_record->component = $component;
557 $dir_record->filearea = $filearea;
558 $dir_record->itemid = $itemid;
559 $dir_record->filepath = $filepath;
560 $dir_record->filename = '.';
561 $dir_record->contenthash = $contenthash;
562 $dir_record->filesize = 0;
564 $dir_record->timecreated = $now;
565 $dir_record->timemodified = $now;
566 $dir_record->mimetype = null;
567 $dir_record->userid = $userid;
569 $dir_record->pathnamehash = $pathnamehash;
571 $DB->insert_record('files', $dir_record);
572 $dir_info = $this->get_file_by_hash($pathnamehash);
574 if ($filepath !== '/') {
575 //recurse to parent dirs
576 $filepath = trim($filepath, '/');
577 $filepath = explode('/', $filepath);
578 array_pop($filepath);
579 $filepath = implode('/', $filepath);
580 $filepath = ($filepath === '') ? '/' : "/$filepath/";
581 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
584 return $dir_info;
588 * Add new local file based on existing local file.
590 * @param mixed $file_record object or array describing changes
591 * @param mixed $fileorid id or stored_file instance of the existing local file
592 * @return stored_file instance of newly created file
594 public function create_file_from_storedfile($file_record, $fileorid) {
595 global $DB;
597 if ($fileorid instanceof stored_file) {
598 $fid = $fileorid->get_id();
599 } else {
600 $fid = $fileorid;
603 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
605 unset($file_record['id']);
606 unset($file_record['filesize']);
607 unset($file_record['contenthash']);
608 unset($file_record['pathnamehash']);
610 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
611 throw new file_exception('storedfileproblem', 'File does not exist');
614 unset($newrecord->id);
616 foreach ($file_record as $key=>$value) {
617 // validate all parameters, we do not want any rubbish stored in database, right?
618 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
619 throw new file_exception('storedfileproblem', 'Invalid contextid');
622 if ($key == 'component') {
623 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
624 throw new file_exception('storedfileproblem', 'Invalid component');
628 if ($key == 'filearea') {
629 if ($value === '' or $value !== clean_param($value, PARAM_ALPHAEXT)) {
630 throw new file_exception('storedfileproblem', 'Invalid filearea');
634 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
635 throw new file_exception('storedfileproblem', 'Invalid itemid');
639 if ($key == 'filepath') {
640 $value = clean_param($value, PARAM_PATH);
641 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
642 // path must start and end with '/'
643 throw new file_exception('storedfileproblem', 'Invalid file path');
647 if ($key == 'filename') {
648 $value = clean_param($value, PARAM_FILE);
649 if ($value === '') {
650 // path must start and end with '/'
651 throw new file_exception('storedfileproblem', 'Invalid file name');
655 $newrecord->$key = $value;
658 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
660 if ($newrecord->filename === '.') {
661 // special case - only this function supports directories ;-)
662 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
663 // update the existing directory with the new data
664 $newrecord->id = $directory->get_id();
665 $DB->update_record('files', $newrecord);
666 return $this->get_file_instance($newrecord);
669 try {
670 $newrecord->id = $DB->insert_record('files', $newrecord);
671 } catch (dml_exception $e) {
672 $newrecord->id = false;
675 if (!$newrecord->id) {
676 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
677 $newrecord->filepath, $newrecord->filename);
680 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
682 return $this->get_file_instance($newrecord);
686 * Add new local file.
688 * @param mixed $file_record object or array describing file
689 * @param string $path path to file or content of file
690 * @param array $options @see download_file_content() options
691 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
692 * @return stored_file instance
694 public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
696 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
697 $file_record = (object)$file_record; // we support arrays too
699 $headers = isset($options['headers']) ? $options['headers'] : null;
700 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
701 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
702 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
703 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
704 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
705 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
707 if (!isset($file_record->filename)) {
708 $parts = explode('/', $url);
709 $filename = array_pop($parts);
710 $file_record->filename = clean_param($filename, PARAM_FILE);
712 $source = !empty($file_record->source) ? $file_record->source : $url;
713 $file_record->source = clean_param($source, PARAM_URL);
715 if ($usetempfile) {
716 check_dir_exists($this->tempdir);
717 $tmpfile = tempnam($this->tempdir, 'newfromurl');
718 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
719 if ($content === false) {
720 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
722 try {
723 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
724 @unlink($tmpfile);
725 return $newfile;
726 } catch (Exception $e) {
727 @unlink($tmpfile);
728 throw $e;
731 } else {
732 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
733 if ($content === false) {
734 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
736 return $this->create_file_from_string($file_record, $content);
741 * Add new local file.
743 * @param mixed $file_record object or array describing file
744 * @param string $path path to file or content of file
745 * @return stored_file instance
747 public function create_file_from_pathname($file_record, $pathname) {
748 global $DB;
750 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
751 $file_record = (object)$file_record; // we support arrays too
753 // validate all parameters, we do not want any rubbish stored in database, right?
754 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
755 throw new file_exception('storedfileproblem', 'Invalid contextid');
758 if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
759 throw new file_exception('storedfileproblem', 'Invalid component');
762 if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
763 throw new file_exception('storedfileproblem', 'Invalid filearea');
766 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
767 throw new file_exception('storedfileproblem', 'Invalid itemid');
770 if (!empty($file_record->sortorder)) {
771 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
772 $file_record->sortorder = 0;
774 } else {
775 $file_record->sortorder = 0;
778 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
779 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
780 // path must start and end with '/'
781 throw new file_exception('storedfileproblem', 'Invalid file path');
784 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
785 if ($file_record->filename === '') {
786 // filename must not be empty
787 throw new file_exception('storedfileproblem', 'Invalid file name');
790 $now = time();
792 $newrecord = new stdClass();
794 $newrecord->contextid = $file_record->contextid;
795 $newrecord->component = $file_record->component;
796 $newrecord->filearea = $file_record->filearea;
797 $newrecord->itemid = $file_record->itemid;
798 $newrecord->filepath = $file_record->filepath;
799 $newrecord->filename = $file_record->filename;
801 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
802 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
803 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
804 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
805 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
806 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
807 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
808 $newrecord->sortorder = $file_record->sortorder;
810 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
812 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
814 try {
815 $newrecord->id = $DB->insert_record('files', $newrecord);
816 } catch (dml_exception $e) {
817 $newrecord->id = false;
820 if (!$newrecord->id) {
821 if ($newfile) {
822 $this->deleted_file_cleanup($newrecord->contenthash);
824 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
825 $newrecord->filepath, $newrecord->filename);
828 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
830 return $this->get_file_instance($newrecord);
834 * Add new local file.
836 * @param mixed $file_record object or array describing file
837 * @param string $content content of file
838 * @return stored_file instance
840 public function create_file_from_string($file_record, $content) {
841 global $DB;
843 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
844 $file_record = (object)$file_record; // we support arrays too
846 // validate all parameters, we do not want any rubbish stored in database, right?
847 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
848 throw new file_exception('storedfileproblem', 'Invalid contextid');
851 if ($file_record->component === '' or $file_record->component !== clean_param($file_record->component, PARAM_ALPHAEXT)) {
852 throw new file_exception('storedfileproblem', 'Invalid component');
855 if ($file_record->filearea === '' or $file_record->filearea !== clean_param($file_record->filearea, PARAM_ALPHAEXT)) {
856 throw new file_exception('storedfileproblem', 'Invalid filearea');
859 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
860 throw new file_exception('storedfileproblem', 'Invalid itemid');
863 if (!empty($file_record->sortorder)) {
864 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
865 $file_record->sortorder = 0;
867 } else {
868 $file_record->sortorder = 0;
871 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
872 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
873 // path must start and end with '/'
874 throw new file_exception('storedfileproblem', 'Invalid file path');
877 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
878 if ($file_record->filename === '') {
879 // path must start and end with '/'
880 throw new file_exception('storedfileproblem', 'Invalid file name');
883 $now = time();
885 $newrecord = new stdClass();
887 $newrecord->contextid = $file_record->contextid;
888 $newrecord->component = $file_record->component;
889 $newrecord->filearea = $file_record->filearea;
890 $newrecord->itemid = $file_record->itemid;
891 $newrecord->filepath = $file_record->filepath;
892 $newrecord->filename = $file_record->filename;
894 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
895 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
896 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
897 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
898 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
899 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
900 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
901 $newrecord->sortorder = $file_record->sortorder;
903 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
905 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
907 try {
908 $newrecord->id = $DB->insert_record('files', $newrecord);
909 } catch (dml_exception $e) {
910 $newrecord->id = false;
913 if (!$newrecord->id) {
914 if ($newfile) {
915 $this->deleted_file_cleanup($newrecord->contenthash);
917 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
918 $newrecord->filepath, $newrecord->filename);
921 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
923 return $this->get_file_instance($newrecord);
927 * Creates new image file from existing.
929 * @param mixed $file_record object or array describing new file
930 * @param mixed file id or stored file object
931 * @param int $newwidth in pixels
932 * @param int $newheight in pixels
933 * @param bool $keepaspectratio
934 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
935 * @return stored_file instance
937 public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
938 if ($fid instanceof stored_file) {
939 $fid = $fid->get_id();
942 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
944 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
945 throw new file_exception('storedfileproblem', 'File does not exist');
948 if (!$imageinfo = $file->get_imageinfo()) {
949 throw new file_exception('storedfileproblem', 'File is not an image');
952 if (!isset($file_record['filename'])) {
953 $file_record['filename'] == $file->get_filename();
956 if (!isset($file_record['mimetype'])) {
957 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
960 $width = $imageinfo['width'];
961 $height = $imageinfo['height'];
962 $mimetype = $imageinfo['mimetype'];
964 if ($keepaspectratio) {
965 if (0 >= $newwidth and 0 >= $newheight) {
966 // no sizes specified
967 $newwidth = $width;
968 $newheight = $height;
970 } else if (0 < $newwidth and 0 < $newheight) {
971 $xheight = ($newwidth*($height/$width));
972 if ($xheight < $newheight) {
973 $newheight = (int)$xheight;
974 } else {
975 $newwidth = (int)($newheight*($width/$height));
978 } else if (0 < $newwidth) {
979 $newheight = (int)($newwidth*($height/$width));
981 } else { //0 < $newheight
982 $newwidth = (int)($newheight*($width/$height));
985 } else {
986 if (0 >= $newwidth) {
987 $newwidth = $width;
989 if (0 >= $newheight) {
990 $newheight = $height;
994 $img = imagecreatefromstring($file->get_content());
995 if ($height != $newheight or $width != $newwidth) {
996 $newimg = imagecreatetruecolor($newwidth, $newheight);
997 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
998 // weird
999 throw new file_exception('storedfileproblem', 'Can not resize image');
1001 imagedestroy($img);
1002 $img = $newimg;
1005 ob_start();
1006 switch ($file_record['mimetype']) {
1007 case 'image/gif':
1008 imagegif($img);
1009 break;
1011 case 'image/jpeg':
1012 if (is_null($quality)) {
1013 imagejpeg($img);
1014 } else {
1015 imagejpeg($img, NULL, $quality);
1017 break;
1019 case 'image/png':
1020 $quality = (int)$quality;
1021 imagepng($img, NULL, $quality, NULL);
1022 break;
1024 default:
1025 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1028 $content = ob_get_contents();
1029 ob_end_clean();
1030 imagedestroy($img);
1032 if (!$content) {
1033 throw new file_exception('storedfileproblem', 'Can not convert image');
1036 return $this->create_file_from_string($file_record, $content);
1040 * Add file content to sha1 pool.
1042 * @param string $pathname path to file
1043 * @param string $contenthash sha1 hash of content if known (performance only)
1044 * @return array (contenthash, filesize, newfile)
1046 public function add_file_to_pool($pathname, $contenthash = NULL) {
1047 if (!is_readable($pathname)) {
1048 throw new file_exception('storedfilecannotread', '', $pathname);
1051 if (is_null($contenthash)) {
1052 $contenthash = sha1_file($pathname);
1055 $filesize = filesize($pathname);
1057 $hashpath = $this->path_from_hash($contenthash);
1058 $hashfile = "$hashpath/$contenthash";
1060 if (file_exists($hashfile)) {
1061 if (filesize($hashfile) !== $filesize) {
1062 throw new file_pool_content_exception($contenthash);
1064 $newfile = false;
1066 } else {
1067 if (!is_dir($hashpath)) {
1068 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1069 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1072 $newfile = true;
1074 if (!copy($pathname, $hashfile)) {
1075 throw new file_exception('storedfilecannotread', '', $pathname);
1078 if (filesize($hashfile) !== $filesize) {
1079 @unlink($hashfile);
1080 throw new file_pool_content_exception($contenthash);
1082 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1086 return array($contenthash, $filesize, $newfile);
1090 * Add string content to sha1 pool.
1092 * @param string $content file content - binary string
1093 * @return array (contenthash, filesize, newfile)
1095 public function add_string_to_pool($content) {
1096 $contenthash = sha1($content);
1097 $filesize = strlen($content); // binary length
1099 $hashpath = $this->path_from_hash($contenthash);
1100 $hashfile = "$hashpath/$contenthash";
1103 if (file_exists($hashfile)) {
1104 if (filesize($hashfile) !== $filesize) {
1105 throw new file_pool_content_exception($contenthash);
1107 $newfile = false;
1109 } else {
1110 if (!is_dir($hashpath)) {
1111 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1112 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1115 $newfile = true;
1117 file_put_contents($hashfile, $content);
1119 if (filesize($hashfile) !== $filesize) {
1120 @unlink($hashfile);
1121 throw new file_pool_content_exception($contenthash);
1123 chmod($hashfile, $this->filepermissions); // fix permissions if needed
1126 return array($contenthash, $filesize, $newfile);
1130 * Return path to file with given hash.
1132 * NOTE: must not be public, files in pool must not be modified
1134 * @param string $contenthash
1135 * @return string expected file location
1137 protected function path_from_hash($contenthash) {
1138 $l1 = $contenthash[0].$contenthash[1];
1139 $l2 = $contenthash[2].$contenthash[3];
1140 return "$this->filedir/$l1/$l2";
1144 * Return path to file with given hash.
1146 * NOTE: must not be public, files in pool must not be modified
1148 * @param string $contenthash
1149 * @return string expected file location
1151 protected function trash_path_from_hash($contenthash) {
1152 $l1 = $contenthash[0].$contenthash[1];
1153 $l2 = $contenthash[2].$contenthash[3];
1154 return "$this->trashdir/$l1/$l2";
1158 * Tries to recover missing content of file from trash.
1160 * @param object $file_record
1161 * @return bool success
1163 public function try_content_recovery($file) {
1164 $contenthash = $file->get_contenthash();
1165 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1166 if (!is_readable($trashfile)) {
1167 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1168 return false;
1170 // nice, at least alternative trash file in trash root exists
1171 $trashfile = $this->trashdir.'/'.$contenthash;
1173 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1174 //weird, better fail early
1175 return false;
1177 $contentdir = $this->path_from_hash($contenthash);
1178 $contentfile = $contentdir.'/'.$contenthash;
1179 if (file_exists($contentfile)) {
1180 //strange, no need to recover anything
1181 return true;
1183 if (!is_dir($contentdir)) {
1184 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1185 return false;
1188 return rename($trashfile, $contentfile);
1192 * Marks pool file as candidate for deleting.
1194 * DO NOT call directly - reserved for core!!
1196 * @param string $contenthash
1197 * @return void
1199 public function deleted_file_cleanup($contenthash) {
1200 global $DB;
1202 //Note: this section is critical - in theory file could be reused at the same
1203 // time, if this happens we can still recover the file from trash
1204 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1205 // file content is still used
1206 return;
1208 //move content file to trash
1209 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1210 if (!file_exists($contentfile)) {
1211 //weird, but no problem
1212 return;
1214 $trashpath = $this->trash_path_from_hash($contenthash);
1215 $trashfile = $trashpath.'/'.$contenthash;
1216 if (file_exists($trashfile)) {
1217 // we already have this content in trash, no need to move it there
1218 unlink($contentfile);
1219 return;
1221 if (!is_dir($trashpath)) {
1222 mkdir($trashpath, $this->dirpermissions, true);
1224 rename($contentfile, $trashfile);
1225 chmod($trashfile, $this->filepermissions); // fix permissions if needed
1229 * Cron cleanup job.
1231 * @return void
1233 public function cron() {
1234 global $CFG, $DB;
1236 // find out all stale draft areas (older than 4 days) and purge them
1237 // those are identified by time stamp of the /. root dir
1238 mtrace('Deleting old draft files... ', '');
1239 $old = time() - 60*60*24*4;
1240 $sql = "SELECT *
1241 FROM {files}
1242 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1243 AND timecreated < :old";
1244 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1245 foreach ($rs as $dir) {
1246 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1248 $rs->close();
1249 mtrace('done.');
1251 // remove trash pool files once a day
1252 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1253 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1254 require_once($CFG->libdir.'/filelib.php');
1255 // Delete files that are associated with a context that no longer exists.
1256 mtrace('Cleaning up files from deleted contexts... ', '');
1257 $sql = "SELECT DISTINCT f.contextid
1258 FROM {files} f
1259 LEFT OUTER JOIN {context} c ON f.contextid = c.id
1260 WHERE c.id IS NULL";
1261 $rs = $DB->get_recordset_sql($sql);
1262 if ($rs->valid()) {
1263 $fs = get_file_storage();
1264 foreach ($rs as $ctx) {
1265 $fs->delete_area_files($ctx->contextid);
1268 $rs->close();
1269 mtrace('done.');
1271 mtrace('Deleting trash files... ', '');
1272 fulldelete($this->trashdir);
1273 set_config('fileslastcleanup', time());
1274 mtrace('done.');