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