MDL-32471 introducing generate_image_thumbnail() function
[moodle.git] / lib / filestorage / file_storage.php
CommitLineData
25aebf09 1<?php
25aebf09 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17
18/**
19 * Core file storage class definition.
20 *
d2b7803e
DC
21 * @package core_files
22 * @copyright 2008 Petr Skoda {@link http://skodak.org}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25aebf09 24 */
172dd12c 25
64f93798
PS
26defined('MOODLE_INTERNAL') || die();
27
28require_once("$CFG->libdir/filestorage/stored_file.php");
172dd12c 29
25aebf09 30/**
31 * File storage class used for low level access to stored files.
bf9ffe27 32 *
25aebf09 33 * Only owner of file area may use this class to access own files,
34 * for example only code in mod/assignment/* may access assignment
bf9ffe27
PS
35 * attachments. When some other part of moodle needs to access
36 * files of modules it has to use file_browser class instead or there
37 * has to be some callback API.
38 *
d2b7803e
DC
39 * @package core_files
40 * @category files
bf9ffe27
PS
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 /**
d2b7803e 58 * Constructor - do not use directly use {@link get_file_storage()} call instead.
bf9ffe27 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 *
d2b7803e
DC
98 * @param int $contextid context ID
99 * @param string $component component
100 * @param string $filearea file area
101 * @param int $itemid item ID
102 * @param string $filepath file path
103 * @param string $filename file name
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 *
d2b7803e
DC
113 * @param int $contextid context ID
114 * @param string $component component
115 * @param string $filearea file area
116 * @param int $itemid item ID
117 * @param string $filepath file path
118 * @param string $filename file name
172dd12c 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 /**
d2b7803e 134 * Whether or not the file exist
bf9ffe27 135 *
d2b7803e 136 * @param string $pathnamehash path name hash
172dd12c 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 *
d2b7803e
DC
161 * @param int $fileid file ID
162 * @return stored_file|bool 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 *
d2b7803e
DC
177 * @param string $pathnamehash path name hash
178 * @return stored_file|bool 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 *
d2b7803e
DC
193 * @param int $contextid context ID
194 * @param string $component component
195 * @param string $filearea file area
196 * @param int $itemid item ID
197 * @param string $filepath file path
198 * @param string $filename file name
199 * @return stored_file|bool 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)
d2b7803e
DC
215 *
216 * @param int $contextid context ID
217 * @param string $component component
218 * @param string $filearea file area
219 * @param bool|int $itemid item id or false if all items
220 * @param bool $ignoredirs whether or not ignore directories
16741cac
PS
221 * @return bool empty
222 */
223 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
224 global $DB;
225
226 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
227 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
228
229 if ($itemid !== false) {
230 $params['itemid'] = $itemid;
231 $where .= " AND itemid = :itemid";
232 }
233
234 if ($ignoredirs) {
235 $sql = "SELECT 'x'
236 FROM {files}
237 WHERE $where AND filename <> '.'";
238 } else {
239 $sql = "SELECT 'x'
240 FROM {files}
241 WHERE $where AND (filename <> '.' OR filepath <> '/')";
242 }
243
244 return !$DB->record_exists_sql($sql, $params);
245 }
246
172dd12c 247 /**
248 * Returns all area files (optionally limited by itemid)
bf9ffe27 249 *
d2b7803e
DC
250 * @param int $contextid context ID
251 * @param string $component component
252 * @param string $filearea file area
253 * @param int $itemid item ID or all files if not specified
254 * @param string $sort sort fields
255 * @param bool $includedirs whether or not include directories
cd5be217 256 * @return array of stored_files indexed by pathanmehash
172dd12c 257 */
16741cac 258 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort="sortorder, itemid, filepath, filename", $includedirs = true) {
172dd12c 259 global $DB;
260
64f93798 261 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
172dd12c 262 if ($itemid !== false) {
263 $conditions['itemid'] = $itemid;
264 }
265
266 $result = array();
267 $file_records = $DB->get_records('files', $conditions, $sort);
268 foreach ($file_records as $file_record) {
46fcbcf4 269 if (!$includedirs and $file_record->filename === '.') {
172dd12c 270 continue;
271 }
693ef3a8 272 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
172dd12c 273 }
274 return $result;
275 }
276
752b9f42 277 /**
278 * Returns array based tree structure of area files
bf9ffe27 279 *
d2b7803e
DC
280 * @param int $contextid context ID
281 * @param string $component component
282 * @param string $filearea file area
283 * @param int $itemid item ID
752b9f42 284 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
285 */
64f93798 286 public function get_area_tree($contextid, $component, $filearea, $itemid) {
752b9f42 287 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
d3427cfe 288 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, "sortorder, itemid, filepath, filename", true);
752b9f42 289 // first create directory structure
290 foreach ($files as $hash=>$dir) {
291 if (!$dir->is_directory()) {
292 continue;
293 }
294 unset($files[$hash]);
295 if ($dir->get_filepath() === '/') {
296 $result['dirfile'] = $dir;
297 continue;
298 }
299 $parts = explode('/', trim($dir->get_filepath(),'/'));
300 $pointer =& $result;
301 foreach ($parts as $part) {
3b607678 302 if ($part === '') {
303 continue;
304 }
752b9f42 305 if (!isset($pointer['subdirs'][$part])) {
306 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
307 }
308 $pointer =& $pointer['subdirs'][$part];
309 }
310 $pointer['dirfile'] = $dir;
311 unset($pointer);
312 }
313 foreach ($files as $hash=>$file) {
314 $parts = explode('/', trim($file->get_filepath(),'/'));
315 $pointer =& $result;
316 foreach ($parts as $part) {
3b607678 317 if ($part === '') {
318 continue;
319 }
752b9f42 320 $pointer =& $pointer['subdirs'][$part];
321 }
322 $pointer['files'][$file->get_filename()] = $file;
323 unset($pointer);
324 }
325 return $result;
326 }
327
ee03a651 328 /**
bf9ffe27
PS
329 * Returns all files and optionally directories
330 *
d2b7803e
DC
331 * @param int $contextid context ID
332 * @param string $component component
333 * @param string $filearea file area
334 * @param int $itemid item ID
ee03a651 335 * @param int $filepath directory path
336 * @param bool $recursive include all subdirectories
46fcbcf4 337 * @param bool $includedirs include files and directories
d2b7803e 338 * @param string $sort sort fields
cd5be217 339 * @return array of stored_files indexed by pathanmehash
ee03a651 340 */
64f93798 341 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
ee03a651 342 global $DB;
343
64f93798 344 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
ee03a651 345 return array();
346 }
347
348 if ($recursive) {
349
46fcbcf4 350 $dirs = $includedirs ? "" : "AND filename <> '.'";
f8311def 351 $length = textlib::strlen($filepath);
ee03a651 352
353 $sql = "SELECT *
354 FROM {files}
64f93798 355 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
655bbf51 356 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 357 AND id <> :dirid
358 $dirs
359 ORDER BY $sort";
64f93798 360 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
ee03a651 361
362 $files = array();
363 $dirs = array();
364 $file_records = $DB->get_records_sql($sql, $params);
365 foreach ($file_records as $file_record) {
366 if ($file_record->filename == '.') {
693ef3a8 367 $dirs[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 368 } else {
693ef3a8 369 $files[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 370 }
371 }
372 $result = array_merge($dirs, $files);
373
374 } else {
375 $result = array();
64f93798 376 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
ee03a651 377
f8311def 378 $length = textlib::strlen($filepath);
ee03a651 379
46fcbcf4 380 if ($includedirs) {
ee03a651 381 $sql = "SELECT *
382 FROM {files}
64f93798 383 WHERE contextid = :contextid AND component = :component AND filearea = :filearea
ee03a651 384 AND itemid = :itemid AND filename = '.'
655bbf51 385 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 386 AND id <> :dirid
387 ORDER BY $sort";
388 $reqlevel = substr_count($filepath, '/') + 1;
389 $file_records = $DB->get_records_sql($sql, $params);
390 foreach ($file_records as $file_record) {
391 if (substr_count($file_record->filepath, '/') !== $reqlevel) {
392 continue;
393 }
693ef3a8 394 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 395 }
396 }
397
398 $sql = "SELECT *
399 FROM {files}
64f93798 400 WHERE contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid
ee03a651 401 AND filepath = :filepath AND filename <> '.'
402 ORDER BY $sort";
403
404 $file_records = $DB->get_records_sql($sql, $params);
405 foreach ($file_records as $file_record) {
693ef3a8 406 $result[$file_record->pathnamehash] = $this->get_file_instance($file_record);
ee03a651 407 }
408 }
409
410 return $result;
411 }
412
172dd12c 413 /**
bf9ffe27
PS
414 * Delete all area files (optionally limited by itemid).
415 *
d2b7803e
DC
416 * @param int $contextid context ID
417 * @param string $component component
418 * @param string $filearea file area or all areas in context if not specified
419 * @param int $itemid item ID or all files if not specified
bf9ffe27 420 * @return bool success
172dd12c 421 */
64f93798 422 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
172dd12c 423 global $DB;
424
6311eb61 425 $conditions = array('contextid'=>$contextid);
64f93798
PS
426 if ($component !== false) {
427 $conditions['component'] = $component;
428 }
6311eb61 429 if ($filearea !== false) {
430 $conditions['filearea'] = $filearea;
431 }
172dd12c 432 if ($itemid !== false) {
433 $conditions['itemid'] = $itemid;
434 }
435
172dd12c 436 $file_records = $DB->get_records('files', $conditions);
437 foreach ($file_records as $file_record) {
af7b3673 438 $this->get_file_instance($file_record)->delete();
172dd12c 439 }
440
bf9ffe27 441 return true; // BC only
172dd12c 442 }
443
af7b3673
TH
444 /**
445 * Delete all the files from certain areas where itemid is limited by an
446 * arbitrary bit of SQL.
447 *
448 * @param int $contextid the id of the context the files belong to. Must be given.
449 * @param string $component the owning component. Must be given.
450 * @param string $filearea the file area name. Must be given.
451 * @param string $itemidstest an SQL fragment that the itemid must match. Used
452 * in the query like WHERE itemid $itemidstest. Must used named parameters,
453 * and may not used named parameters called contextid, component or filearea.
454 * @param array $params any query params used by $itemidstest.
455 */
456 public function delete_area_files_select($contextid, $component,
457 $filearea, $itemidstest, array $params = null) {
458 global $DB;
459
460 $where = "contextid = :contextid
461 AND component = :component
462 AND filearea = :filearea
463 AND itemid $itemidstest";
464 $params['contextid'] = $contextid;
465 $params['component'] = $component;
466 $params['filearea'] = $filearea;
467
468 $file_records = $DB->get_recordset_select('files', $where, $params);
469 foreach ($file_records as $file_record) {
470 $this->get_file_instance($file_record)->delete();
471 }
472 $file_records->close();
473 }
474
d2af1014
TH
475 /**
476 * Move all the files in a file area from one context to another.
d2b7803e
DC
477 *
478 * @param int $oldcontextid the context the files are being moved from.
479 * @param int $newcontextid the context the files are being moved to.
d2af1014
TH
480 * @param string $component the plugin that these files belong to.
481 * @param string $filearea the name of the file area.
d2b7803e
DC
482 * @param int $itemid file item ID
483 * @return int the number of files moved, for information.
d2af1014
TH
484 */
485 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
486 // Note, this code is based on some code that Petr wrote in
487 // forum_move_attachments in mod/forum/lib.php. I moved it here because
488 // I needed it in the question code too.
489 $count = 0;
490
491 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
492 foreach ($oldfiles as $oldfile) {
493 $filerecord = new stdClass();
494 $filerecord->contextid = $newcontextid;
495 $this->create_file_from_storedfile($filerecord, $oldfile);
496 $count += 1;
497 }
498
499 if ($count) {
500 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
501 }
502
503 return $count;
504 }
505
172dd12c 506 /**
bf9ffe27
PS
507 * Recursively creates directory.
508 *
d2b7803e
DC
509 * @param int $contextid context ID
510 * @param string $component component
511 * @param string $filearea file area
512 * @param int $itemid item ID
513 * @param string $filepath file path
514 * @param int $userid the user ID
172dd12c 515 * @return bool success
516 */
64f93798 517 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
172dd12c 518 global $DB;
519
520 // validate all parameters, we do not want any rubbish stored in database, right?
521 if (!is_number($contextid) or $contextid < 1) {
145a0a31 522 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 523 }
524
aff24313
PS
525 $component = clean_param($component, PARAM_COMPONENT);
526 if (empty($component)) {
64f93798
PS
527 throw new file_exception('storedfileproblem', 'Invalid component');
528 }
529
aff24313
PS
530 $filearea = clean_param($filearea, PARAM_AREA);
531 if (empty($filearea)) {
145a0a31 532 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 533 }
534
535 if (!is_number($itemid) or $itemid < 0) {
145a0a31 536 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 537 }
538
539 $filepath = clean_param($filepath, PARAM_PATH);
540 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
541 // path must start and end with '/'
145a0a31 542 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 543 }
544
64f93798 545 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
172dd12c 546
547 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
548 return $dir_info;
549 }
550
551 static $contenthash = null;
552 if (!$contenthash) {
b48f3e06 553 $this->add_string_to_pool('');
172dd12c 554 $contenthash = sha1('');
555 }
556
557 $now = time();
558
ac6f1a82 559 $dir_record = new stdClass();
172dd12c 560 $dir_record->contextid = $contextid;
64f93798 561 $dir_record->component = $component;
172dd12c 562 $dir_record->filearea = $filearea;
563 $dir_record->itemid = $itemid;
564 $dir_record->filepath = $filepath;
565 $dir_record->filename = '.';
566 $dir_record->contenthash = $contenthash;
567 $dir_record->filesize = 0;
568
569 $dir_record->timecreated = $now;
570 $dir_record->timemodified = $now;
571 $dir_record->mimetype = null;
572 $dir_record->userid = $userid;
573
574 $dir_record->pathnamehash = $pathnamehash;
575
576 $DB->insert_record('files', $dir_record);
577 $dir_info = $this->get_file_by_hash($pathnamehash);
578
579 if ($filepath !== '/') {
580 //recurse to parent dirs
581 $filepath = trim($filepath, '/');
582 $filepath = explode('/', $filepath);
583 array_pop($filepath);
584 $filepath = implode('/', $filepath);
585 $filepath = ($filepath === '') ? '/' : "/$filepath/";
64f93798 586 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
172dd12c 587 }
588
589 return $dir_info;
590 }
591
592 /**
bf9ffe27
PS
593 * Add new local file based on existing local file.
594 *
d2b7803e
DC
595 * @param stdClass|array $file_record object or array describing changes
596 * @param stored_file|int $fileorid id or stored_file instance of the existing local file
bf9ffe27 597 * @return stored_file instance of newly created file
172dd12c 598 */
72d0aed6 599 public function create_file_from_storedfile($file_record, $fileorid) {
4fb2306e 600 global $DB;
172dd12c 601
72d0aed6 602 if ($fileorid instanceof stored_file) {
603 $fid = $fileorid->get_id();
604 } else {
605 $fid = $fileorid;
8eb1e0a1 606 }
607
ec8b711f 608 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
609
172dd12c 610 unset($file_record['id']);
611 unset($file_record['filesize']);
612 unset($file_record['contenthash']);
8eb1e0a1 613 unset($file_record['pathnamehash']);
172dd12c 614
ebcac6c6 615 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
145a0a31 616 throw new file_exception('storedfileproblem', 'File does not exist');
172dd12c 617 }
618
619 unset($newrecord->id);
620
621 foreach ($file_record as $key=>$value) {
622 // validate all parameters, we do not want any rubbish stored in database, right?
623 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
145a0a31 624 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 625 }
626
64f93798 627 if ($key == 'component') {
aff24313
PS
628 $value = clean_param($value, PARAM_COMPONENT);
629 if (empty($value)) {
64f93798
PS
630 throw new file_exception('storedfileproblem', 'Invalid component');
631 }
632 }
633
172dd12c 634 if ($key == 'filearea') {
aff24313
PS
635 $value = clean_param($value, PARAM_AREA);
636 if (empty($value)) {
145a0a31 637 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 638 }
639 }
640
641 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
145a0a31 642 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 643 }
644
645
646 if ($key == 'filepath') {
647 $value = clean_param($value, PARAM_PATH);
00c32c54 648 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
172dd12c 649 // path must start and end with '/'
145a0a31 650 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 651 }
652 }
653
654 if ($key == 'filename') {
655 $value = clean_param($value, PARAM_FILE);
656 if ($value === '') {
657 // path must start and end with '/'
145a0a31 658 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 659 }
660 }
661
260c4a5b
PS
662 if ($key === 'timecreated' or $key === 'timemodified') {
663 if (!is_number($value)) {
664 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
665 }
666 if ($value < 0) {
667 //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)
668 $value = 0;
669 }
670 }
671
172dd12c 672 $newrecord->$key = $value;
673 }
674
64f93798 675 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 676
cd5be217 677 if ($newrecord->filename === '.') {
678 // special case - only this function supports directories ;-)
64f93798 679 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
cd5be217 680 // update the existing directory with the new data
681 $newrecord->id = $directory->get_id();
b8ac7ece 682 $DB->update_record('files', $newrecord);
693ef3a8 683 return $this->get_file_instance($newrecord);
cd5be217 684 }
685
172dd12c 686 try {
687 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 688 } catch (dml_exception $e) {
64f93798 689 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 690 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 691 }
692
64f93798 693 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 694
693ef3a8 695 return $this->get_file_instance($newrecord);
172dd12c 696 }
697
6e73ac42 698 /**
bf9ffe27
PS
699 * Add new local file.
700 *
d2b7803e
DC
701 * @param stdClass|array $file_record object or array describing file
702 * @param string $url the URL to the file
703 * @param array $options {@link download_file_content()} options
3a1055a5 704 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
d2b7803e 705 * @return stored_file
6e73ac42 706 */
3a1055a5 707 public function create_file_from_url($file_record, $url, array $options = NULL, $usetempfile = false) {
ec8b711f 708
709 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
6e73ac42 710 $file_record = (object)$file_record; // we support arrays too
711
712 $headers = isset($options['headers']) ? $options['headers'] : null;
713 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
714 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
715 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
716 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
717 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
5f1c825d 718 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
6e73ac42 719
6e73ac42 720 if (!isset($file_record->filename)) {
721 $parts = explode('/', $url);
722 $filename = array_pop($parts);
723 $file_record->filename = clean_param($filename, PARAM_FILE);
724 }
1dce6261
DC
725 $source = !empty($file_record->source) ? $file_record->source : $url;
726 $file_record->source = clean_param($source, PARAM_URL);
6e73ac42 727
3a1055a5 728 if ($usetempfile) {
c426ef3a 729 check_dir_exists($this->tempdir);
3a1055a5 730 $tmpfile = tempnam($this->tempdir, 'newfromurl');
60b5a2fe 731 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
3a1055a5
PS
732 if ($content === false) {
733 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
734 }
735 try {
736 $newfile = $this->create_file_from_pathname($file_record, $tmpfile);
737 @unlink($tmpfile);
738 return $newfile;
739 } catch (Exception $e) {
740 @unlink($tmpfile);
741 throw $e;
742 }
743
744 } else {
60b5a2fe 745 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
3a1055a5
PS
746 if ($content === false) {
747 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
748 }
749 return $this->create_file_from_string($file_record, $content);
750 }
6e73ac42 751 }
752
172dd12c 753 /**
bf9ffe27
PS
754 * Add new local file.
755 *
d2b7803e
DC
756 * @param stdClass|array $file_record object or array describing file
757 * @param string $pathname path to file or content of file
758 * @return stored_file
172dd12c 759 */
760 public function create_file_from_pathname($file_record, $pathname) {
4fb2306e 761 global $DB;
172dd12c 762
ec8b711f 763 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 764 $file_record = (object)$file_record; // we support arrays too
765
766 // validate all parameters, we do not want any rubbish stored in database, right?
767 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 768 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 769 }
770
aff24313
PS
771 $file_record->component = clean_param($file_record->component, PARAM_COMPONENT);
772 if (empty($file_record->component)) {
64f93798
PS
773 throw new file_exception('storedfileproblem', 'Invalid component');
774 }
775
aff24313
PS
776 $file_record->filearea = clean_param($file_record->filearea, PARAM_AREA);
777 if (empty($file_record->filearea)) {
145a0a31 778 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 779 }
780
781 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 782 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 783 }
784
f79321f1
DC
785 if (!empty($file_record->sortorder)) {
786 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
787 $file_record->sortorder = 0;
788 }
789 } else {
790 $file_record->sortorder = 0;
791 }
792
172dd12c 793 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
794 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
795 // path must start and end with '/'
145a0a31 796 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 797 }
798
799 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
800 if ($file_record->filename === '') {
e1dcb950 801 // filename must not be empty
145a0a31 802 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 803 }
804
805 $now = time();
260c4a5b
PS
806 if (isset($file_record->timecreated)) {
807 if (!is_number($file_record->timecreated)) {
808 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
809 }
810 if ($file_record->timecreated < 0) {
811 //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)
812 $file_record->timecreated = 0;
813 }
814 } else {
815 $file_record->timecreated = $now;
816 }
817
818 if (isset($file_record->timemodified)) {
819 if (!is_number($file_record->timemodified)) {
820 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
821 }
822 if ($file_record->timemodified < 0) {
823 //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)
824 $file_record->timemodified = 0;
825 }
826 } else {
827 $file_record->timemodified = $now;
828 }
172dd12c 829
ac6f1a82 830 $newrecord = new stdClass();
172dd12c 831
832 $newrecord->contextid = $file_record->contextid;
64f93798 833 $newrecord->component = $file_record->component;
172dd12c 834 $newrecord->filearea = $file_record->filearea;
835 $newrecord->itemid = $file_record->itemid;
836 $newrecord->filepath = $file_record->filepath;
837 $newrecord->filename = $file_record->filename;
838
260c4a5b
PS
839 $newrecord->timecreated = $file_record->timecreated;
840 $newrecord->timemodified = $file_record->timemodified;
172dd12c 841 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
842 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
4fb2306e
PS
843 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
844 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
845 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
f79321f1 846 $newrecord->sortorder = $file_record->sortorder;
172dd12c 847
b48f3e06 848 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
172dd12c 849
64f93798 850 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 851
852 try {
853 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 854 } catch (dml_exception $e) {
172dd12c 855 if ($newfile) {
ead14290 856 $this->deleted_file_cleanup($newrecord->contenthash);
172dd12c 857 }
64f93798 858 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 859 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 860 }
861
64f93798 862 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 863
693ef3a8 864 return $this->get_file_instance($newrecord);
172dd12c 865 }
866
867 /**
bf9ffe27
PS
868 * Add new local file.
869 *
d2b7803e 870 * @param stdClass|array $file_record object or array describing file
172dd12c 871 * @param string $content content of file
d2b7803e 872 * @return stored_file
172dd12c 873 */
874 public function create_file_from_string($file_record, $content) {
4fb2306e 875 global $DB;
172dd12c 876
ec8b711f 877 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 878 $file_record = (object)$file_record; // we support arrays too
879
880 // validate all parameters, we do not want any rubbish stored in database, right?
881 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 882 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 883 }
884
aff24313
PS
885 $file_record->component = clean_param($file_record->component, PARAM_COMPONENT);
886 if (empty($file_record->component)) {
64f93798
PS
887 throw new file_exception('storedfileproblem', 'Invalid component');
888 }
889
65e9eac0 890 $file_record->filearea = clean_param($file_record->filearea, PARAM_AREA);
aff24313 891 if (empty($file_record->filearea)) {
145a0a31 892 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 893 }
894
895 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 896 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 897 }
898
f79321f1
DC
899 if (!empty($file_record->sortorder)) {
900 if (!is_number($file_record->sortorder) or $file_record->sortorder < 0) {
901 $file_record->sortorder = 0;
902 }
903 } else {
904 $file_record->sortorder = 0;
905 }
906
172dd12c 907 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
908 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
909 // path must start and end with '/'
145a0a31 910 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 911 }
912
913 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
914 if ($file_record->filename === '') {
915 // path must start and end with '/'
145a0a31 916 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 917 }
918
919 $now = time();
260c4a5b
PS
920 if (isset($file_record->timecreated)) {
921 if (!is_number($file_record->timecreated)) {
922 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
923 }
924 if ($file_record->timecreated < 0) {
925 //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)
926 $file_record->timecreated = 0;
927 }
928 } else {
929 $file_record->timecreated = $now;
930 }
931
932 if (isset($file_record->timemodified)) {
933 if (!is_number($file_record->timemodified)) {
934 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
935 }
936 if ($file_record->timemodified < 0) {
937 //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)
938 $file_record->timemodified = 0;
939 }
940 } else {
941 $file_record->timemodified = $now;
942 }
172dd12c 943
ac6f1a82 944 $newrecord = new stdClass();
172dd12c 945
946 $newrecord->contextid = $file_record->contextid;
64f93798 947 $newrecord->component = $file_record->component;
172dd12c 948 $newrecord->filearea = $file_record->filearea;
949 $newrecord->itemid = $file_record->itemid;
950 $newrecord->filepath = $file_record->filepath;
951 $newrecord->filename = $file_record->filename;
952
260c4a5b
PS
953 $newrecord->timecreated = $file_record->timecreated;
954 $newrecord->timemodified = $file_record->timemodified;
172dd12c 955 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
956 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
4fb2306e
PS
957 $newrecord->source = empty($file_record->source) ? null : $file_record->source;
958 $newrecord->author = empty($file_record->author) ? null : $file_record->author;
959 $newrecord->license = empty($file_record->license) ? null : $file_record->license;
f79321f1 960 $newrecord->sortorder = $file_record->sortorder;
1dce6261 961
b48f3e06 962 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
172dd12c 963
64f93798 964 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 965
966 try {
967 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 968 } catch (dml_exception $e) {
172dd12c 969 if ($newfile) {
ead14290 970 $this->deleted_file_cleanup($newrecord->contenthash);
172dd12c 971 }
64f93798 972 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 973 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 974 }
975
64f93798 976 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 977
693ef3a8 978 return $this->get_file_instance($newrecord);
172dd12c 979 }
980
797f19e8 981 /**
982 * Creates new image file from existing.
bf9ffe27 983 *
d2b7803e
DC
984 * @param stdClass|array $file_record object or array describing new file
985 * @param int|stored_file $fid file id or stored file object
797f19e8 986 * @param int $newwidth in pixels
987 * @param int $newheight in pixels
d2b7803e 988 * @param bool $keepaspectratio whether or not keep aspect ratio
bf9ffe27 989 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
d2b7803e 990 * @return stored_file
797f19e8 991 */
bf9ffe27 992 public function convert_image($file_record, $fid, $newwidth = NULL, $newheight = NULL, $keepaspectratio = true, $quality = NULL) {
6b2f2184
AD
993 if (!function_exists('imagecreatefromstring')) {
994 //Most likely the GD php extension isn't installed
995 //image conversion cannot succeed
996 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
997 }
998
797f19e8 999 if ($fid instanceof stored_file) {
1000 $fid = $fid->get_id();
1001 }
1002
1003 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
1004
1005 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
1006 throw new file_exception('storedfileproblem', 'File does not exist');
1007 }
1008
1009 if (!$imageinfo = $file->get_imageinfo()) {
1010 throw new file_exception('storedfileproblem', 'File is not an image');
1011 }
1012
1013 if (!isset($file_record['filename'])) {
0ea7fc25 1014 $file_record['filename'] = $file->get_filename();
797f19e8 1015 }
1016
1017 if (!isset($file_record['mimetype'])) {
1018 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
1019 }
1020
1021 $width = $imageinfo['width'];
1022 $height = $imageinfo['height'];
1023 $mimetype = $imageinfo['mimetype'];
1024
1025 if ($keepaspectratio) {
1026 if (0 >= $newwidth and 0 >= $newheight) {
1027 // no sizes specified
1028 $newwidth = $width;
1029 $newheight = $height;
1030
1031 } else if (0 < $newwidth and 0 < $newheight) {
1032 $xheight = ($newwidth*($height/$width));
1033 if ($xheight < $newheight) {
1034 $newheight = (int)$xheight;
1035 } else {
1036 $newwidth = (int)($newheight*($width/$height));
1037 }
1038
1039 } else if (0 < $newwidth) {
1040 $newheight = (int)($newwidth*($height/$width));
1041
1042 } else { //0 < $newheight
1043 $newwidth = (int)($newheight*($width/$height));
1044 }
1045
1046 } else {
1047 if (0 >= $newwidth) {
1048 $newwidth = $width;
1049 }
1050 if (0 >= $newheight) {
1051 $newheight = $height;
1052 }
1053 }
1054
1055 $img = imagecreatefromstring($file->get_content());
1056 if ($height != $newheight or $width != $newwidth) {
1057 $newimg = imagecreatetruecolor($newwidth, $newheight);
1058 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1059 // weird
1060 throw new file_exception('storedfileproblem', 'Can not resize image');
1061 }
1062 imagedestroy($img);
1063 $img = $newimg;
1064 }
1065
1066 ob_start();
1067 switch ($file_record['mimetype']) {
1068 case 'image/gif':
1069 imagegif($img);
1070 break;
1071
1072 case 'image/jpeg':
1073 if (is_null($quality)) {
1074 imagejpeg($img);
1075 } else {
1076 imagejpeg($img, NULL, $quality);
1077 }
1078 break;
1079
1080 case 'image/png':
8bd49ec0 1081 $quality = (int)$quality;
797f19e8 1082 imagepng($img, NULL, $quality, NULL);
1083 break;
1084
1085 default:
1086 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1087 }
1088
1089 $content = ob_get_contents();
1090 ob_end_clean();
1091 imagedestroy($img);
1092
1093 if (!$content) {
1094 throw new file_exception('storedfileproblem', 'Can not convert image');
1095 }
1096
1097 return $this->create_file_from_string($file_record, $content);
1098 }
1099
172dd12c 1100 /**
bf9ffe27
PS
1101 * Add file content to sha1 pool.
1102 *
172dd12c 1103 * @param string $pathname path to file
bf9ffe27
PS
1104 * @param string $contenthash sha1 hash of content if known (performance only)
1105 * @return array (contenthash, filesize, newfile)
172dd12c 1106 */
bf9ffe27 1107 public function add_file_to_pool($pathname, $contenthash = NULL) {
172dd12c 1108 if (!is_readable($pathname)) {
d610cb89 1109 throw new file_exception('storedfilecannotread', '', $pathname);
172dd12c 1110 }
1111
1112 if (is_null($contenthash)) {
1113 $contenthash = sha1_file($pathname);
1114 }
1115
1116 $filesize = filesize($pathname);
1117
1118 $hashpath = $this->path_from_hash($contenthash);
1119 $hashfile = "$hashpath/$contenthash";
1120
1121 if (file_exists($hashfile)) {
1122 if (filesize($hashfile) !== $filesize) {
1123 throw new file_pool_content_exception($contenthash);
1124 }
1125 $newfile = false;
1126
1127 } else {
1aa01caf 1128 if (!is_dir($hashpath)) {
1129 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1130 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1131 }
172dd12c 1132 }
1133 $newfile = true;
1134
6c0e2d08 1135 if (!copy($pathname, $hashfile)) {
d610cb89 1136 throw new file_exception('storedfilecannotread', '', $pathname);
172dd12c 1137 }
172dd12c 1138
1139 if (filesize($hashfile) !== $filesize) {
1140 @unlink($hashfile);
1141 throw new file_pool_content_exception($contenthash);
1142 }
1aa01caf 1143 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1144 }
1145
1146
1147 return array($contenthash, $filesize, $newfile);
1148 }
1149
1150 /**
bf9ffe27
PS
1151 * Add string content to sha1 pool.
1152 *
172dd12c 1153 * @param string $content file content - binary string
bf9ffe27 1154 * @return array (contenthash, filesize, newfile)
172dd12c 1155 */
b48f3e06 1156 public function add_string_to_pool($content) {
172dd12c 1157 $contenthash = sha1($content);
1158 $filesize = strlen($content); // binary length
1159
1160 $hashpath = $this->path_from_hash($contenthash);
1161 $hashfile = "$hashpath/$contenthash";
1162
1163
1164 if (file_exists($hashfile)) {
1165 if (filesize($hashfile) !== $filesize) {
1166 throw new file_pool_content_exception($contenthash);
1167 }
1168 $newfile = false;
1169
1170 } else {
1aa01caf 1171 if (!is_dir($hashpath)) {
1172 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1173 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1174 }
172dd12c 1175 }
1176 $newfile = true;
1177
6c0e2d08 1178 file_put_contents($hashfile, $content);
172dd12c 1179
1180 if (filesize($hashfile) !== $filesize) {
1181 @unlink($hashfile);
1182 throw new file_pool_content_exception($contenthash);
1183 }
1aa01caf 1184 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1185 }
1186
1187 return array($contenthash, $filesize, $newfile);
1188 }
1189
1190 /**
bf9ffe27 1191 * Return path to file with given hash.
172dd12c 1192 *
17d9269f 1193 * NOTE: must not be public, files in pool must not be modified
172dd12c 1194 *
d2b7803e 1195 * @param string $contenthash content hash
172dd12c 1196 * @return string expected file location
1197 */
17d9269f 1198 protected function path_from_hash($contenthash) {
172dd12c 1199 $l1 = $contenthash[0].$contenthash[1];
1200 $l2 = $contenthash[2].$contenthash[3];
d0b6f92a 1201 return "$this->filedir/$l1/$l2";
172dd12c 1202 }
1203
1aa01caf 1204 /**
bf9ffe27 1205 * Return path to file with given hash.
1aa01caf 1206 *
1207 * NOTE: must not be public, files in pool must not be modified
1208 *
d2b7803e 1209 * @param string $contenthash content hash
1aa01caf 1210 * @return string expected file location
1211 */
1212 protected function trash_path_from_hash($contenthash) {
1213 $l1 = $contenthash[0].$contenthash[1];
1214 $l2 = $contenthash[2].$contenthash[3];
d0b6f92a 1215 return "$this->trashdir/$l1/$l2";
1aa01caf 1216 }
1217
1218 /**
bf9ffe27
PS
1219 * Tries to recover missing content of file from trash.
1220 *
d2b7803e 1221 * @param stored_file $file stored_file instance
1aa01caf 1222 * @return bool success
1223 */
1224 public function try_content_recovery($file) {
1225 $contenthash = $file->get_contenthash();
1226 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1227 if (!is_readable($trashfile)) {
1228 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1229 return false;
1230 }
1231 // nice, at least alternative trash file in trash root exists
1232 $trashfile = $this->trashdir.'/'.$contenthash;
1233 }
1234 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1235 //weird, better fail early
1236 return false;
1237 }
1238 $contentdir = $this->path_from_hash($contenthash);
1239 $contentfile = $contentdir.'/'.$contenthash;
1240 if (file_exists($contentfile)) {
1241 //strange, no need to recover anything
1242 return true;
1243 }
1244 if (!is_dir($contentdir)) {
1245 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1246 return false;
1247 }
1248 }
1249 return rename($trashfile, $contentfile);
1250 }
1251
172dd12c 1252 /**
bf9ffe27
PS
1253 * Marks pool file as candidate for deleting.
1254 *
1255 * DO NOT call directly - reserved for core!!
1256 *
172dd12c 1257 * @param string $contenthash
1258 */
1aa01caf 1259 public function deleted_file_cleanup($contenthash) {
172dd12c 1260 global $DB;
1261
1aa01caf 1262 //Note: this section is critical - in theory file could be reused at the same
1263 // time, if this happens we can still recover the file from trash
1264 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1265 // file content is still used
1266 return;
1267 }
1268 //move content file to trash
1269 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1270 if (!file_exists($contentfile)) {
1271 //weird, but no problem
172dd12c 1272 return;
1273 }
1aa01caf 1274 $trashpath = $this->trash_path_from_hash($contenthash);
1275 $trashfile = $trashpath.'/'.$contenthash;
1276 if (file_exists($trashfile)) {
1277 // we already have this content in trash, no need to move it there
1278 unlink($contentfile);
1279 return;
1280 }
1281 if (!is_dir($trashpath)) {
1282 mkdir($trashpath, $this->dirpermissions, true);
1283 }
1284 rename($contentfile, $trashfile);
1285 chmod($trashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1286 }
1287
1288 /**
1289 * Cron cleanup job.
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