weekly release 2.3dev
[moodle.git] / lib / filestorage / file_storage.php
CommitLineData
25aebf09 1<?php
2
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.
14//
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/>.
17
18
19/**
20 * Core file storage class definition.
21 *
64f93798
PS
22 * @package core
23 * @subpackage filestorage
bf9ffe27 24 * @copyright 2008 Petr Skoda {@link http://skodak.org}
64a19b38 25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25aebf09 26 */
172dd12c 27
64f93798
PS
28defined('MOODLE_INTERNAL') || die();
29
30require_once("$CFG->libdir/filestorage/stored_file.php");
172dd12c 31
25aebf09 32/**
33 * File storage class used for low level access to stored files.
bf9ffe27 34 *
25aebf09 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
bf9ffe27
PS
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.
40 *
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
25aebf09 44 */
172dd12c 45class file_storage {
bf9ffe27 46 /** @var string Directory with file contents */
172dd12c 47 private $filedir;
bf9ffe27 48 /** @var string Contents of deleted files not needed any more */
1aa01caf 49 private $trashdir;
3a1055a5
PS
50 /** @var string tempdir */
51 private $tempdir;
bf9ffe27 52 /** @var int Permissions for new directories */
1aa01caf 53 private $dirpermissions;
bf9ffe27 54 /** @var int Permissions for new files */
1aa01caf 55 private $filepermissions;
bf9ffe27 56
172dd12c 57 /**
bf9ffe27
PS
58 * Constructor - do not use directly use @see get_file_storage() call instead.
59 *
172dd12c 60 * @param string $filedir full path to pool directory
bf9ffe27 61 * @param string $trashdir temporary storage of deleted area
3a1055a5 62 * @param string $tempdir temporary storage of various files
bf9ffe27
PS
63 * @param int $dirpermissions new directory permissions
64 * @param int $filepermissions new file permissions
172dd12c 65 */
3a1055a5 66 public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
1aa01caf 67 $this->filedir = $filedir;
68 $this->trashdir = $trashdir;
3a1055a5 69 $this->tempdir = $tempdir;
1aa01caf 70 $this->dirpermissions = $dirpermissions;
71 $this->filepermissions = $filepermissions;
172dd12c 72
73 // make sure the file pool directory exists
74 if (!is_dir($this->filedir)) {
1aa01caf 75 if (!mkdir($this->filedir, $this->dirpermissions, true)) {
145a0a31 76 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
172dd12c 77 }
78 // place warning file in file pool root
1aa01caf 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.');
82 }
83 }
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
88 }
172dd12c 89 }
90 }
91
92 /**
bf9ffe27
PS
93 * Calculates sha1 hash of unique full path name information.
94 *
95 * This hash is a unique file identifier - it is used to improve
96 * performance and overcome db index size limits.
97 *
172dd12c 98 * @param int $contextid
64f93798 99 * @param string $component
172dd12c 100 * @param string $filearea
101 * @param int $itemid
102 * @param string $filepath
103 * @param string $filename
bf9ffe27 104 * @return string sha1 hash
172dd12c 105 */
64f93798
PS
106 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
172dd12c 108 }
109
110 /**
111 * Does this file exist?
bf9ffe27 112 *
172dd12c 113 * @param int $contextid
64f93798 114 * @param string $component
172dd12c 115 * @param string $filearea
116 * @param int $itemid
117 * @param string $filepath
118 * @param string $filename
119 * @return bool
120 */
64f93798 121 public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
172dd12c 122 $filepath = clean_param($filepath, PARAM_PATH);
123 $filename = clean_param($filename, PARAM_FILE);
124
125 if ($filename === '') {
126 $filename = '.';
127 }
128
64f93798 129 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
172dd12c 130 return $this->file_exists_by_hash($pathnamehash);
131 }
132
133 /**
134 * Does this file exist?
bf9ffe27 135 *
172dd12c 136 * @param string $pathnamehash
137 * @return bool
138 */
139 public function file_exists_by_hash($pathnamehash) {
140 global $DB;
141
142 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
143 }
144
693ef3a8
PS
145 /**
146 * Create instance of file class from database record.
147 *
148 * @param stdClass $file_record record from the files table
149 * @return stored_file instance of file abstraction class
150 */
151 public function get_file_instance(stdClass $file_record) {
152 return new stored_file($this, $file_record, $this->filedir);
153 }
154
172dd12c 155 /**
25aebf09 156 * Fetch file using local file id.
bf9ffe27 157 *
25aebf09 158 * Please do not rely on file ids, it is usually easier to use
159 * pathname hashes instead.
bf9ffe27 160 *
172dd12c 161 * @param int $fileid
bf9ffe27 162 * @return stored_file instance if exists, false if not
172dd12c 163 */
164 public function get_file_by_id($fileid) {
165 global $DB;
166
167 if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
693ef3a8 168 return $this->get_file_instance($file_record);
172dd12c 169 } else {
170 return false;
171 }
172 }
173
174 /**
175 * Fetch file using local file full pathname hash
bf9ffe27 176 *
172dd12c 177 * @param string $pathnamehash
bf9ffe27 178 * @return stored_file instance if exists, false if not
172dd12c 179 */
180 public function get_file_by_hash($pathnamehash) {
181 global $DB;
182
183 if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
693ef3a8 184 return $this->get_file_instance($file_record);
172dd12c 185 } else {
186 return false;
187 }
188 }
189
190 /**
bf9ffe27
PS
191 * Fetch locally stored file.
192 *
172dd12c 193 * @param int $contextid
64f93798 194 * @param string $component
172dd12c 195 * @param string $filearea
196 * @param int $itemid
197 * @param string $filepath
198 * @param string $filename
bf9ffe27 199 * @return stored_file instance if exists, false if not
172dd12c 200 */
64f93798 201 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
172dd12c 202 $filepath = clean_param($filepath, PARAM_PATH);
203 $filename = clean_param($filename, PARAM_FILE);
204
205 if ($filename === '') {
206 $filename = '.';
207 }
208
64f93798 209 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
172dd12c 210 return $this->get_file_by_hash($pathnamehash);
211 }
212
16741cac
PS
213 /**
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
221 */
222 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
223 global $DB;
224
225 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
226 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
227
228 if ($itemid !== false) {
229 $params['itemid'] = $itemid;
230 $where .= " AND itemid = :itemid";
231 }
232
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 <> '/')";
241 }
242
243 return !$DB->record_exists_sql($sql, $params);
244 }
245
172dd12c 246 /**
247 * Returns all area files (optionally limited by itemid)
bf9ffe27 248 *
172dd12c 249 * @param int $contextid
64f93798 250 * @param string $component
172dd12c 251 * @param string $filearea
252 * @param int $itemid (all files if not specified)
253 * @param string $sort
254 * @param bool $includedirs
cd5be217 255 * @return array of stored_files indexed by pathanmehash
172dd12c 256 */
16741cac 257 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
172dd12c 258 global $DB;
259
64f93798 260 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
172dd12c 261 if ($itemid !== false) {
262 $conditions['itemid'] = $itemid;
263 }
264
265 $result = array();
266 $file_records = $DB->get_records('files', $conditions, $sort);
267 foreach ($file_records as $file_record) {
46fcbcf4 268 if (!$includedirs and $file_record->filename === '.') {
172dd12c 269 continue;
270 }
693ef3a8 271 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
172dd12c 272 }
273 return $result;
274 }
275
752b9f42 276 /**
277 * Returns array based tree structure of area files
bf9ffe27 278 *
752b9f42 279 * @param int $contextid
64f93798 280 * @param string $component
752b9f42 281 * @param string $filearea
282 * @param int $itemid
283 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
284 */
64f93798 285 public function get_area_tree($contextid, $component, $filearea, $itemid) {
752b9f42 286 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
d3427cfe 287 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
752b9f42 288 // first create directory structure
289 foreach ($files as $hash=>$dir) {
290 if (!$dir->is_directory()) {
291 continue;
292 }
293 unset($files[$hash]);
294 if ($dir->get_filepath() === '/') {
295 $result['dirfile'] = $dir;
296 continue;
297 }
298 $parts = explode('/', trim($dir->get_filepath(),'/'));
299 $pointer =& $result;
300 foreach ($parts as $part) {
3b607678 301 if ($part === '') {
302 continue;
303 }
752b9f42 304 if (!isset($pointer['subdirs'][$part])) {
305 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
306 }
307 $pointer =& $pointer['subdirs'][$part];
308 }
309 $pointer['dirfile'] = $dir;
310 unset($pointer);
311 }
312 foreach ($files as $hash=>$file) {
313 $parts = explode('/', trim($file->get_filepath(),'/'));
314 $pointer =& $result;
315 foreach ($parts as $part) {
3b607678 316 if ($part === '') {
317 continue;
318 }
752b9f42 319 $pointer =& $pointer['subdirs'][$part];
320 }
321 $pointer['files'][$file->get_filename()] = $file;
322 unset($pointer);
323 }
324 return $result;
325 }
326
ee03a651 327 /**
bf9ffe27
PS
328 * Returns all files and optionally directories
329 *
ee03a651 330 * @param int $contextid
64f93798 331 * @param string $component
ee03a651 332 * @param string $filearea
333 * @param int $itemid
334 * @param int $filepath directory path
335 * @param bool $recursive include all subdirectories
46fcbcf4 336 * @param bool $includedirs include files and directories
ee03a651 337 * @param string $sort
cd5be217 338 * @return array of stored_files indexed by pathanmehash
ee03a651 339 */
64f93798 340 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
ee03a651 341 global $DB;
342
64f93798 343 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
ee03a651 344 return array();
345 }
346
347 if ($recursive) {
348
46fcbcf4 349 $dirs = $includedirs ? "" : "AND filename <> '.'";
ee03a651 350 $length = textlib_get_instance()->strlen($filepath);
351
352 $sql = "SELECT *
353 FROM {files}
64f93798 354 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
655bbf51 355 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 356 AND id <> :dirid
357 $dirs
358 ORDER BY $sort";
64f93798 359 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
ee03a651 360
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 == '.') {
693ef3a8 366 $dirs[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 367 } else {
693ef3a8 368 $files[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 369 }
370 }
371 $result = array_merge($dirs, $files);
372
373 } else {
374 $result = array();
64f93798 375 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
ee03a651 376
377 $length = textlib_get_instance()->strlen($filepath);
378
46fcbcf4 379 if ($includedirs) {
ee03a651 380 $sql = "SELECT *
381 FROM {files}
64f93798 382 WHERE contextid = :contextid AND component = :component AND filearea = :filearea
ee03a651 383 AND itemid = :itemid AND filename = '.'
655bbf51 384 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 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;
392 }
693ef3a8 393 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 394 }
395 }
396
397 $sql = "SELECT *
398 FROM {files}
64f93798 399 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
ee03a651 400 AND filepath = :filepath AND filename <> '.'
401 ORDER BY $sort";
402
403 $file_records = $DB->get_records_sql($sql, $params);
404 foreach ($file_records as $file_record) {
693ef3a8 405 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 406 }
407 }
408
409 return $result;
410 }
411
172dd12c 412 /**
bf9ffe27
PS
413 * Delete all area files (optionally limited by itemid).
414 *
172dd12c 415 * @param int $contextid
64f93798 416 * @param string $component
6311eb61 417 * @param string $filearea (all areas in context if not specified)
172dd12c 418 * @param int $itemid (all files if not specified)
bf9ffe27 419 * @return bool success
172dd12c 420 */
64f93798 421 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
172dd12c 422 global $DB;
423
6311eb61 424 $conditions = array('contextid'=>$contextid);
64f93798
PS
425 if ($component !== false) {
426 $conditions['component'] = $component;
427 }
6311eb61 428 if ($filearea !== false) {
429 $conditions['filearea'] = $filearea;
430 }
172dd12c 431 if ($itemid !== false) {
432 $conditions['itemid'] = $itemid;
433 }
434
172dd12c 435 $file_records = $DB->get_records('files', $conditions);
436 foreach ($file_records as $file_record) {
af7b3673 437 $this->get_file_instance($file_record)->delete();
172dd12c 438 }
439
bf9ffe27 440 return true; // BC only
172dd12c 441 }
442
af7b3673
TH
443 /**
444 * Delete all the files from certain areas where itemid is limited by an
445 * arbitrary bit of SQL.
446 *
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.
454 */
455 public function delete_area_files_select($contextid, $component,
456 $filearea, $itemidstest, array $params = null) {
457 global $DB;
458
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;
466
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();
470 }
471 $file_records->close();
472 }
473
d2af1014
TH
474 /**
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.
481 */
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;
487
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;
494 }
495
496 if ($count) {
497 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
498 }
499
500 return $count;
501 }
502
172dd12c 503 /**
bf9ffe27
PS
504 * Recursively creates directory.
505 *
172dd12c 506 * @param int $contextid
64f93798 507 * @param string $component
172dd12c 508 * @param string $filearea
509 * @param int $itemid
510 * @param string $filepath
511 * @param string $filename
512 * @return bool success
513 */
64f93798 514 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
172dd12c 515 global $DB;
516
517 // validate all parameters, we do not want any rubbish stored in database, right?
518 if (!is_number($contextid) or $contextid < 1) {
145a0a31 519 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 520 }
521
aff24313
PS
522 $component = clean_param($component, PARAM_COMPONENT);
523 if (empty($component)) {
64f93798
PS
524 throw new file_exception('storedfileproblem', 'Invalid component');
525 }
526
aff24313
PS
527 $filearea = clean_param($filearea, PARAM_AREA);
528 if (empty($filearea)) {
145a0a31 529 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 530 }
531
532 if (!is_number($itemid) or $itemid < 0) {
145a0a31 533 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 534 }
535
536 $filepath = clean_param($filepath, PARAM_PATH);
537 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
538 // path must start and end with '/'
145a0a31 539 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 540 }
541
64f93798 542 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
172dd12c 543
544 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
545 return $dir_info;
546 }
547
548 static $contenthash = null;
549 if (!$contenthash) {
b48f3e06 550 $this->add_string_to_pool('');
172dd12c 551 $contenthash = sha1('');
552 }
553
554 $now = time();
555
ac6f1a82 556 $dir_record = new stdClass();
172dd12c 557 $dir_record->contextid = $contextid;
64f93798 558 $dir_record->component = $component;
172dd12c 559 $dir_record->filearea = $filearea;
560 $dir_record->itemid = $itemid;
561 $dir_record->filepath = $filepath;
562 $dir_record->filename = '.';
563 $dir_record->contenthash = $contenthash;
564 $dir_record->filesize = 0;
565
566 $dir_record->timecreated = $now;
567 $dir_record->timemodified = $now;
568 $dir_record->mimetype = null;
569 $dir_record->userid = $userid;
570
571 $dir_record->pathnamehash = $pathnamehash;
572
573 $DB->insert_record('files', $dir_record);
574 $dir_info = $this->get_file_by_hash($pathnamehash);
575
576 if ($filepath !== '/') {
577 //recurse to parent dirs
578 $filepath = trim($filepath, '/');
579 $filepath = explode('/', $filepath);
580 array_pop($filepath);
581 $filepath = implode('/', $filepath);
582 $filepath = ($filepath === '') ? '/' : "/$filepath/";
64f93798 583 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
172dd12c 584 }
585
586 return $dir_info;
587 }
588
589 /**
bf9ffe27
PS
590 * Add new local file based on existing local file.
591 *
172dd12c 592 * @param mixed $file_record object or array describing changes
72d0aed6 593 * @param mixed $fileorid id or stored_file instance of the existing local file
bf9ffe27 594 * @return stored_file instance of newly created file
172dd12c 595 */
72d0aed6 596 public function create_file_from_storedfile($file_record, $fileorid) {
4fb2306e 597 global $DB;
172dd12c 598
72d0aed6 599 if ($fileorid instanceof stored_file) {
600 $fid = $fileorid->get_id();
601 } else {
602 $fid = $fileorid;
8eb1e0a1 603 }
604
ec8b711f 605 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
606
172dd12c 607 unset($file_record['id']);
608 unset($file_record['filesize']);
609 unset($file_record['contenthash']);
8eb1e0a1 610 unset($file_record['pathnamehash']);
172dd12c 611
ebcac6c6 612 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
145a0a31 613 throw new file_exception('storedfileproblem', 'File does not exist');
172dd12c 614 }
615
616 unset($newrecord->id);
617
618 foreach ($file_record as $key=>$value) {
619 // validate all parameters, we do not want any rubbish stored in database, right?
620 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
145a0a31 621 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 622 }
623
64f93798 624 if ($key == 'component') {
aff24313
PS
625 $value = clean_param($value, PARAM_COMPONENT);
626 if (empty($value)) {
64f93798
PS
627 throw new file_exception('storedfileproblem', 'Invalid component');
628 }
629 }
630
172dd12c 631 if ($key == 'filearea') {
aff24313
PS
632 $value = clean_param($value, PARAM_AREA);
633 if (empty($value)) {
145a0a31 634 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 635 }
636 }
637
638 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
145a0a31 639 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 640 }
641
642
643 if ($key == 'filepath') {
644 $value = clean_param($value, PARAM_PATH);
00c32c54 645 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
172dd12c 646 // path must start and end with '/'
145a0a31 647 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 648 }
649 }
650
651 if ($key == 'filename') {
652 $value = clean_param($value, PARAM_FILE);
653 if ($value === '') {
654 // path must start and end with '/'
145a0a31 655 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 656 }
657 }
658
260c4a5b
PS
659 if ($key === 'timecreated' or $key === 'timemodified') {
660 if (!is_number($value)) {
661 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
662 }
663 if ($value < 0) {
664 //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)
665 $value = 0;
666 }
667 }
668
172dd12c 669 $newrecord->$key = $value;
670 }
671
64f93798 672 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 673
cd5be217 674 if ($newrecord->filename === '.') {
675 // special case - only this function supports directories ;-)
64f93798 676 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
cd5be217 677 // update the existing directory with the new data
678 $newrecord->id = $directory->get_id();
b8ac7ece 679 $DB->update_record('files', $newrecord);
693ef3a8 680 return $this->get_file_instance($newrecord);
cd5be217 681 }
682
172dd12c 683 try {
684 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 685 } catch (dml_exception $e) {
64f93798 686 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 687 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 688 }
689
64f93798 690 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 691
693ef3a8 692 return $this->get_file_instance($newrecord);
172dd12c 693 }
694
6e73ac42 695 /**
bf9ffe27
PS
696 * Add new local file.
697 *
6e73ac42 698 * @param mixed $file_record object or array describing file
699 * @param string $path path to file or content of file
700 * @param array $options @see download_file_content() options
3a1055a5 701 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
bf9ffe27 702 * @return stored_file instance
6e73ac42 703 */
3a1055a5 704 public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
ec8b711f 705
706 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
6e73ac42 707 $file_record = (object)$file_record; // we support arrays too
708
709 $headers = isset($options['headers']) ? $options['headers'] : null;
710 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
711 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
712 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
713 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
714 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
5f1c825d 715 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
6e73ac42 716
6e73ac42 717 if (!isset($file_record->filename)) {
718 $parts = explode('/', $url);
719 $filename = array_pop($parts);
720 $file_record->filename = clean_param($filename, PARAM_FILE);
721 }
1dce6261
DC
722 $source = !empty($file_record->source) ? $file_record->source : $url;
723 $file_record->source = clean_param($source, PARAM_URL);
6e73ac42 724
3a1055a5 725 if ($usetempfile) {
c426ef3a 726 check_dir_exists($this->tempdir);
3a1055a5 727 $tmpfile = tempnam($this->tempdir, 'newfromurl');
60b5a2fe 728 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
3a1055a5
PS
729 if ($content === false) {
730 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
731 }
732 try {
733 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
734 @unlink($tmpfile);
735 return $newfile;
736 } catch (Exception $e) {
737 @unlink($tmpfile);
738 throw $e;
739 }
740
741 } else {
60b5a2fe 742 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
3a1055a5
PS
743 if ($content === false) {
744 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
745 }
746 return $this->create_file_from_string($file_record, $content);
747 }
6e73ac42 748 }
749
172dd12c 750 /**
bf9ffe27
PS
751 * Add new local file.
752 *
172dd12c 753 * @param mixed $file_record object or array describing file
754 * @param string $path path to file or content of file
bf9ffe27 755 * @return stored_file instance
172dd12c 756 */
757 public function create_file_from_pathname($file_record, $pathname) {
4fb2306e 758 global $DB;
172dd12c 759
ec8b711f 760 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 761 $file_record = (object)$file_record; // we support arrays too
762
763 // validate all parameters, we do not want any rubbish stored in database, right?
764 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 765 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 766 }
767
aff24313
PS
768 $file_record->component = clean_param($file_record->component, PARAM_COMPONENT);
769 if (empty($file_record->component)) {
64f93798
PS
770 throw new file_exception('storedfileproblem', 'Invalid component');
771 }
772
aff24313
PS
773 $file_record->filearea = clean_param($file_record->filearea, PARAM_AREA);
774 if (empty($file_record->filearea)) {
145a0a31 775 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 776 }
777
778 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 779 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 780 }
781
f79321f1
DC
782 if (!empty($file_record->sortorder)) {
783 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
784 $file_record->sortorder = 0;
785 }
786 } else {
787 $file_record->sortorder = 0;
788 }
789
172dd12c 790 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
791 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
792 // path must start and end with '/'
145a0a31 793 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 794 }
795
796 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
797 if ($file_record->filename === '') {
e1dcb950 798 // filename must not be empty
145a0a31 799 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 800 }
801
802 $now = time();
260c4a5b
PS
803 if (isset($file_record->timecreated)) {
804 if (!is_number($file_record->timecreated)) {
805 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
806 }
807 if ($file_record->timecreated < 0) {
808 //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)
809 $file_record->timecreated = 0;
810 }
811 } else {
812 $file_record->timecreated = $now;
813 }
814
815 if (isset($file_record->timemodified)) {
816 if (!is_number($file_record->timemodified)) {
817 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
818 }
819 if ($file_record->timemodified < 0) {
820 //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)
821 $file_record->timemodified = 0;
822 }
823 } else {
824 $file_record->timemodified = $now;
825 }
172dd12c 826
ac6f1a82 827 $newrecord = new stdClass();
172dd12c 828
829 $newrecord->contextid = $file_record->contextid;
64f93798 830 $newrecord->component = $file_record->component;
172dd12c 831 $newrecord->filearea = $file_record->filearea;
832 $newrecord->itemid = $file_record->itemid;
833 $newrecord->filepath = $file_record->filepath;
834 $newrecord->filename = $file_record->filename;
835
260c4a5b
PS
836 $newrecord->timecreated = $file_record->timecreated;
837 $newrecord->timemodified = $file_record->timemodified;
172dd12c 838 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
839 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
4fb2306e
PS
840 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
841 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
842 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
f79321f1 843 $newrecord->sortorder = $file_record->sortorder;
172dd12c 844
b48f3e06 845 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
172dd12c 846
64f93798 847 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 848
849 try {
850 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 851 } catch (dml_exception $e) {
172dd12c 852 if ($newfile) {
ead14290 853 $this->deleted_file_cleanup($newrecord->contenthash);
172dd12c 854 }
64f93798 855 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 856 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 857 }
858
64f93798 859 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 860
693ef3a8 861 return $this->get_file_instance($newrecord);
172dd12c 862 }
863
864 /**
bf9ffe27
PS
865 * Add new local file.
866 *
172dd12c 867 * @param mixed $file_record object or array describing file
868 * @param string $content content of file
bf9ffe27 869 * @return stored_file instance
172dd12c 870 */
871 public function create_file_from_string($file_record, $content) {
4fb2306e 872 global $DB;
172dd12c 873
ec8b711f 874 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 875 $file_record = (object)$file_record; // we support arrays too
876
877 // validate all parameters, we do not want any rubbish stored in database, right?
878 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 879 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 880 }
881
aff24313
PS
882 $file_record->component = clean_param($file_record->component, PARAM_COMPONENT);
883 if (empty($file_record->component)) {
64f93798
PS
884 throw new file_exception('storedfileproblem', 'Invalid component');
885 }
886
65e9eac0 887 $file_record->filearea = clean_param($file_record->filearea, PARAM_AREA);
aff24313 888 if (empty($file_record->filearea)) {
145a0a31 889 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 890 }
891
892 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 893 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 894 }
895
f79321f1
DC
896 if (!empty($file_record->sortorder)) {
897 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
898 $file_record->sortorder = 0;
899 }
900 } else {
901 $file_record->sortorder = 0;
902 }
903
172dd12c 904 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
905 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
906 // path must start and end with '/'
145a0a31 907 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 908 }
909
910 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
911 if ($file_record->filename === '') {
912 // path must start and end with '/'
145a0a31 913 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 914 }
915
916 $now = time();
260c4a5b
PS
917 if (isset($file_record->timecreated)) {
918 if (!is_number($file_record->timecreated)) {
919 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
920 }
921 if ($file_record->timecreated < 0) {
922 //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)
923 $file_record->timecreated = 0;
924 }
925 } else {
926 $file_record->timecreated = $now;
927 }
928
929 if (isset($file_record->timemodified)) {
930 if (!is_number($file_record->timemodified)) {
931 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
932 }
933 if ($file_record->timemodified < 0) {
934 //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)
935 $file_record->timemodified = 0;
936 }
937 } else {
938 $file_record->timemodified = $now;
939 }
172dd12c 940
ac6f1a82 941 $newrecord = new stdClass();
172dd12c 942
943 $newrecord->contextid = $file_record->contextid;
64f93798 944 $newrecord->component = $file_record->component;
172dd12c 945 $newrecord->filearea = $file_record->filearea;
946 $newrecord->itemid = $file_record->itemid;
947 $newrecord->filepath = $file_record->filepath;
948 $newrecord->filename = $file_record->filename;
949
260c4a5b
PS
950 $newrecord->timecreated = $file_record->timecreated;
951 $newrecord->timemodified = $file_record->timemodified;
172dd12c 952 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
953 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
4fb2306e
PS
954 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
955 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
956 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
f79321f1 957 $newrecord->sortorder = $file_record->sortorder;
1dce6261 958
b48f3e06 959 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
172dd12c 960
64f93798 961 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 962
963 try {
964 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 965 } catch (dml_exception $e) {
172dd12c 966 if ($newfile) {
ead14290 967 $this->deleted_file_cleanup($newrecord->contenthash);
172dd12c 968 }
64f93798 969 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 970 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 971 }
972
64f93798 973 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 974
693ef3a8 975 return $this->get_file_instance($newrecord);
172dd12c 976 }
977
797f19e8 978 /**
979 * Creates new image file from existing.
bf9ffe27 980 *
797f19e8 981 * @param mixed $file_record object or array describing new file
982 * @param mixed file id or stored file object
983 * @param int $newwidth in pixels
984 * @param int $newheight in pixels
985 * @param bool $keepaspectratio
bf9ffe27
PS
986 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
987 * @return stored_file instance
797f19e8 988 */
bf9ffe27 989 public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
6b2f2184
AD
990 if (!function_exists('imagecreatefromstring')) {
991 //Most likely the GD php extension isn't installed
992 //image conversion cannot succeed
993 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
994 }
995
797f19e8 996 if ($fid instanceof stored_file) {
997 $fid = $fid->get_id();
998 }
999
1000 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
1001
1002 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
1003 throw new file_exception('storedfileproblem', 'File does not exist');
1004 }
1005
1006 if (!$imageinfo = $file->get_imageinfo()) {
1007 throw new file_exception('storedfileproblem', 'File is not an image');
1008 }
1009
1010 if (!isset($file_record['filename'])) {
0ea7fc25 1011 $file_record['filename'] = $file->get_filename();
797f19e8 1012 }
1013
1014 if (!isset($file_record['mimetype'])) {
1015 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
1016 }
1017
1018 $width = $imageinfo['width'];
1019 $height = $imageinfo['height'];
1020 $mimetype = $imageinfo['mimetype'];
1021
1022 if ($keepaspectratio) {
1023 if (0 >= $newwidth and 0 >= $newheight) {
1024 // no sizes specified
1025 $newwidth = $width;
1026 $newheight = $height;
1027
1028 } else if (0 < $newwidth and 0 < $newheight) {
1029 $xheight = ($newwidth*($height/$width));
1030 if ($xheight < $newheight) {
1031 $newheight = (int)$xheight;
1032 } else {
1033 $newwidth = (int)($newheight*($width/$height));
1034 }
1035
1036 } else if (0 < $newwidth) {
1037 $newheight = (int)($newwidth*($height/$width));
1038
1039 } else { //0 < $newheight
1040 $newwidth = (int)($newheight*($width/$height));
1041 }
1042
1043 } else {
1044 if (0 >= $newwidth) {
1045 $newwidth = $width;
1046 }
1047 if (0 >= $newheight) {
1048 $newheight = $height;
1049 }
1050 }
1051
1052 $img = imagecreatefromstring($file->get_content());
1053 if ($height != $newheight or $width != $newwidth) {
1054 $newimg = imagecreatetruecolor($newwidth, $newheight);
1055 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1056 // weird
1057 throw new file_exception('storedfileproblem', 'Can not resize image');
1058 }
1059 imagedestroy($img);
1060 $img = $newimg;
1061 }
1062
1063 ob_start();
1064 switch ($file_record['mimetype']) {
1065 case 'image/gif':
1066 imagegif($img);
1067 break;
1068
1069 case 'image/jpeg':
1070 if (is_null($quality)) {
1071 imagejpeg($img);
1072 } else {
1073 imagejpeg($img, NULL, $quality);
1074 }
1075 break;
1076
1077 case 'image/png':
8bd49ec0 1078 $quality = (int)$quality;
797f19e8 1079 imagepng($img, NULL, $quality, NULL);
1080 break;
1081
1082 default:
1083 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1084 }
1085
1086 $content = ob_get_contents();
1087 ob_end_clean();
1088 imagedestroy($img);
1089
1090 if (!$content) {
1091 throw new file_exception('storedfileproblem', 'Can not convert image');
1092 }
1093
1094 return $this->create_file_from_string($file_record, $content);
1095 }
1096
172dd12c 1097 /**
bf9ffe27
PS
1098 * Add file content to sha1 pool.
1099 *
172dd12c 1100 * @param string $pathname path to file
bf9ffe27
PS
1101 * @param string $contenthash sha1 hash of content if known (performance only)
1102 * @return array (contenthash, filesize, newfile)
172dd12c 1103 */
bf9ffe27 1104 public function add_file_to_pool($pathname, $contenthash = NULL) {
172dd12c 1105 if (!is_readable($pathname)) {
d610cb89 1106 throw new file_exception('storedfilecannotread', '', $pathname);
172dd12c 1107 }
1108
1109 if (is_null($contenthash)) {
1110 $contenthash = sha1_file($pathname);
1111 }
1112
1113 $filesize = filesize($pathname);
1114
1115 $hashpath = $this->path_from_hash($contenthash);
1116 $hashfile = "$hashpath/$contenthash";
1117
1118 if (file_exists($hashfile)) {
1119 if (filesize($hashfile) !== $filesize) {
1120 throw new file_pool_content_exception($contenthash);
1121 }
1122 $newfile = false;
1123
1124 } else {
1aa01caf 1125 if (!is_dir($hashpath)) {
1126 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1127 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1128 }
172dd12c 1129 }
1130 $newfile = true;
1131
6c0e2d08 1132 if (!copy($pathname, $hashfile)) {
d610cb89 1133 throw new file_exception('storedfilecannotread', '', $pathname);
172dd12c 1134 }
172dd12c 1135
1136 if (filesize($hashfile) !== $filesize) {
1137 @unlink($hashfile);
1138 throw new file_pool_content_exception($contenthash);
1139 }
1aa01caf 1140 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1141 }
1142
1143
1144 return array($contenthash, $filesize, $newfile);
1145 }
1146
1147 /**
bf9ffe27
PS
1148 * Add string content to sha1 pool.
1149 *
172dd12c 1150 * @param string $content file content - binary string
bf9ffe27 1151 * @return array (contenthash, filesize, newfile)
172dd12c 1152 */
b48f3e06 1153 public function add_string_to_pool($content) {
172dd12c 1154 $contenthash = sha1($content);
1155 $filesize = strlen($content); // binary length
1156
1157 $hashpath = $this->path_from_hash($contenthash);
1158 $hashfile = "$hashpath/$contenthash";
1159
1160
1161 if (file_exists($hashfile)) {
1162 if (filesize($hashfile) !== $filesize) {
1163 throw new file_pool_content_exception($contenthash);
1164 }
1165 $newfile = false;
1166
1167 } else {
1aa01caf 1168 if (!is_dir($hashpath)) {
1169 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1170 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1171 }
172dd12c 1172 }
1173 $newfile = true;
1174
6c0e2d08 1175 file_put_contents($hashfile, $content);
172dd12c 1176
1177 if (filesize($hashfile) !== $filesize) {
1178 @unlink($hashfile);
1179 throw new file_pool_content_exception($contenthash);
1180 }
1aa01caf 1181 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1182 }
1183
1184 return array($contenthash, $filesize, $newfile);
1185 }
1186
1187 /**
bf9ffe27 1188 * Return path to file with given hash.
172dd12c 1189 *
17d9269f 1190 * NOTE: must not be public, files in pool must not be modified
172dd12c 1191 *
1192 * @param string $contenthash
1193 * @return string expected file location
1194 */
17d9269f 1195 protected function path_from_hash($contenthash) {
172dd12c 1196 $l1 = $contenthash[0].$contenthash[1];
1197 $l2 = $contenthash[2].$contenthash[3];
d0b6f92a 1198 return "$this->filedir/$l1/$l2";
172dd12c 1199 }
1200
1aa01caf 1201 /**
bf9ffe27 1202 * Return path to file with given hash.
1aa01caf 1203 *
1204 * NOTE: must not be public, files in pool must not be modified
1205 *
1206 * @param string $contenthash
1207 * @return string expected file location
1208 */
1209 protected function trash_path_from_hash($contenthash) {
1210 $l1 = $contenthash[0].$contenthash[1];
1211 $l2 = $contenthash[2].$contenthash[3];
d0b6f92a 1212 return "$this->trashdir/$l1/$l2";
1aa01caf 1213 }
1214
1215 /**
bf9ffe27
PS
1216 * Tries to recover missing content of file from trash.
1217 *
1aa01caf 1218 * @param object $file_record
1219 * @return bool success
1220 */
1221 public function try_content_recovery($file) {
1222 $contenthash = $file->get_contenthash();
1223 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1224 if (!is_readable($trashfile)) {
1225 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1226 return false;
1227 }
1228 // nice, at least alternative trash file in trash root exists
1229 $trashfile = $this->trashdir.'/'.$contenthash;
1230 }
1231 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1232 //weird, better fail early
1233 return false;
1234 }
1235 $contentdir = $this->path_from_hash($contenthash);
1236 $contentfile = $contentdir.'/'.$contenthash;
1237 if (file_exists($contentfile)) {
1238 //strange, no need to recover anything
1239 return true;
1240 }
1241 if (!is_dir($contentdir)) {
1242 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1243 return false;
1244 }
1245 }
1246 return rename($trashfile, $contentfile);
1247 }
1248
172dd12c 1249 /**
bf9ffe27
PS
1250 * Marks pool file as candidate for deleting.
1251 *
1252 * DO NOT call directly - reserved for core!!
1253 *
172dd12c 1254 * @param string $contenthash
1aa01caf 1255 * @return void
172dd12c 1256 */
1aa01caf 1257 public function deleted_file_cleanup($contenthash) {
172dd12c 1258 global $DB;
1259
1aa01caf 1260 //Note: this section is critical - in theory file could be reused at the same
1261 // time, if this happens we can still recover the file from trash
1262 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1263 // file content is still used
1264 return;
1265 }
1266 //move content file to trash
1267 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1268 if (!file_exists($contentfile)) {
1269 //weird, but no problem
172dd12c 1270 return;
1271 }
1aa01caf 1272 $trashpath = $this->trash_path_from_hash($contenthash);
1273 $trashfile = $trashpath.'/'.$contenthash;
1274 if (file_exists($trashfile)) {
1275 // we already have this content in trash, no need to move it there
1276 unlink($contentfile);
1277 return;
1278 }
1279 if (!is_dir($trashpath)) {
1280 mkdir($trashpath, $this->dirpermissions, true);
1281 }
1282 rename($contentfile, $trashfile);
1283 chmod($trashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1284 }
1285
1286 /**
1287 * Cron cleanup job.
bf9ffe27
PS
1288 *
1289 * @return void
172dd12c 1290 */
1291 public function cron() {
a881f970 1292 global $CFG, $DB;
64f93798 1293
2e69ea4a
PS
1294 // find out all stale draft areas (older than 4 days) and purge them
1295 // those are identified by time stamp of the /. root dir
1296 mtrace('Deleting old draft files... ', '');
1297 $old = time() - 60*60*24*4;
1298 $sql = "SELECT *
1299 FROM {files}
1300 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
1301 AND timecreated < :old";
1302 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
1303 foreach ($rs as $dir) {
1304 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
1305 }
be981316 1306 $rs->close();
b5541735 1307 mtrace('done.');
64f93798 1308
1aa01caf 1309 // remove trash pool files once a day
1310 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
1311 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
1312 require_once($CFG->libdir.'/filelib.php');
a881f970
SH
1313 // Delete files that are associated with a context that no longer exists.
1314 mtrace('Cleaning up files from deleted contexts... ', '');
1315 $sql = "SELECT DISTINCT f.contextid
1316 FROM {files} f
1317 LEFT OUTER JOIN {context} c ON f.contextid = c.id
1318 WHERE c.id IS NULL";
be981316
EL
1319 $rs = $DB->get_recordset_sql($sql);
1320 if ($rs->valid()) {
a881f970
SH
1321 $fs = get_file_storage();
1322 foreach ($rs as $ctx) {
1323 $fs->delete_area_files($ctx->contextid);
1324 }
1325 }
be981316 1326 $rs->close();
a881f970
SH
1327 mtrace('done.');
1328
1aa01caf 1329 mtrace('Deleting trash files... ', '');
1330 fulldelete($this->trashdir);
1331 set_config('fileslastcleanup', time());
1332 mtrace('done.');
172dd12c 1333 }
1334 }
1335}
bf9ffe27 1336