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