MDL-14589 fixed error message if url malformed
[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
ee03a651 214 /**
215 * Returns all files and otionally directories
216 * @param int $contextid
217 * @param string $filearea
218 * @param int $itemid
219 * @param int $filepath directory path
220 * @param bool $recursive include all subdirectories
46fcbcf4 221 * @param bool $includedirs include files and directories
ee03a651 222 * @param string $sort
cd5be217 223 * @return array of stored_files indexed by pathanmehash
ee03a651 224 */
46fcbcf4 225 public function get_directory_files($contextid, $filearea, $itemid, $filepath, $recursive=false, $includedirs=true, $sort="filepath, filename") {
ee03a651 226 global $DB;
227
228 if (!$directory = $this->get_file($contextid, $filearea, $itemid, $filepath, '.')) {
229 return array();
230 }
231
232 if ($recursive) {
233
46fcbcf4 234 $dirs = $includedirs ? "" : "AND filename <> '.'";
ee03a651 235 $length = textlib_get_instance()->strlen($filepath);
236
237 $sql = "SELECT *
238 FROM {files}
239 WHERE contextid = :contextid AND filearea = :filearea AND itemid = :itemid
655bbf51 240 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 241 AND id <> :dirid
242 $dirs
243 ORDER BY $sort";
244 $params = array('contextid'=>$contextid, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
245
246 $files = array();
247 $dirs = array();
248 $file_records = $DB->get_records_sql($sql, $params);
249 foreach ($file_records as $file_record) {
250 if ($file_record->filename == '.') {
cd5be217 251 $dirs[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 252 } else {
cd5be217 253 $files[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 254 }
255 }
256 $result = array_merge($dirs, $files);
257
258 } else {
259 $result = array();
260 $params = array('contextid'=>$contextid, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
261
262 $length = textlib_get_instance()->strlen($filepath);
263
46fcbcf4 264 if ($includedirs) {
ee03a651 265 $sql = "SELECT *
266 FROM {files}
267 WHERE contextid = :contextid AND filearea = :filearea
268 AND itemid = :itemid AND filename = '.'
655bbf51 269 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 270 AND id <> :dirid
271 ORDER BY $sort";
272 $reqlevel = substr_count($filepath, '/') + 1;
273 $file_records = $DB->get_records_sql($sql, $params);
274 foreach ($file_records as $file_record) {
275 if (substr_count($file_record->filepath, '/') !== $reqlevel) {
276 continue;
277 }
cd5be217 278 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 279 }
280 }
281
282 $sql = "SELECT *
283 FROM {files}
284 WHERE contextid = :contextid AND filearea = :filearea AND itemid = :itemid
285 AND filepath = :filepath AND filename <> '.'
286 ORDER BY $sort";
287
288 $file_records = $DB->get_records_sql($sql, $params);
289 foreach ($file_records as $file_record) {
cd5be217 290 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 291 }
292 }
293
294 return $result;
295 }
296
172dd12c 297 /**
298 * Delete all area files (optionally limited by itemid)
299 * @param int $contextid
6311eb61 300 * @param string $filearea (all areas in context if not specified)
172dd12c 301 * @param int $itemid (all files if not specified)
302 * @return success
303 */
6311eb61 304 public function delete_area_files($contextid, $filearea=false, $itemid=false) {
172dd12c 305 global $DB;
306
6311eb61 307 $conditions = array('contextid'=>$contextid);
308 if ($filearea !== false) {
309 $conditions['filearea'] = $filearea;
310 }
172dd12c 311 if ($itemid !== false) {
312 $conditions['itemid'] = $itemid;
313 }
314
315 $success = true;
316
317 $file_records = $DB->get_records('files', $conditions);
318 foreach ($file_records as $file_record) {
319 $stored_file = new stored_file($this, $file_record);
320 $success = $stored_file->delete() && $success;
321 }
322
323 return $success;
324 }
325
326 /**
25aebf09 327 * Recursively creates directory
172dd12c 328 * @param int $contextid
329 * @param string $filearea
330 * @param int $itemid
331 * @param string $filepath
332 * @param string $filename
333 * @return bool success
334 */
335 public function create_directory($contextid, $filearea, $itemid, $filepath, $userid=null) {
336 global $DB;
337
338 // validate all parameters, we do not want any rubbish stored in database, right?
339 if (!is_number($contextid) or $contextid < 1) {
145a0a31 340 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 341 }
342
6c0e2d08 343 $filearea = clean_param($filearea, PARAM_ALPHAEXT);
172dd12c 344 if ($filearea === '') {
145a0a31 345 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 346 }
347
348 if (!is_number($itemid) or $itemid < 0) {
145a0a31 349 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 350 }
351
352 $filepath = clean_param($filepath, PARAM_PATH);
353 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
354 // path must start and end with '/'
145a0a31 355 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 356 }
357
358 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, '.');
359
360 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
361 return $dir_info;
362 }
363
364 static $contenthash = null;
365 if (!$contenthash) {
b48f3e06 366 $this->add_string_to_pool('');
172dd12c 367 $contenthash = sha1('');
368 }
369
370 $now = time();
371
372 $dir_record = new object();
373 $dir_record->contextid = $contextid;
374 $dir_record->filearea = $filearea;
375 $dir_record->itemid = $itemid;
376 $dir_record->filepath = $filepath;
377 $dir_record->filename = '.';
378 $dir_record->contenthash = $contenthash;
379 $dir_record->filesize = 0;
380
381 $dir_record->timecreated = $now;
382 $dir_record->timemodified = $now;
383 $dir_record->mimetype = null;
384 $dir_record->userid = $userid;
385
386 $dir_record->pathnamehash = $pathnamehash;
387
388 $DB->insert_record('files', $dir_record);
389 $dir_info = $this->get_file_by_hash($pathnamehash);
390
391 if ($filepath !== '/') {
392 //recurse to parent dirs
393 $filepath = trim($filepath, '/');
394 $filepath = explode('/', $filepath);
395 array_pop($filepath);
396 $filepath = implode('/', $filepath);
397 $filepath = ($filepath === '') ? '/' : "/$filepath/";
398 $this->create_directory($contextid, $filearea, $itemid, $filepath, $userid);
399 }
400
401 return $dir_info;
402 }
403
404 /**
405 * Add new local file based on existing local file
406 * @param mixed $file_record object or array describing changes
407 * @param int $fid id of existing local file
408 * @return object stored_file instance
409 */
3501d96b 410 public function create_file_from_storedfile($file_record, $fid) {
172dd12c 411 global $DB;
412
8eb1e0a1 413 if ($fid instanceof stored_file) {
414 $fid = $fid->get_id();
415 }
416
ec8b711f 417 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
418
172dd12c 419 unset($file_record['id']);
420 unset($file_record['filesize']);
421 unset($file_record['contenthash']);
8eb1e0a1 422 unset($file_record['pathnamehash']);
172dd12c 423
424 $now = time();
425
ebcac6c6 426 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
145a0a31 427 throw new file_exception('storedfileproblem', 'File does not exist');
172dd12c 428 }
429
430 unset($newrecord->id);
431
432 foreach ($file_record as $key=>$value) {
433 // validate all parameters, we do not want any rubbish stored in database, right?
434 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
145a0a31 435 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 436 }
437
438 if ($key == 'filearea') {
6c0e2d08 439 $value = clean_param($value, PARAM_ALPHAEXT);
172dd12c 440 if ($value === '') {
145a0a31 441 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 442 }
443 }
444
445 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
145a0a31 446 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 447 }
448
449
450 if ($key == 'filepath') {
451 $value = clean_param($value, PARAM_PATH);
00c32c54 452 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
172dd12c 453 // path must start and end with '/'
145a0a31 454 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 455 }
456 }
457
458 if ($key == 'filename') {
459 $value = clean_param($value, PARAM_FILE);
460 if ($value === '') {
461 // path must start and end with '/'
145a0a31 462 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 463 }
464 }
465
466 $newrecord->$key = $value;
467 }
468
469 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
470
cd5be217 471 if ($newrecord->filename === '.') {
472 // special case - only this function supports directories ;-)
473 $directory = $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
474 // update the existing directory with the new data
475 $newrecord->id = $directory->get_id();
b8ac7ece 476 $DB->update_record('files', $newrecord);
cd5be217 477 return new stored_file($this, $newrecord);
478 }
479
172dd12c 480 try {
481 $newrecord->id = $DB->insert_record('files', $newrecord);
482 } catch (database_exception $e) {
483 $newrecord->id = false;
484 }
485
486 if (!$newrecord->id) {
487 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
cd5be217 488 $newrecord->filepath, $newrecord->filename);
172dd12c 489 }
490
491 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
492
493 return new stored_file($this, $newrecord);
494 }
495
6e73ac42 496 /**
497 * Add new local file
498 * @param mixed $file_record object or array describing file
499 * @param string $path path to file or content of file
500 * @param array $options @see download_file_content() options
501 * @return object stored_file instance
502 */
503 public function create_file_from_url($file_record, $url, $options=null) {
ec8b711f 504
505 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
6e73ac42 506 $file_record = (object)$file_record; // we support arrays too
507
508 $headers = isset($options['headers']) ? $options['headers'] : null;
509 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
510 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
511 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
512 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
513 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
514
515 // TODO: it might be better to add a new option to file file content to temp file,
516 // the problem here is that the size of file is limited by available memory
517
518 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify);
519
520 if (!isset($file_record->filename)) {
521 $parts = explode('/', $url);
522 $filename = array_pop($parts);
523 $file_record->filename = clean_param($filename, PARAM_FILE);
524 }
525
526 return $this->create_file_from_string($file_record, $content);
527 }
528
172dd12c 529 /**
530 * Add new local file
531 * @param mixed $file_record object or array describing file
532 * @param string $path path to file or content of file
533 * @return object stored_file instance
534 */
535 public function create_file_from_pathname($file_record, $pathname) {
536 global $DB;
537
ec8b711f 538 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 539 $file_record = (object)$file_record; // we support arrays too
540
541 // validate all parameters, we do not want any rubbish stored in database, right?
542 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 543 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 544 }
545
6c0e2d08 546 $file_record->filearea = clean_param($file_record->filearea, PARAM_ALPHAEXT);
172dd12c 547 if ($file_record->filearea === '') {
145a0a31 548 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 549 }
550
551 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 552 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 553 }
554
555 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
556 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
557 // path must start and end with '/'
145a0a31 558 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 559 }
560
561 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
562 if ($file_record->filename === '') {
e1dcb950 563 // filename must not be empty
145a0a31 564 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 565 }
566
567 $now = time();
568
569 $newrecord = new object();
570
571 $newrecord->contextid = $file_record->contextid;
572 $newrecord->filearea = $file_record->filearea;
573 $newrecord->itemid = $file_record->itemid;
574 $newrecord->filepath = $file_record->filepath;
575 $newrecord->filename = $file_record->filename;
576
577 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
578 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
579 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
580 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
581
b48f3e06 582 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
172dd12c 583
584 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
585
586 try {
587 $newrecord->id = $DB->insert_record('files', $newrecord);
588 } catch (database_exception $e) {
589 $newrecord->id = false;
590 }
591
592 if (!$newrecord->id) {
593 if ($newfile) {
594 $this->mark_delete_candidate($newrecord->contenthash);
595 }
596 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
597 $newrecord->filepath, $newrecord->filename);
598 }
599
600 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
601
602 return new stored_file($this, $newrecord);
603 }
604
605 /**
606 * Add new local file
607 * @param mixed $file_record object or array describing file
608 * @param string $content content of file
609 * @return object stored_file instance
610 */
611 public function create_file_from_string($file_record, $content) {
612 global $DB;
613
ec8b711f 614 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 615 $file_record = (object)$file_record; // we support arrays too
616
617 // validate all parameters, we do not want any rubbish stored in database, right?
618 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 619 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 620 }
621
6c0e2d08 622 $file_record->filearea = clean_param($file_record->filearea, PARAM_ALPHAEXT);
172dd12c 623 if ($file_record->filearea === '') {
145a0a31 624 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 625 }
626
627 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 628 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 629 }
630
631 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
632 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
633 // path must start and end with '/'
145a0a31 634 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 635 }
636
637 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
638 if ($file_record->filename === '') {
639 // path must start and end with '/'
145a0a31 640 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 641 }
642
643 $now = time();
644
645 $newrecord = new object();
646
647 $newrecord->contextid = $file_record->contextid;
648 $newrecord->filearea = $file_record->filearea;
649 $newrecord->itemid = $file_record->itemid;
650 $newrecord->filepath = $file_record->filepath;
651 $newrecord->filename = $file_record->filename;
652
653 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
654 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
655 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
656 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
657
b48f3e06 658 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
172dd12c 659
660 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
661
662 try {
663 $newrecord->id = $DB->insert_record('files', $newrecord);
664 } catch (database_exception $e) {
665 $newrecord->id = false;
666 }
667
668 if (!$newrecord->id) {
669 if ($newfile) {
670 $this->mark_delete_candidate($newrecord->contenthash);
671 }
672 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
673 $newrecord->filepath, $newrecord->filename);
674 }
675
676 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
677
678 return new stored_file($this, $newrecord);
679 }
680
797f19e8 681 /**
682 * Creates new image file from existing.
683 * @param mixed $file_record object or array describing new file
684 * @param mixed file id or stored file object
685 * @param int $newwidth in pixels
686 * @param int $newheight in pixels
687 * @param bool $keepaspectratio
688 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no comppression) for png
689 * @return object stored_file instance
690 */
691 public function convert_image($file_record, $fid, $newwidth=null, $newheight=null, $keepaspectratio=true, $quality=null) {
692 global $DB;
693
694 if ($fid instanceof stored_file) {
695 $fid = $fid->get_id();
696 }
697
698 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
699
700 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
701 throw new file_exception('storedfileproblem', 'File does not exist');
702 }
703
704 if (!$imageinfo = $file->get_imageinfo()) {
705 throw new file_exception('storedfileproblem', 'File is not an image');
706 }
707
708 if (!isset($file_record['filename'])) {
709 $file_record['filename'] == $file->get_filename();
710 }
711
712 if (!isset($file_record['mimetype'])) {
713 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
714 }
715
716 $width = $imageinfo['width'];
717 $height = $imageinfo['height'];
718 $mimetype = $imageinfo['mimetype'];
719
720 if ($keepaspectratio) {
721 if (0 >= $newwidth and 0 >= $newheight) {
722 // no sizes specified
723 $newwidth = $width;
724 $newheight = $height;
725
726 } else if (0 < $newwidth and 0 < $newheight) {
727 $xheight = ($newwidth*($height/$width));
728 if ($xheight < $newheight) {
729 $newheight = (int)$xheight;
730 } else {
731 $newwidth = (int)($newheight*($width/$height));
732 }
733
734 } else if (0 < $newwidth) {
735 $newheight = (int)($newwidth*($height/$width));
736
737 } else { //0 < $newheight
738 $newwidth = (int)($newheight*($width/$height));
739 }
740
741 } else {
742 if (0 >= $newwidth) {
743 $newwidth = $width;
744 }
745 if (0 >= $newheight) {
746 $newheight = $height;
747 }
748 }
749
750 $img = imagecreatefromstring($file->get_content());
751 if ($height != $newheight or $width != $newwidth) {
752 $newimg = imagecreatetruecolor($newwidth, $newheight);
753 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
754 // weird
755 throw new file_exception('storedfileproblem', 'Can not resize image');
756 }
757 imagedestroy($img);
758 $img = $newimg;
759 }
760
761 ob_start();
762 switch ($file_record['mimetype']) {
763 case 'image/gif':
764 imagegif($img);
765 break;
766
767 case 'image/jpeg':
768 if (is_null($quality)) {
769 imagejpeg($img);
770 } else {
771 imagejpeg($img, NULL, $quality);
772 }
773 break;
774
775 case 'image/png':
8bd49ec0 776 $quality = (int)$quality;
797f19e8 777 imagepng($img, NULL, $quality, NULL);
778 break;
779
780 default:
781 throw new file_exception('storedfileproblem', 'Unsupported mime type');
782 }
783
784 $content = ob_get_contents();
785 ob_end_clean();
786 imagedestroy($img);
787
788 if (!$content) {
789 throw new file_exception('storedfileproblem', 'Can not convert image');
790 }
791
792 return $this->create_file_from_string($file_record, $content);
793 }
794
172dd12c 795 /**
796 * Add file content to sha1 pool
797 * @param string $pathname path to file
798 * @param string sha1 hash of content if known (performance only)
799 * @return array(contenthash, filesize, newfile)
800 */
b48f3e06 801 public function add_file_to_pool($pathname, $contenthash=null) {
172dd12c 802 if (!is_readable($pathname)) {
145a0a31 803 throw new file_exception('storedfilecannotread');
172dd12c 804 }
805
806 if (is_null($contenthash)) {
807 $contenthash = sha1_file($pathname);
808 }
809
810 $filesize = filesize($pathname);
811
812 $hashpath = $this->path_from_hash($contenthash);
813 $hashfile = "$hashpath/$contenthash";
814
815 if (file_exists($hashfile)) {
816 if (filesize($hashfile) !== $filesize) {
817 throw new file_pool_content_exception($contenthash);
818 }
819 $newfile = false;
820
821 } else {
1aa01caf 822 if (!is_dir($hashpath)) {
823 if (!mkdir($hashpath, $this->dirpermissions, true)) {
824 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
825 }
172dd12c 826 }
827 $newfile = true;
828
6c0e2d08 829 if (!copy($pathname, $hashfile)) {
145a0a31 830 throw new file_exception('storedfilecannotread');
172dd12c 831 }
172dd12c 832
833 if (filesize($hashfile) !== $filesize) {
834 @unlink($hashfile);
835 throw new file_pool_content_exception($contenthash);
836 }
1aa01caf 837 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 838 }
839
840
841 return array($contenthash, $filesize, $newfile);
842 }
843
844 /**
845 * Add string content to sha1 pool
846 * @param string $content file content - binary string
847 * @return array(contenthash, filesize, newfile)
848 */
b48f3e06 849 public function add_string_to_pool($content) {
172dd12c 850 $contenthash = sha1($content);
851 $filesize = strlen($content); // binary length
852
853 $hashpath = $this->path_from_hash($contenthash);
854 $hashfile = "$hashpath/$contenthash";
855
856
857 if (file_exists($hashfile)) {
858 if (filesize($hashfile) !== $filesize) {
859 throw new file_pool_content_exception($contenthash);
860 }
861 $newfile = false;
862
863 } else {
1aa01caf 864 if (!is_dir($hashpath)) {
865 if (!mkdir($hashpath, $this->dirpermissions, true)) {
866 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
867 }
172dd12c 868 }
869 $newfile = true;
870
6c0e2d08 871 file_put_contents($hashfile, $content);
172dd12c 872
873 if (filesize($hashfile) !== $filesize) {
874 @unlink($hashfile);
875 throw new file_pool_content_exception($contenthash);
876 }
1aa01caf 877 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 878 }
879
880 return array($contenthash, $filesize, $newfile);
881 }
882
883 /**
884 * Return path to file with given hash
885 *
17d9269f 886 * NOTE: must not be public, files in pool must not be modified
172dd12c 887 *
888 * @param string $contenthash
889 * @return string expected file location
890 */
17d9269f 891 protected function path_from_hash($contenthash) {
172dd12c 892 $l1 = $contenthash[0].$contenthash[1];
893 $l2 = $contenthash[2].$contenthash[3];
894 $l3 = $contenthash[4].$contenthash[5];
895 return "$this->filedir/$l1/$l2/$l3";
896 }
897
1aa01caf 898 /**
899 * Return path to file with given hash
900 *
901 * NOTE: must not be public, files in pool must not be modified
902 *
903 * @param string $contenthash
904 * @return string expected file location
905 */
906 protected function trash_path_from_hash($contenthash) {
907 $l1 = $contenthash[0].$contenthash[1];
908 $l2 = $contenthash[2].$contenthash[3];
909 $l3 = $contenthash[4].$contenthash[5];
910 return "$this->trashdir/$l1/$l2/$l3";
911 }
912
913 /**
914 * Tries to recover missing content of file from trash
915 * @param object $file_record
916 * @return bool success
917 */
918 public function try_content_recovery($file) {
919 $contenthash = $file->get_contenthash();
920 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
921 if (!is_readable($trashfile)) {
922 if (!is_readable($this->trashdir.'/'.$contenthash)) {
923 return false;
924 }
925 // nice, at least alternative trash file in trash root exists
926 $trashfile = $this->trashdir.'/'.$contenthash;
927 }
928 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
929 //weird, better fail early
930 return false;
931 }
932 $contentdir = $this->path_from_hash($contenthash);
933 $contentfile = $contentdir.'/'.$contenthash;
934 if (file_exists($contentfile)) {
935 //strange, no need to recover anything
936 return true;
937 }
938 if (!is_dir($contentdir)) {
939 if (!mkdir($contentdir, $this->dirpermissions, true)) {
940 return false;
941 }
942 }
943 return rename($trashfile, $contentfile);
944 }
945
172dd12c 946 /**
947 * Marks pool file as candidate for deleting
1aa01caf 948 * DO NOT call directly - reserved for core!
172dd12c 949 * @param string $contenthash
1aa01caf 950 * @return void
172dd12c 951 */
1aa01caf 952 public function deleted_file_cleanup($contenthash) {
172dd12c 953 global $DB;
954
1aa01caf 955 //Note: this section is critical - in theory file could be reused at the same
956 // time, if this happens we can still recover the file from trash
957 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
958 // file content is still used
959 return;
960 }
961 //move content file to trash
962 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
963 if (!file_exists($contentfile)) {
964 //weird, but no problem
172dd12c 965 return;
966 }
1aa01caf 967 $trashpath = $this->trash_path_from_hash($contenthash);
968 $trashfile = $trashpath.'/'.$contenthash;
969 if (file_exists($trashfile)) {
970 // we already have this content in trash, no need to move it there
971 unlink($contentfile);
972 return;
973 }
974 if (!is_dir($trashpath)) {
975 mkdir($trashpath, $this->dirpermissions, true);
976 }
977 rename($contentfile, $trashfile);
978 chmod($trashfile, $this->filepermissions); // fix permissions if needed
172dd12c 979 }
980
981 /**
982 * Cron cleanup job.
983 */
984 public function cron() {
1aa01caf 985 global $CFG;
986 // remove trash pool files once a day
987 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
988 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
989 require_once($CFG->libdir.'/filelib.php');
990 mtrace('Deleting trash files... ', '');
991 fulldelete($this->trashdir);
992 set_config('fileslastcleanup', time());
993 mtrace('done.');
172dd12c 994 }
995 }
996}