on demand release 2.5beta+
[moodle.git] / lib / filestorage / file_storage.php
CommitLineData
25aebf09 1<?php
25aebf09 2// This file is part of Moodle - http://moodle.org/
3//
4// Moodle is free software: you can redistribute it and/or modify
5// it under the terms of the GNU General Public License as published by
6// the Free Software Foundation, either version 3 of the License, or
7// (at your option) any later version.
8//
9// Moodle is distributed in the hope that it will be useful,
10// but WITHOUT ANY WARRANTY; without even the implied warranty of
11// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12// GNU General Public License for more details.
13//
14// You should have received a copy of the GNU General Public License
15// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
16
17
18/**
19 * Core file storage class definition.
20 *
d2b7803e
DC
21 * @package core_files
22 * @copyright 2008 Petr Skoda {@link http://skodak.org}
23 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25aebf09 24 */
172dd12c 25
64f93798
PS
26defined('MOODLE_INTERNAL') || die();
27
28require_once("$CFG->libdir/filestorage/stored_file.php");
172dd12c 29
25aebf09 30/**
31 * File storage class used for low level access to stored files.
bf9ffe27 32 *
25aebf09 33 * Only owner of file area may use this class to access own files,
34 * for example only code in mod/assignment/* may access assignment
bf9ffe27
PS
35 * attachments. When some other part of moodle needs to access
36 * files of modules it has to use file_browser class instead or there
37 * has to be some callback API.
38 *
d2b7803e
DC
39 * @package core_files
40 * @category files
bf9ffe27
PS
41 * @copyright 2008 Petr Skoda {@link http://skodak.org}
42 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
43 * @since Moodle 2.0
25aebf09 44 */
172dd12c 45class file_storage {
bf9ffe27 46 /** @var string Directory with file contents */
172dd12c 47 private $filedir;
bf9ffe27 48 /** @var string Contents of deleted files not needed any more */
1aa01caf 49 private $trashdir;
3a1055a5
PS
50 /** @var string tempdir */
51 private $tempdir;
bf9ffe27 52 /** @var int Permissions for new directories */
1aa01caf 53 private $dirpermissions;
bf9ffe27 54 /** @var int Permissions for new files */
1aa01caf 55 private $filepermissions;
bf9ffe27 56
172dd12c 57 /**
d2b7803e 58 * Constructor - do not use directly use {@link get_file_storage()} call instead.
bf9ffe27 59 *
172dd12c 60 * @param string $filedir full path to pool directory
bf9ffe27 61 * @param string $trashdir temporary storage of deleted area
3a1055a5 62 * @param string $tempdir temporary storage of various files
bf9ffe27
PS
63 * @param int $dirpermissions new directory permissions
64 * @param int $filepermissions new file permissions
172dd12c 65 */
3a1055a5 66 public function __construct($filedir, $trashdir, $tempdir, $dirpermissions, $filepermissions) {
1aa01caf 67 $this->filedir = $filedir;
68 $this->trashdir = $trashdir;
3a1055a5 69 $this->tempdir = $tempdir;
1aa01caf 70 $this->dirpermissions = $dirpermissions;
71 $this->filepermissions = $filepermissions;
172dd12c 72
73 // make sure the file pool directory exists
74 if (!is_dir($this->filedir)) {
1aa01caf 75 if (!mkdir($this->filedir, $this->dirpermissions, true)) {
145a0a31 76 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
172dd12c 77 }
78 // place warning file in file pool root
1aa01caf 79 if (!file_exists($this->filedir.'/warning.txt')) {
80 file_put_contents($this->filedir.'/warning.txt',
81 'This directory contains the content of uploaded files and is controlled by Moodle code. Do not manually move, change or rename any of the files and subdirectories here.');
82 }
83 }
84 // make sure the file pool directory exists
85 if (!is_dir($this->trashdir)) {
86 if (!mkdir($this->trashdir, $this->dirpermissions, true)) {
87 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
88 }
172dd12c 89 }
90 }
91
92 /**
bf9ffe27
PS
93 * Calculates sha1 hash of unique full path name information.
94 *
95 * This hash is a unique file identifier - it is used to improve
96 * performance and overcome db index size limits.
97 *
d2b7803e
DC
98 * @param int $contextid context ID
99 * @param string $component component
100 * @param string $filearea file area
101 * @param int $itemid item ID
102 * @param string $filepath file path
103 * @param string $filename file name
bf9ffe27 104 * @return string sha1 hash
172dd12c 105 */
64f93798
PS
106 public static function get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename) {
107 return sha1("/$contextid/$component/$filearea/$itemid".$filepath.$filename);
172dd12c 108 }
109
110 /**
111 * Does this file exist?
bf9ffe27 112 *
d2b7803e
DC
113 * @param int $contextid context ID
114 * @param string $component component
115 * @param string $filearea file area
116 * @param int $itemid item ID
117 * @param string $filepath file path
118 * @param string $filename file name
172dd12c 119 * @return bool
120 */
64f93798 121 public function file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename) {
172dd12c 122 $filepath = clean_param($filepath, PARAM_PATH);
123 $filename = clean_param($filename, PARAM_FILE);
124
125 if ($filename === '') {
126 $filename = '.';
127 }
128
64f93798 129 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
172dd12c 130 return $this->file_exists_by_hash($pathnamehash);
131 }
132
133 /**
d2b7803e 134 * Whether or not the file exist
bf9ffe27 135 *
d2b7803e 136 * @param string $pathnamehash path name hash
172dd12c 137 * @return bool
138 */
139 public function file_exists_by_hash($pathnamehash) {
140 global $DB;
141
142 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
143 }
144
693ef3a8
PS
145 /**
146 * Create instance of file class from database record.
147 *
04e3b007 148 * @param stdClass $filerecord record from the files table left join files_reference table
693ef3a8
PS
149 * @return stored_file instance of file abstraction class
150 */
67233725
DC
151 public function get_file_instance(stdClass $filerecord) {
152 $storedfile = new stored_file($this, $filerecord, $this->filedir);
153 return $storedfile;
693ef3a8
PS
154 }
155
c4d19c5a
DM
156 /**
157 * Returns an image file that represent the given stored file as a preview
158 *
159 * At the moment, only GIF, JPEG and PNG files are supported to have previews. In the
160 * future, the support for other mimetypes can be added, too (eg. generate an image
161 * preview of PDF, text documents etc).
162 *
163 * @param stored_file $file the file we want to preview
164 * @param string $mode preview mode, eg. 'thumb'
165 * @return stored_file|bool false if unable to create the preview, stored file otherwise
166 */
167 public function get_file_preview(stored_file $file, $mode) {
168
169 $context = context_system::instance();
170 $path = '/' . trim($mode, '/') . '/';
171 $preview = $this->get_file($context->id, 'core', 'preview', 0, $path, $file->get_contenthash());
172
173 if (!$preview) {
174 $preview = $this->create_file_preview($file, $mode);
175 if (!$preview) {
176 return false;
177 }
178 }
179
180 return $preview;
181 }
182
d7d69396
FM
183 /**
184 * Return an available file name.
185 *
186 * This will return the next available file name in the area, adding/incrementing a suffix
187 * of the file, ie: file.txt > file (1).txt > file (2).txt > etc...
188 *
189 * If the file name passed is available without modification, it is returned as is.
190 *
191 * @param int $contextid context ID.
192 * @param string $component component.
193 * @param string $filearea file area.
194 * @param int $itemid area item ID.
195 * @param string $filepath the file path.
196 * @param string $filename the file name.
197 * @return string available file name.
198 * @throws coding_exception if the file name is invalid.
199 * @since 2.5
200 */
201 public function get_unused_filename($contextid, $component, $filearea, $itemid, $filepath, $filename) {
202 global $DB;
203
204 // Do not accept '.' or an empty file name (zero is acceptable).
205 if ($filename == '.' || (empty($filename) && !is_numeric($filename))) {
206 throw new coding_exception('Invalid file name passed', $filename);
207 }
208
209 // The file does not exist, we return the same file name.
210 if (!$this->file_exists($contextid, $component, $filearea, $itemid, $filepath, $filename)) {
211 return $filename;
212 }
213
214 // Trying to locate a file name using the used pattern. We remove the used pattern from the file name first.
215 $pathinfo = pathinfo($filename);
216 $basename = $pathinfo['filename'];
217 $matches = array();
218 if (preg_match('~^(.+) \(([0-9]+)\)$~', $basename, $matches)) {
219 $basename = $matches[1];
220 }
221
222 $filenamelike = $DB->sql_like_escape($basename) . ' (%)';
223 if (isset($pathinfo['extension'])) {
224 $filenamelike .= '.' . $DB->sql_like_escape($pathinfo['extension']);
225 }
226
227 $filenamelikesql = $DB->sql_like('f.filename', ':filenamelike');
228 $filenamelen = $DB->sql_length('f.filename');
229 $sql = "SELECT filename
230 FROM {files} f
231 WHERE
232 f.contextid = :contextid AND
233 f.component = :component AND
234 f.filearea = :filearea AND
235 f.itemid = :itemid AND
236 f.filepath = :filepath AND
237 $filenamelikesql
238 ORDER BY
239 $filenamelen DESC,
240 f.filename DESC";
241 $params = array('contextid' => $contextid, 'component' => $component, 'filearea' => $filearea, 'itemid' => $itemid,
242 'filepath' => $filepath, 'filenamelike' => $filenamelike);
243 $results = $DB->get_fieldset_sql($sql, $params, IGNORE_MULTIPLE);
244
245 // Loop over the results to make sure we are working on a valid file name. Because 'file (1).txt' and 'file (copy).txt'
246 // would both be returned, but only the one only containing digits should be used.
247 $number = 1;
248 foreach ($results as $result) {
249 $resultbasename = pathinfo($result, PATHINFO_FILENAME);
250 $matches = array();
251 if (preg_match('~^(.+) \(([0-9]+)\)$~', $resultbasename, $matches)) {
252 $number = $matches[2] + 1;
253 break;
254 }
255 }
256
257 // Constructing the new filename.
258 $newfilename = $basename . ' (' . $number . ')';
259 if (isset($pathinfo['extension'])) {
260 $newfilename .= '.' . $pathinfo['extension'];
261 }
262
263 return $newfilename;
264 }
265
c4d19c5a
DM
266 /**
267 * Generates a preview image for the stored file
268 *
269 * @param stored_file $file the file we want to preview
270 * @param string $mode preview mode, eg. 'thumb'
271 * @return stored_file|bool the newly created preview file or false
272 */
273 protected function create_file_preview(stored_file $file, $mode) {
274
275 $mimetype = $file->get_mimetype();
276
fe68aac7 277 if ($mimetype === 'image/gif' or $mimetype === 'image/jpeg' or $mimetype === 'image/png') {
c4d19c5a
DM
278 // make a preview of the image
279 $data = $this->create_imagefile_preview($file, $mode);
280
281 } else {
282 // unable to create the preview of this mimetype yet
283 return false;
284 }
285
286 if (empty($data)) {
287 return false;
288 }
289
290 // getimagesizefromstring() is available from PHP 5.4 but we need to support
291 // lower versions, so...
292 $tmproot = make_temp_directory('thumbnails');
94d10417 293 $tmpfilepath = $tmproot.'/'.$file->get_contenthash().'_'.$mode;
c4d19c5a
DM
294 file_put_contents($tmpfilepath, $data);
295 $imageinfo = getimagesize($tmpfilepath);
296 unlink($tmpfilepath);
297
298 $context = context_system::instance();
299
300 $record = array(
301 'contextid' => $context->id,
302 'component' => 'core',
303 'filearea' => 'preview',
304 'itemid' => 0,
305 'filepath' => '/' . trim($mode, '/') . '/',
306 'filename' => $file->get_contenthash(),
307 );
308
309 if ($imageinfo) {
310 $record['mimetype'] = $imageinfo['mime'];
311 }
312
313 return $this->create_file_from_string($record, $data);
314 }
315
316 /**
317 * Generates a preview for the stored image file
318 *
319 * @param stored_file $file the image we want to preview
320 * @param string $mode preview mode, eg. 'thumb'
321 * @return string|bool false if a problem occurs, the thumbnail image data otherwise
322 */
323 protected function create_imagefile_preview(stored_file $file, $mode) {
324 global $CFG;
325 require_once($CFG->libdir.'/gdlib.php');
326
327 $tmproot = make_temp_directory('thumbnails');
328 $tmpfilepath = $tmproot.'/'.$file->get_contenthash();
329 $file->copy_content_to($tmpfilepath);
330
fe68aac7 331 if ($mode === 'tinyicon') {
10f0978b 332 $data = generate_image_thumbnail($tmpfilepath, 24, 24);
c4d19c5a 333
fe68aac7 334 } else if ($mode === 'thumb') {
c4d19c5a
DM
335 $data = generate_image_thumbnail($tmpfilepath, 90, 90);
336
8f5cbbd6
DM
337 } else if ($mode === 'bigthumb') {
338 $data = generate_image_thumbnail($tmpfilepath, 250, 250);
339
c4d19c5a
DM
340 } else {
341 throw new file_exception('storedfileproblem', 'Invalid preview mode requested');
342 }
343
344 unlink($tmpfilepath);
345
346 return $data;
347 }
348
172dd12c 349 /**
25aebf09 350 * Fetch file using local file id.
bf9ffe27 351 *
25aebf09 352 * Please do not rely on file ids, it is usually easier to use
353 * pathname hashes instead.
bf9ffe27 354 *
d2b7803e
DC
355 * @param int $fileid file ID
356 * @return stored_file|bool stored_file instance if exists, false if not
172dd12c 357 */
358 public function get_file_by_id($fileid) {
359 global $DB;
360
3447100c 361 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
67233725
DC
362 FROM {files} f
363 LEFT JOIN {files_reference} r
364 ON f.referencefileid = r.id
365 WHERE f.id = ?";
366 if ($filerecord = $DB->get_record_sql($sql, array($fileid))) {
367 return $this->get_file_instance($filerecord);
172dd12c 368 } else {
369 return false;
370 }
371 }
372
373 /**
374 * Fetch file using local file full pathname hash
bf9ffe27 375 *
d2b7803e
DC
376 * @param string $pathnamehash path name hash
377 * @return stored_file|bool stored_file instance if exists, false if not
172dd12c 378 */
379 public function get_file_by_hash($pathnamehash) {
380 global $DB;
381
3447100c 382 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
67233725
DC
383 FROM {files} f
384 LEFT JOIN {files_reference} r
385 ON f.referencefileid = r.id
386 WHERE f.pathnamehash = ?";
387 if ($filerecord = $DB->get_record_sql($sql, array($pathnamehash))) {
388 return $this->get_file_instance($filerecord);
172dd12c 389 } else {
390 return false;
391 }
392 }
393
394 /**
bf9ffe27
PS
395 * Fetch locally stored file.
396 *
d2b7803e
DC
397 * @param int $contextid context ID
398 * @param string $component component
399 * @param string $filearea file area
400 * @param int $itemid item ID
401 * @param string $filepath file path
402 * @param string $filename file name
403 * @return stored_file|bool stored_file instance if exists, false if not
172dd12c 404 */
64f93798 405 public function get_file($contextid, $component, $filearea, $itemid, $filepath, $filename) {
172dd12c 406 $filepath = clean_param($filepath, PARAM_PATH);
407 $filename = clean_param($filename, PARAM_FILE);
408
409 if ($filename === '') {
410 $filename = '.';
411 }
412
64f93798 413 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename);
172dd12c 414 return $this->get_file_by_hash($pathnamehash);
415 }
416
16741cac
PS
417 /**
418 * Are there any files (or directories)
d2b7803e
DC
419 *
420 * @param int $contextid context ID
421 * @param string $component component
422 * @param string $filearea file area
423 * @param bool|int $itemid item id or false if all items
424 * @param bool $ignoredirs whether or not ignore directories
16741cac
PS
425 * @return bool empty
426 */
427 public function is_area_empty($contextid, $component, $filearea, $itemid = false, $ignoredirs = true) {
428 global $DB;
429
430 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
431 $where = "contextid = :contextid AND component = :component AND filearea = :filearea";
432
433 if ($itemid !== false) {
434 $params['itemid'] = $itemid;
435 $where .= " AND itemid = :itemid";
436 }
437
438 if ($ignoredirs) {
439 $sql = "SELECT 'x'
440 FROM {files}
441 WHERE $where AND filename <> '.'";
442 } else {
443 $sql = "SELECT 'x'
444 FROM {files}
445 WHERE $where AND (filename <> '.' OR filepath <> '/')";
446 }
447
448 return !$DB->record_exists_sql($sql, $params);
449 }
450
67233725
DC
451 /**
452 * Returns all files belonging to given repository
453 *
454 * @param int $repositoryid
9f4789b8 455 * @param string $sort A fragment of SQL to use for sorting
67233725
DC
456 */
457 public function get_external_files($repositoryid, $sort = 'sortorder, itemid, filepath, filename') {
458 global $DB;
3447100c 459 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
67233725
DC
460 FROM {files} f
461 LEFT JOIN {files_reference} r
462 ON f.referencefileid = r.id
9f4789b8
SH
463 WHERE r.repositoryid = ?";
464 if (!empty($sort)) {
465 $sql .= " ORDER BY {$sort}";
466 }
67233725
DC
467
468 $result = array();
469 $filerecords = $DB->get_records_sql($sql, array($repositoryid));
470 foreach ($filerecords as $filerecord) {
471 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
472 }
473 return $result;
474 }
475
172dd12c 476 /**
477 * Returns all area files (optionally limited by itemid)
bf9ffe27 478 *
d2b7803e
DC
479 * @param int $contextid context ID
480 * @param string $component component
481 * @param string $filearea file area
482 * @param int $itemid item ID or all files if not specified
9f4789b8 483 * @param string $sort A fragment of SQL to use for sorting
d2b7803e 484 * @param bool $includedirs whether or not include directories
cd5be217 485 * @return array of stored_files indexed by pathanmehash
172dd12c 486 */
db232bb0 487 public function get_area_files($contextid, $component, $filearea, $itemid = false, $sort = "itemid, filepath, filename", $includedirs = true) {
172dd12c 488 global $DB;
489
64f93798 490 $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
172dd12c 491 if ($itemid !== false) {
67233725 492 $itemidsql = ' AND f.itemid = :itemid ';
172dd12c 493 $conditions['itemid'] = $itemid;
67233725
DC
494 } else {
495 $itemidsql = '';
172dd12c 496 }
497
3447100c 498 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
67233725
DC
499 FROM {files} f
500 LEFT JOIN {files_reference} r
501 ON f.referencefileid = r.id
502 WHERE f.contextid = :contextid
503 AND f.component = :component
504 AND f.filearea = :filearea
9f4789b8
SH
505 $itemidsql";
506 if (!empty($sort)) {
507 $sql .= " ORDER BY {$sort}";
508 }
67233725 509
172dd12c 510 $result = array();
67233725
DC
511 $filerecords = $DB->get_records_sql($sql, $conditions);
512 foreach ($filerecords as $filerecord) {
513 if (!$includedirs and $filerecord->filename === '.') {
172dd12c 514 continue;
515 }
67233725 516 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
172dd12c 517 }
518 return $result;
519 }
520
752b9f42 521 /**
522 * Returns array based tree structure of area files
bf9ffe27 523 *
d2b7803e
DC
524 * @param int $contextid context ID
525 * @param string $component component
526 * @param string $filearea file area
527 * @param int $itemid item ID
752b9f42 528 * @return array each dir represented by dirname, subdirs, files and dirfile array elements
529 */
64f93798 530 public function get_area_tree($contextid, $component, $filearea, $itemid) {
752b9f42 531 $result = array('dirname'=>'', 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
db232bb0 532 $files = $this->get_area_files($contextid, $component, $filearea, $itemid, '', true);
752b9f42 533 // first create directory structure
534 foreach ($files as $hash=>$dir) {
535 if (!$dir->is_directory()) {
536 continue;
537 }
538 unset($files[$hash]);
539 if ($dir->get_filepath() === '/') {
540 $result['dirfile'] = $dir;
541 continue;
542 }
543 $parts = explode('/', trim($dir->get_filepath(),'/'));
544 $pointer =& $result;
545 foreach ($parts as $part) {
3b607678 546 if ($part === '') {
547 continue;
548 }
752b9f42 549 if (!isset($pointer['subdirs'][$part])) {
550 $pointer['subdirs'][$part] = array('dirname'=>$part, 'dirfile'=>null, 'subdirs'=>array(), 'files'=>array());
551 }
552 $pointer =& $pointer['subdirs'][$part];
553 }
554 $pointer['dirfile'] = $dir;
555 unset($pointer);
556 }
557 foreach ($files as $hash=>$file) {
558 $parts = explode('/', trim($file->get_filepath(),'/'));
559 $pointer =& $result;
560 foreach ($parts as $part) {
3b607678 561 if ($part === '') {
562 continue;
563 }
752b9f42 564 $pointer =& $pointer['subdirs'][$part];
565 }
566 $pointer['files'][$file->get_filename()] = $file;
567 unset($pointer);
568 }
db232bb0 569 $result = $this->sort_area_tree($result);
752b9f42 570 return $result;
571 }
572
db232bb0
FM
573 /**
574 * Sorts the result of {@link file_storage::get_area_tree()}.
575 *
52ebfade 576 * @param array $tree Array of results provided by {@link file_storage::get_area_tree()}
db232bb0
FM
577 * @return array of sorted results
578 */
579 protected function sort_area_tree($tree) {
580 foreach ($tree as $key => &$value) {
581 if ($key == 'subdirs') {
582 $value = $this->sort_area_tree($value);
583 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
584 } else if ($key == 'files') {
585 collatorlib::ksort($value, collatorlib::SORT_NATURAL);
586 }
587 }
588 return $tree;
589 }
590
ee03a651 591 /**
bf9ffe27
PS
592 * Returns all files and optionally directories
593 *
d2b7803e
DC
594 * @param int $contextid context ID
595 * @param string $component component
596 * @param string $filearea file area
597 * @param int $itemid item ID
ee03a651 598 * @param int $filepath directory path
599 * @param bool $recursive include all subdirectories
46fcbcf4 600 * @param bool $includedirs include files and directories
9f4789b8 601 * @param string $sort A fragment of SQL to use for sorting
cd5be217 602 * @return array of stored_files indexed by pathanmehash
ee03a651 603 */
64f93798 604 public function get_directory_files($contextid, $component, $filearea, $itemid, $filepath, $recursive = false, $includedirs = true, $sort = "filepath, filename") {
ee03a651 605 global $DB;
606
64f93798 607 if (!$directory = $this->get_file($contextid, $component, $filearea, $itemid, $filepath, '.')) {
ee03a651 608 return array();
609 }
610
9f4789b8
SH
611 $orderby = (!empty($sort)) ? " ORDER BY {$sort}" : '';
612
ee03a651 613 if ($recursive) {
614
46fcbcf4 615 $dirs = $includedirs ? "" : "AND filename <> '.'";
f8311def 616 $length = textlib::strlen($filepath);
ee03a651 617
3447100c 618 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
462c4955
DC
619 FROM {files} f
620 LEFT JOIN {files_reference} r
621 ON f.referencefileid = r.id
622 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
623 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
624 AND f.id <> :dirid
ee03a651 625 $dirs
9f4789b8 626 $orderby";
64f93798 627 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
ee03a651 628
629 $files = array();
630 $dirs = array();
67233725
DC
631 $filerecords = $DB->get_records_sql($sql, $params);
632 foreach ($filerecords as $filerecord) {
633 if ($filerecord->filename == '.') {
634 $dirs[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
ee03a651 635 } else {
67233725 636 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
ee03a651 637 }
638 }
639 $result = array_merge($dirs, $files);
640
641 } else {
642 $result = array();
64f93798 643 $params = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
ee03a651 644
f8311def 645 $length = textlib::strlen($filepath);
ee03a651 646
46fcbcf4 647 if ($includedirs) {
3447100c 648 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
462c4955
DC
649 FROM {files} f
650 LEFT JOIN {files_reference} r
651 ON f.referencefileid = r.id
652 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea
653 AND f.itemid = :itemid AND f.filename = '.'
654 AND ".$DB->sql_substr("f.filepath", 1, $length)." = :filepath
655 AND f.id <> :dirid
9f4789b8 656 $orderby";
ee03a651 657 $reqlevel = substr_count($filepath, '/') + 1;
67233725
DC
658 $filerecords = $DB->get_records_sql($sql, $params);
659 foreach ($filerecords as $filerecord) {
660 if (substr_count($filerecord->filepath, '/') !== $reqlevel) {
ee03a651 661 continue;
662 }
67233725 663 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
ee03a651 664 }
665 }
666
3447100c 667 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
462c4955
DC
668 FROM {files} f
669 LEFT JOIN {files_reference} r
670 ON f.referencefileid = r.id
671 WHERE f.contextid = :contextid AND f.component = :component AND f.filearea = :filearea AND f.itemid = :itemid
672 AND f.filepath = :filepath AND f.filename <> '.'
9f4789b8 673 $orderby";
ee03a651 674
67233725
DC
675 $filerecords = $DB->get_records_sql($sql, $params);
676 foreach ($filerecords as $filerecord) {
677 $result[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
ee03a651 678 }
679 }
680
681 return $result;
682 }
683
172dd12c 684 /**
bf9ffe27
PS
685 * Delete all area files (optionally limited by itemid).
686 *
d2b7803e
DC
687 * @param int $contextid context ID
688 * @param string $component component
689 * @param string $filearea file area or all areas in context if not specified
690 * @param int $itemid item ID or all files if not specified
bf9ffe27 691 * @return bool success
172dd12c 692 */
64f93798 693 public function delete_area_files($contextid, $component = false, $filearea = false, $itemid = false) {
172dd12c 694 global $DB;
695
6311eb61 696 $conditions = array('contextid'=>$contextid);
64f93798
PS
697 if ($component !== false) {
698 $conditions['component'] = $component;
699 }
6311eb61 700 if ($filearea !== false) {
701 $conditions['filearea'] = $filearea;
702 }
172dd12c 703 if ($itemid !== false) {
704 $conditions['itemid'] = $itemid;
705 }
706
67233725
DC
707 $filerecords = $DB->get_records('files', $conditions);
708 foreach ($filerecords as $filerecord) {
709 $this->get_file_instance($filerecord)->delete();
172dd12c 710 }
711
bf9ffe27 712 return true; // BC only
172dd12c 713 }
714
af7b3673
TH
715 /**
716 * Delete all the files from certain areas where itemid is limited by an
717 * arbitrary bit of SQL.
718 *
719 * @param int $contextid the id of the context the files belong to. Must be given.
720 * @param string $component the owning component. Must be given.
721 * @param string $filearea the file area name. Must be given.
722 * @param string $itemidstest an SQL fragment that the itemid must match. Used
723 * in the query like WHERE itemid $itemidstest. Must used named parameters,
724 * and may not used named parameters called contextid, component or filearea.
725 * @param array $params any query params used by $itemidstest.
726 */
727 public function delete_area_files_select($contextid, $component,
728 $filearea, $itemidstest, array $params = null) {
729 global $DB;
730
731 $where = "contextid = :contextid
732 AND component = :component
733 AND filearea = :filearea
734 AND itemid $itemidstest";
735 $params['contextid'] = $contextid;
736 $params['component'] = $component;
737 $params['filearea'] = $filearea;
738
67233725
DC
739 $filerecords = $DB->get_recordset_select('files', $where, $params);
740 foreach ($filerecords as $filerecord) {
741 $this->get_file_instance($filerecord)->delete();
af7b3673 742 }
67233725 743 $filerecords->close();
af7b3673
TH
744 }
745
546b8864
DM
746 /**
747 * Delete all files associated with the given component.
748 *
749 * @param string $component the component owning the file
750 */
751 public function delete_component_files($component) {
752 global $DB;
753
754 $filerecords = $DB->get_recordset('files', array('component' => $component));
755 foreach ($filerecords as $filerecord) {
756 $this->get_file_instance($filerecord)->delete();
757 }
758 $filerecords->close();
759 }
760
d2af1014
TH
761 /**
762 * Move all the files in a file area from one context to another.
d2b7803e
DC
763 *
764 * @param int $oldcontextid the context the files are being moved from.
765 * @param int $newcontextid the context the files are being moved to.
d2af1014
TH
766 * @param string $component the plugin that these files belong to.
767 * @param string $filearea the name of the file area.
d2b7803e
DC
768 * @param int $itemid file item ID
769 * @return int the number of files moved, for information.
d2af1014
TH
770 */
771 public function move_area_files_to_new_context($oldcontextid, $newcontextid, $component, $filearea, $itemid = false) {
772 // Note, this code is based on some code that Petr wrote in
773 // forum_move_attachments in mod/forum/lib.php. I moved it here because
774 // I needed it in the question code too.
775 $count = 0;
776
777 $oldfiles = $this->get_area_files($oldcontextid, $component, $filearea, $itemid, 'id', false);
778 foreach ($oldfiles as $oldfile) {
779 $filerecord = new stdClass();
780 $filerecord->contextid = $newcontextid;
781 $this->create_file_from_storedfile($filerecord, $oldfile);
782 $count += 1;
783 }
784
785 if ($count) {
786 $this->delete_area_files($oldcontextid, $component, $filearea, $itemid);
787 }
788
789 return $count;
790 }
791
172dd12c 792 /**
bf9ffe27
PS
793 * Recursively creates directory.
794 *
d2b7803e
DC
795 * @param int $contextid context ID
796 * @param string $component component
797 * @param string $filearea file area
798 * @param int $itemid item ID
799 * @param string $filepath file path
800 * @param int $userid the user ID
172dd12c 801 * @return bool success
802 */
64f93798 803 public function create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid = null) {
172dd12c 804 global $DB;
805
806 // validate all parameters, we do not want any rubbish stored in database, right?
807 if (!is_number($contextid) or $contextid < 1) {
145a0a31 808 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 809 }
810
aff24313
PS
811 $component = clean_param($component, PARAM_COMPONENT);
812 if (empty($component)) {
64f93798
PS
813 throw new file_exception('storedfileproblem', 'Invalid component');
814 }
815
aff24313
PS
816 $filearea = clean_param($filearea, PARAM_AREA);
817 if (empty($filearea)) {
145a0a31 818 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 819 }
820
821 if (!is_number($itemid) or $itemid < 0) {
145a0a31 822 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 823 }
824
825 $filepath = clean_param($filepath, PARAM_PATH);
826 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
827 // path must start and end with '/'
145a0a31 828 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 829 }
830
64f93798 831 $pathnamehash = $this->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, '.');
172dd12c 832
833 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
834 return $dir_info;
835 }
836
837 static $contenthash = null;
838 if (!$contenthash) {
b48f3e06 839 $this->add_string_to_pool('');
172dd12c 840 $contenthash = sha1('');
841 }
842
843 $now = time();
844
ac6f1a82 845 $dir_record = new stdClass();
172dd12c 846 $dir_record->contextid = $contextid;
64f93798 847 $dir_record->component = $component;
172dd12c 848 $dir_record->filearea = $filearea;
849 $dir_record->itemid = $itemid;
850 $dir_record->filepath = $filepath;
851 $dir_record->filename = '.';
852 $dir_record->contenthash = $contenthash;
853 $dir_record->filesize = 0;
854
855 $dir_record->timecreated = $now;
856 $dir_record->timemodified = $now;
857 $dir_record->mimetype = null;
858 $dir_record->userid = $userid;
859
860 $dir_record->pathnamehash = $pathnamehash;
861
862 $DB->insert_record('files', $dir_record);
863 $dir_info = $this->get_file_by_hash($pathnamehash);
864
865 if ($filepath !== '/') {
866 //recurse to parent dirs
867 $filepath = trim($filepath, '/');
868 $filepath = explode('/', $filepath);
869 array_pop($filepath);
870 $filepath = implode('/', $filepath);
871 $filepath = ($filepath === '') ? '/' : "/$filepath/";
64f93798 872 $this->create_directory($contextid, $component, $filearea, $itemid, $filepath, $userid);
172dd12c 873 }
874
875 return $dir_info;
876 }
877
878 /**
bf9ffe27
PS
879 * Add new local file based on existing local file.
880 *
67233725 881 * @param stdClass|array $filerecord object or array describing changes
d2b7803e 882 * @param stored_file|int $fileorid id or stored_file instance of the existing local file
bf9ffe27 883 * @return stored_file instance of newly created file
172dd12c 884 */
67233725 885 public function create_file_from_storedfile($filerecord, $fileorid) {
4fb2306e 886 global $DB;
172dd12c 887
72d0aed6 888 if ($fileorid instanceof stored_file) {
889 $fid = $fileorid->get_id();
890 } else {
891 $fid = $fileorid;
8eb1e0a1 892 }
893
67233725 894 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
ec8b711f 895
67233725
DC
896 unset($filerecord['id']);
897 unset($filerecord['filesize']);
898 unset($filerecord['contenthash']);
899 unset($filerecord['pathnamehash']);
172dd12c 900
3447100c 901 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
67233725
DC
902 FROM {files} f
903 LEFT JOIN {files_reference} r
904 ON f.referencefileid = r.id
905 WHERE f.id = ?";
906
907 if (!$newrecord = $DB->get_record_sql($sql, array($fid))) {
145a0a31 908 throw new file_exception('storedfileproblem', 'File does not exist');
172dd12c 909 }
910
911 unset($newrecord->id);
912
67233725 913 foreach ($filerecord as $key => $value) {
172dd12c 914 // validate all parameters, we do not want any rubbish stored in database, right?
915 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
145a0a31 916 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 917 }
918
64f93798 919 if ($key == 'component') {
aff24313
PS
920 $value = clean_param($value, PARAM_COMPONENT);
921 if (empty($value)) {
64f93798
PS
922 throw new file_exception('storedfileproblem', 'Invalid component');
923 }
924 }
925
172dd12c 926 if ($key == 'filearea') {
aff24313
PS
927 $value = clean_param($value, PARAM_AREA);
928 if (empty($value)) {
145a0a31 929 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 930 }
931 }
932
933 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
145a0a31 934 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 935 }
936
937
938 if ($key == 'filepath') {
939 $value = clean_param($value, PARAM_PATH);
00c32c54 940 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
172dd12c 941 // path must start and end with '/'
145a0a31 942 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 943 }
944 }
945
946 if ($key == 'filename') {
947 $value = clean_param($value, PARAM_FILE);
948 if ($value === '') {
949 // path must start and end with '/'
145a0a31 950 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 951 }
952 }
953
260c4a5b
PS
954 if ($key === 'timecreated' or $key === 'timemodified') {
955 if (!is_number($value)) {
956 throw new file_exception('storedfileproblem', 'Invalid file '.$key);
957 }
958 if ($value < 0) {
959 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
960 $value = 0;
961 }
962 }
963
67233725
DC
964 if ($key == 'referencefileid' or $key == 'referencelastsync' or $key == 'referencelifetime') {
965 $value = clean_param($value, PARAM_INT);
966 }
967
172dd12c 968 $newrecord->$key = $value;
969 }
970
64f93798 971 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 972
cd5be217 973 if ($newrecord->filename === '.') {
974 // special case - only this function supports directories ;-)
64f93798 975 $directory = $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
cd5be217 976 // update the existing directory with the new data
977 $newrecord->id = $directory->get_id();
b8ac7ece 978 $DB->update_record('files', $newrecord);
693ef3a8 979 return $this->get_file_instance($newrecord);
cd5be217 980 }
981
d83ce953
DM
982 // note: referencefileid is copied from the original file so that
983 // creating a new file from an existing alias creates new alias implicitly.
984 // here we just check the database consistency.
67233725 985 if (!empty($newrecord->repositoryid)) {
d83ce953
DM
986 if ($newrecord->referencefileid != $this->get_referencefileid($newrecord->repositoryid, $newrecord->reference, MUST_EXIST)) {
987 throw new file_reference_exception($newrecord->repositoryid, $newrecord->reference, $newrecord->referencefileid);
67233725 988 }
67233725
DC
989 }
990
172dd12c 991 try {
992 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 993 } catch (dml_exception $e) {
64f93798 994 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 995 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 996 }
997
67233725 998
64f93798 999 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 1000
693ef3a8 1001 return $this->get_file_instance($newrecord);
172dd12c 1002 }
1003
6e73ac42 1004 /**
bf9ffe27
PS
1005 * Add new local file.
1006 *
67233725 1007 * @param stdClass|array $filerecord object or array describing file
d2b7803e
DC
1008 * @param string $url the URL to the file
1009 * @param array $options {@link download_file_content()} options
3a1055a5 1010 * @param bool $usetempfile use temporary file for download, may prevent out of memory problems
d2b7803e 1011 * @return stored_file
6e73ac42 1012 */
67233725 1013 public function create_file_from_url($filerecord, $url, array $options = null, $usetempfile = false) {
ec8b711f 1014
67233725
DC
1015 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1016 $filerecord = (object)$filerecord; // We support arrays too.
6e73ac42 1017
1018 $headers = isset($options['headers']) ? $options['headers'] : null;
1019 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
1020 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
1021 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
1022 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
1023 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
5f1c825d 1024 $calctimeout = isset($options['calctimeout']) ? $options['calctimeout'] : false;
6e73ac42 1025
67233725 1026 if (!isset($filerecord->filename)) {
6e73ac42 1027 $parts = explode('/', $url);
1028 $filename = array_pop($parts);
67233725 1029 $filerecord->filename = clean_param($filename, PARAM_FILE);
6e73ac42 1030 }
67233725
DC
1031 $source = !empty($filerecord->source) ? $filerecord->source : $url;
1032 $filerecord->source = clean_param($source, PARAM_URL);
6e73ac42 1033
3a1055a5 1034 if ($usetempfile) {
c426ef3a 1035 check_dir_exists($this->tempdir);
3a1055a5 1036 $tmpfile = tempnam($this->tempdir, 'newfromurl');
60b5a2fe 1037 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, $tmpfile, $calctimeout);
3a1055a5
PS
1038 if ($content === false) {
1039 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1040 }
1041 try {
67233725 1042 $newfile = $this->create_file_from_pathname($filerecord, $tmpfile);
3a1055a5
PS
1043 @unlink($tmpfile);
1044 return $newfile;
1045 } catch (Exception $e) {
1046 @unlink($tmpfile);
1047 throw $e;
1048 }
1049
1050 } else {
60b5a2fe 1051 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify, NULL, $calctimeout);
3a1055a5
PS
1052 if ($content === false) {
1053 throw new file_exception('storedfileproblem', 'Can not fetch file form URL');
1054 }
67233725 1055 return $this->create_file_from_string($filerecord, $content);
3a1055a5 1056 }
6e73ac42 1057 }
1058
172dd12c 1059 /**
bf9ffe27
PS
1060 * Add new local file.
1061 *
67233725 1062 * @param stdClass|array $filerecord object or array describing file
d2b7803e
DC
1063 * @param string $pathname path to file or content of file
1064 * @return stored_file
172dd12c 1065 */
67233725 1066 public function create_file_from_pathname($filerecord, $pathname) {
4fb2306e 1067 global $DB;
172dd12c 1068
67233725
DC
1069 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1070 $filerecord = (object)$filerecord; // We support arrays too.
172dd12c 1071
1072 // validate all parameters, we do not want any rubbish stored in database, right?
67233725 1073 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
145a0a31 1074 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 1075 }
1076
67233725
DC
1077 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1078 if (empty($filerecord->component)) {
64f93798
PS
1079 throw new file_exception('storedfileproblem', 'Invalid component');
1080 }
1081
67233725
DC
1082 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1083 if (empty($filerecord->filearea)) {
145a0a31 1084 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 1085 }
1086
67233725 1087 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
145a0a31 1088 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 1089 }
1090
67233725
DC
1091 if (!empty($filerecord->sortorder)) {
1092 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1093 $filerecord->sortorder = 0;
f79321f1
DC
1094 }
1095 } else {
67233725 1096 $filerecord->sortorder = 0;
f79321f1
DC
1097 }
1098
67233725
DC
1099 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1100 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
172dd12c 1101 // path must start and end with '/'
145a0a31 1102 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 1103 }
1104
67233725
DC
1105 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1106 if ($filerecord->filename === '') {
e1dcb950 1107 // filename must not be empty
145a0a31 1108 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 1109 }
1110
1111 $now = time();
67233725
DC
1112 if (isset($filerecord->timecreated)) {
1113 if (!is_number($filerecord->timecreated)) {
260c4a5b
PS
1114 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1115 }
67233725 1116 if ($filerecord->timecreated < 0) {
260c4a5b 1117 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
67233725 1118 $filerecord->timecreated = 0;
260c4a5b
PS
1119 }
1120 } else {
67233725 1121 $filerecord->timecreated = $now;
260c4a5b
PS
1122 }
1123
67233725
DC
1124 if (isset($filerecord->timemodified)) {
1125 if (!is_number($filerecord->timemodified)) {
260c4a5b
PS
1126 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1127 }
67233725 1128 if ($filerecord->timemodified < 0) {
260c4a5b 1129 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
67233725 1130 $filerecord->timemodified = 0;
260c4a5b
PS
1131 }
1132 } else {
67233725 1133 $filerecord->timemodified = $now;
260c4a5b 1134 }
172dd12c 1135
ac6f1a82 1136 $newrecord = new stdClass();
172dd12c 1137
67233725
DC
1138 $newrecord->contextid = $filerecord->contextid;
1139 $newrecord->component = $filerecord->component;
1140 $newrecord->filearea = $filerecord->filearea;
1141 $newrecord->itemid = $filerecord->itemid;
1142 $newrecord->filepath = $filerecord->filepath;
1143 $newrecord->filename = $filerecord->filename;
1144
1145 $newrecord->timecreated = $filerecord->timecreated;
1146 $newrecord->timemodified = $filerecord->timemodified;
4c2fcbfc 1147 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($pathname, $filerecord->filename) : $filerecord->mimetype;
67233725
DC
1148 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1149 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1150 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1151 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
cfc4db40 1152 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
67233725 1153 $newrecord->sortorder = $filerecord->sortorder;
172dd12c 1154
b48f3e06 1155 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
172dd12c 1156
64f93798 1157 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 1158
1159 try {
1160 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 1161 } catch (dml_exception $e) {
172dd12c 1162 if ($newfile) {
ead14290 1163 $this->deleted_file_cleanup($newrecord->contenthash);
172dd12c 1164 }
64f93798 1165 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 1166 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 1167 }
1168
64f93798 1169 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 1170
693ef3a8 1171 return $this->get_file_instance($newrecord);
172dd12c 1172 }
1173
1174 /**
bf9ffe27
PS
1175 * Add new local file.
1176 *
67233725 1177 * @param stdClass|array $filerecord object or array describing file
172dd12c 1178 * @param string $content content of file
d2b7803e 1179 * @return stored_file
172dd12c 1180 */
67233725 1181 public function create_file_from_string($filerecord, $content) {
4fb2306e 1182 global $DB;
172dd12c 1183
67233725
DC
1184 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1185 $filerecord = (object)$filerecord; // We support arrays too.
172dd12c 1186
1187 // validate all parameters, we do not want any rubbish stored in database, right?
67233725 1188 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
145a0a31 1189 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 1190 }
1191
67233725
DC
1192 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1193 if (empty($filerecord->component)) {
64f93798
PS
1194 throw new file_exception('storedfileproblem', 'Invalid component');
1195 }
1196
67233725
DC
1197 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1198 if (empty($filerecord->filearea)) {
145a0a31 1199 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 1200 }
1201
67233725 1202 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
145a0a31 1203 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 1204 }
1205
67233725
DC
1206 if (!empty($filerecord->sortorder)) {
1207 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1208 $filerecord->sortorder = 0;
f79321f1
DC
1209 }
1210 } else {
67233725 1211 $filerecord->sortorder = 0;
f79321f1
DC
1212 }
1213
67233725
DC
1214 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1215 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
172dd12c 1216 // path must start and end with '/'
145a0a31 1217 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 1218 }
1219
67233725
DC
1220 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1221 if ($filerecord->filename === '') {
172dd12c 1222 // path must start and end with '/'
145a0a31 1223 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 1224 }
1225
1226 $now = time();
67233725
DC
1227 if (isset($filerecord->timecreated)) {
1228 if (!is_number($filerecord->timecreated)) {
260c4a5b
PS
1229 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1230 }
67233725 1231 if ($filerecord->timecreated < 0) {
260c4a5b 1232 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
67233725 1233 $filerecord->timecreated = 0;
260c4a5b
PS
1234 }
1235 } else {
67233725 1236 $filerecord->timecreated = $now;
260c4a5b
PS
1237 }
1238
67233725
DC
1239 if (isset($filerecord->timemodified)) {
1240 if (!is_number($filerecord->timemodified)) {
260c4a5b
PS
1241 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1242 }
67233725 1243 if ($filerecord->timemodified < 0) {
260c4a5b 1244 //NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
67233725 1245 $filerecord->timemodified = 0;
260c4a5b
PS
1246 }
1247 } else {
67233725 1248 $filerecord->timemodified = $now;
260c4a5b 1249 }
172dd12c 1250
ac6f1a82 1251 $newrecord = new stdClass();
172dd12c 1252
67233725
DC
1253 $newrecord->contextid = $filerecord->contextid;
1254 $newrecord->component = $filerecord->component;
1255 $newrecord->filearea = $filerecord->filearea;
1256 $newrecord->itemid = $filerecord->itemid;
1257 $newrecord->filepath = $filerecord->filepath;
1258 $newrecord->filename = $filerecord->filename;
1259
1260 $newrecord->timecreated = $filerecord->timecreated;
1261 $newrecord->timemodified = $filerecord->timemodified;
67233725
DC
1262 $newrecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1263 $newrecord->source = empty($filerecord->source) ? null : $filerecord->source;
1264 $newrecord->author = empty($filerecord->author) ? null : $filerecord->author;
1265 $newrecord->license = empty($filerecord->license) ? null : $filerecord->license;
cfc4db40 1266 $newrecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
67233725 1267 $newrecord->sortorder = $filerecord->sortorder;
1dce6261 1268
b48f3e06 1269 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
8177b7b9
DC
1270 $filepathname = $this->path_from_hash($newrecord->contenthash) . '/' . $newrecord->contenthash;
1271 // get mimetype by magic bytes
4c2fcbfc 1272 $newrecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filepathname, $filerecord->filename) : $filerecord->mimetype;
172dd12c 1273
64f93798 1274 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
172dd12c 1275
1276 try {
1277 $newrecord->id = $DB->insert_record('files', $newrecord);
8a680500 1278 } catch (dml_exception $e) {
172dd12c 1279 if ($newfile) {
ead14290 1280 $this->deleted_file_cleanup($newrecord->contenthash);
172dd12c 1281 }
64f93798 1282 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid,
694f3b74 1283 $newrecord->filepath, $newrecord->filename, $e->debuginfo);
172dd12c 1284 }
1285
64f93798 1286 $this->create_directory($newrecord->contextid, $newrecord->component, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
172dd12c 1287
693ef3a8 1288 return $this->get_file_instance($newrecord);
172dd12c 1289 }
1290
67233725 1291 /**
d83ce953 1292 * Create a new alias/shortcut file from file reference information
67233725 1293 *
d83ce953
DM
1294 * @param stdClass|array $filerecord object or array describing the new file
1295 * @param int $repositoryid the id of the repository that provides the original file
1296 * @param string $reference the information required by the repository to locate the original file
1297 * @param array $options options for creating the new file
67233725
DC
1298 * @return stored_file
1299 */
1300 public function create_file_from_reference($filerecord, $repositoryid, $reference, $options = array()) {
1301 global $DB;
1302
1303 $filerecord = (array)$filerecord; // Do not modify the submitted record, this cast unlinks objects.
1304 $filerecord = (object)$filerecord; // We support arrays too.
1305
1306 // validate all parameters, we do not want any rubbish stored in database, right?
1307 if (!is_number($filerecord->contextid) or $filerecord->contextid < 1) {
1308 throw new file_exception('storedfileproblem', 'Invalid contextid');
1309 }
1310
1311 $filerecord->component = clean_param($filerecord->component, PARAM_COMPONENT);
1312 if (empty($filerecord->component)) {
1313 throw new file_exception('storedfileproblem', 'Invalid component');
1314 }
1315
1316 $filerecord->filearea = clean_param($filerecord->filearea, PARAM_AREA);
1317 if (empty($filerecord->filearea)) {
1318 throw new file_exception('storedfileproblem', 'Invalid filearea');
1319 }
1320
1321 if (!is_number($filerecord->itemid) or $filerecord->itemid < 0) {
1322 throw new file_exception('storedfileproblem', 'Invalid itemid');
1323 }
1324
1325 if (!empty($filerecord->sortorder)) {
1326 if (!is_number($filerecord->sortorder) or $filerecord->sortorder < 0) {
1327 $filerecord->sortorder = 0;
1328 }
1329 } else {
1330 $filerecord->sortorder = 0;
1331 }
1332
42aa6e15
MG
1333 // TODO MDL-33416 [2.4] fields referencelastsync and referencelifetime to be removed from {files} table completely
1334 unset($filerecord->referencelastsync);
1335 unset($filerecord->referencelifetime);
1336
8177b7b9 1337 $filerecord->mimetype = empty($filerecord->mimetype) ? $this->mimetype($filerecord->filename) : $filerecord->mimetype;
67233725
DC
1338 $filerecord->userid = empty($filerecord->userid) ? null : $filerecord->userid;
1339 $filerecord->source = empty($filerecord->source) ? null : $filerecord->source;
1340 $filerecord->author = empty($filerecord->author) ? null : $filerecord->author;
1341 $filerecord->license = empty($filerecord->license) ? null : $filerecord->license;
cfc4db40 1342 $filerecord->status = empty($filerecord->status) ? 0 : $filerecord->status;
67233725
DC
1343 $filerecord->filepath = clean_param($filerecord->filepath, PARAM_PATH);
1344 if (strpos($filerecord->filepath, '/') !== 0 or strrpos($filerecord->filepath, '/') !== strlen($filerecord->filepath)-1) {
1345 // Path must start and end with '/'.
1346 throw new file_exception('storedfileproblem', 'Invalid file path');
1347 }
1348
1349 $filerecord->filename = clean_param($filerecord->filename, PARAM_FILE);
1350 if ($filerecord->filename === '') {
1351 // Path must start and end with '/'.
1352 throw new file_exception('storedfileproblem', 'Invalid file name');
1353 }
1354
1355 $now = time();
1356 if (isset($filerecord->timecreated)) {
1357 if (!is_number($filerecord->timecreated)) {
1358 throw new file_exception('storedfileproblem', 'Invalid file timecreated');
1359 }
1360 if ($filerecord->timecreated < 0) {
1361 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1362 $filerecord->timecreated = 0;
1363 }
1364 } else {
1365 $filerecord->timecreated = $now;
1366 }
1367
1368 if (isset($filerecord->timemodified)) {
1369 if (!is_number($filerecord->timemodified)) {
1370 throw new file_exception('storedfileproblem', 'Invalid file timemodified');
1371 }
1372 if ($filerecord->timemodified < 0) {
1373 // NOTE: unfortunately I make a mistake when creating the "files" table, we can not have negative numbers there, on the other hand no file should be older than 1970, right? (skodak)
1374 $filerecord->timemodified = 0;
1375 }
1376 } else {
1377 $filerecord->timemodified = $now;
1378 }
1379
e3c02118
DC
1380 $transaction = $DB->start_delegated_transaction();
1381
67233725 1382 try {
437f5dc4 1383 $filerecord->referencefileid = $this->get_or_create_referencefileid($repositoryid, $reference);
d83ce953
DM
1384 } catch (Exception $e) {
1385 throw new file_reference_exception($repositoryid, $reference, null, null, $e->getMessage());
67233725
DC
1386 }
1387
437f5dc4
MG
1388 if (isset($filerecord->contenthash) && $this->content_exists($filerecord->contenthash)) {
1389 // there was specified the contenthash for a file already stored in moodle filepool
1390 if (empty($filerecord->filesize)) {
1391 $filepathname = $this->path_from_hash($filerecord->contenthash) . '/' . $filerecord->contenthash;
1392 $filerecord->filesize = filesize($filepathname);
1393 } else {
1394 $filerecord->filesize = clean_param($filerecord->filesize, PARAM_INT);
1395 }
1396 } else {
1397 // atempt to get the result of last synchronisation for this reference
1398 $lastcontent = $DB->get_record('files', array('referencefileid' => $filerecord->referencefileid),
1399 'id, contenthash, filesize', IGNORE_MULTIPLE);
1400 if ($lastcontent) {
1401 $filerecord->contenthash = $lastcontent->contenthash;
1402 $filerecord->filesize = $lastcontent->filesize;
1403 } else {
1404 // External file doesn't have content in moodle.
1405 // So we create an empty file for it.
1406 list($filerecord->contenthash, $filerecord->filesize, $newfile) = $this->add_string_to_pool(null);
1407 }
1408 }
67233725
DC
1409
1410 $filerecord->pathnamehash = $this->get_pathname_hash($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->filename);
1411
1412 try {
1413 $filerecord->id = $DB->insert_record('files', $filerecord);
1414 } catch (dml_exception $e) {
437f5dc4 1415 if (!empty($newfile)) {
67233725
DC
1416 $this->deleted_file_cleanup($filerecord->contenthash);
1417 }
1418 throw new stored_file_creation_exception($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid,
1419 $filerecord->filepath, $filerecord->filename, $e->debuginfo);
1420 }
1421
1422 $this->create_directory($filerecord->contextid, $filerecord->component, $filerecord->filearea, $filerecord->itemid, $filerecord->filepath, $filerecord->userid);
1423
e3c02118
DC
1424 $transaction->allow_commit();
1425
42aa6e15
MG
1426 // this will retrieve all reference information from DB as well
1427 return $this->get_file_by_id($filerecord->id);
67233725
DC
1428 }
1429
797f19e8 1430 /**
1431 * Creates new image file from existing.
bf9ffe27 1432 *
67233725 1433 * @param stdClass|array $filerecord object or array describing new file
d2b7803e 1434 * @param int|stored_file $fid file id or stored file object
797f19e8 1435 * @param int $newwidth in pixels
1436 * @param int $newheight in pixels
d2b7803e 1437 * @param bool $keepaspectratio whether or not keep aspect ratio
bf9ffe27 1438 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no compression) for png
d2b7803e 1439 * @return stored_file
797f19e8 1440 */
67233725 1441 public function convert_image($filerecord, $fid, $newwidth = null, $newheight = null, $keepaspectratio = true, $quality = null) {
6b2f2184
AD
1442 if (!function_exists('imagecreatefromstring')) {
1443 //Most likely the GD php extension isn't installed
1444 //image conversion cannot succeed
1445 throw new file_exception('storedfileproblem', 'imagecreatefromstring() doesnt exist. The PHP extension "GD" must be installed for image conversion.');
1446 }
1447
797f19e8 1448 if ($fid instanceof stored_file) {
1449 $fid = $fid->get_id();
1450 }
1451
67233725 1452 $filerecord = (array)$filerecord; // We support arrays too, do not modify the submitted record!
797f19e8 1453
67233725 1454 if (!$file = $this->get_file_by_id($fid)) { // Make sure file really exists and we we correct data.
797f19e8 1455 throw new file_exception('storedfileproblem', 'File does not exist');
1456 }
1457
1458 if (!$imageinfo = $file->get_imageinfo()) {
1459 throw new file_exception('storedfileproblem', 'File is not an image');
1460 }
1461
67233725
DC
1462 if (!isset($filerecord['filename'])) {
1463 $filerecord['filename'] = $file->get_filename();
797f19e8 1464 }
1465
67233725 1466 if (!isset($filerecord['mimetype'])) {
8177b7b9 1467 $filerecord['mimetype'] = $imageinfo['mimetype'];
797f19e8 1468 }
1469
1470 $width = $imageinfo['width'];
1471 $height = $imageinfo['height'];
1472 $mimetype = $imageinfo['mimetype'];
1473
1474 if ($keepaspectratio) {
1475 if (0 >= $newwidth and 0 >= $newheight) {
1476 // no sizes specified
1477 $newwidth = $width;
1478 $newheight = $height;
1479
1480 } else if (0 < $newwidth and 0 < $newheight) {
1481 $xheight = ($newwidth*($height/$width));
1482 if ($xheight < $newheight) {
1483 $newheight = (int)$xheight;
1484 } else {
1485 $newwidth = (int)($newheight*($width/$height));
1486 }
1487
1488 } else if (0 < $newwidth) {
1489 $newheight = (int)($newwidth*($height/$width));
1490
1491 } else { //0 < $newheight
1492 $newwidth = (int)($newheight*($width/$height));
1493 }
1494
1495 } else {
1496 if (0 >= $newwidth) {
1497 $newwidth = $width;
1498 }
1499 if (0 >= $newheight) {
1500 $newheight = $height;
1501 }
1502 }
1503
1504 $img = imagecreatefromstring($file->get_content());
1505 if ($height != $newheight or $width != $newwidth) {
1506 $newimg = imagecreatetruecolor($newwidth, $newheight);
1507 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
1508 // weird
1509 throw new file_exception('storedfileproblem', 'Can not resize image');
1510 }
1511 imagedestroy($img);
1512 $img = $newimg;
1513 }
1514
1515 ob_start();
67233725 1516 switch ($filerecord['mimetype']) {
797f19e8 1517 case 'image/gif':
1518 imagegif($img);
1519 break;
1520
1521 case 'image/jpeg':
1522 if (is_null($quality)) {
1523 imagejpeg($img);
1524 } else {
1525 imagejpeg($img, NULL, $quality);
1526 }
1527 break;
1528
1529 case 'image/png':
8bd49ec0 1530 $quality = (int)$quality;
797f19e8 1531 imagepng($img, NULL, $quality, NULL);
1532 break;
1533
1534 default:
1535 throw new file_exception('storedfileproblem', 'Unsupported mime type');
1536 }
1537
1538 $content = ob_get_contents();
1539 ob_end_clean();
1540 imagedestroy($img);
1541
1542 if (!$content) {
1543 throw new file_exception('storedfileproblem', 'Can not convert image');
1544 }
1545
67233725 1546 return $this->create_file_from_string($filerecord, $content);
797f19e8 1547 }
1548
172dd12c 1549 /**
bf9ffe27
PS
1550 * Add file content to sha1 pool.
1551 *
172dd12c 1552 * @param string $pathname path to file
bf9ffe27
PS
1553 * @param string $contenthash sha1 hash of content if known (performance only)
1554 * @return array (contenthash, filesize, newfile)
172dd12c 1555 */
bf9ffe27 1556 public function add_file_to_pool($pathname, $contenthash = NULL) {
172dd12c 1557 if (!is_readable($pathname)) {
d610cb89 1558 throw new file_exception('storedfilecannotread', '', $pathname);
172dd12c 1559 }
1560
1561 if (is_null($contenthash)) {
1562 $contenthash = sha1_file($pathname);
1563 }
1564
1565 $filesize = filesize($pathname);
1566
1567 $hashpath = $this->path_from_hash($contenthash);
1568 $hashfile = "$hashpath/$contenthash";
1569
1570 if (file_exists($hashfile)) {
1571 if (filesize($hashfile) !== $filesize) {
1572 throw new file_pool_content_exception($contenthash);
1573 }
1574 $newfile = false;
1575
1576 } else {
1aa01caf 1577 if (!is_dir($hashpath)) {
1578 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1579 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1580 }
172dd12c 1581 }
1582 $newfile = true;
1583
6c0e2d08 1584 if (!copy($pathname, $hashfile)) {
d610cb89 1585 throw new file_exception('storedfilecannotread', '', $pathname);
172dd12c 1586 }
172dd12c 1587
1588 if (filesize($hashfile) !== $filesize) {
1589 @unlink($hashfile);
1590 throw new file_pool_content_exception($contenthash);
1591 }
1aa01caf 1592 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1593 }
1594
1595
1596 return array($contenthash, $filesize, $newfile);
1597 }
1598
1599 /**
bf9ffe27
PS
1600 * Add string content to sha1 pool.
1601 *
172dd12c 1602 * @param string $content file content - binary string
bf9ffe27 1603 * @return array (contenthash, filesize, newfile)
172dd12c 1604 */
b48f3e06 1605 public function add_string_to_pool($content) {
172dd12c 1606 $contenthash = sha1($content);
1607 $filesize = strlen($content); // binary length
1608
1609 $hashpath = $this->path_from_hash($contenthash);
1610 $hashfile = "$hashpath/$contenthash";
1611
1612
1613 if (file_exists($hashfile)) {
1614 if (filesize($hashfile) !== $filesize) {
1615 throw new file_pool_content_exception($contenthash);
1616 }
1617 $newfile = false;
1618
1619 } else {
1aa01caf 1620 if (!is_dir($hashpath)) {
1621 if (!mkdir($hashpath, $this->dirpermissions, true)) {
1622 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
1623 }
172dd12c 1624 }
1625 $newfile = true;
1626
6c0e2d08 1627 file_put_contents($hashfile, $content);
172dd12c 1628
1629 if (filesize($hashfile) !== $filesize) {
1630 @unlink($hashfile);
1631 throw new file_pool_content_exception($contenthash);
1632 }
1aa01caf 1633 chmod($hashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1634 }
1635
1636 return array($contenthash, $filesize, $newfile);
1637 }
1638
d5dd0540
PS
1639 /**
1640 * Serve file content using X-Sendfile header.
1641 * Please make sure that all headers are already sent
1642 * and the all access control checks passed.
1643 *
1644 * @param string $contenthash sah1 hash of the file content to be served
1645 * @return bool success
1646 */
1647 public function xsendfile($contenthash) {
1648 global $CFG;
1649 require_once("$CFG->libdir/xsendfilelib.php");
1650
1651 $hashpath = $this->path_from_hash($contenthash);
1652 return xsendfile("$hashpath/$contenthash");
1653 }
1654
67233725
DC
1655 /**
1656 * Content exists
1657 *
1658 * @param string $contenthash
1659 * @return bool
1660 */
1661 public function content_exists($contenthash) {
1662 $dir = $this->path_from_hash($contenthash);
1663 $filepath = $dir . '/' . $contenthash;
1664 return file_exists($filepath);
1665 }
1666
172dd12c 1667 /**
bf9ffe27 1668 * Return path to file with given hash.
172dd12c 1669 *
17d9269f 1670 * NOTE: must not be public, files in pool must not be modified
172dd12c 1671 *
d2b7803e 1672 * @param string $contenthash content hash
172dd12c 1673 * @return string expected file location
1674 */
17d9269f 1675 protected function path_from_hash($contenthash) {
172dd12c 1676 $l1 = $contenthash[0].$contenthash[1];
1677 $l2 = $contenthash[2].$contenthash[3];
d0b6f92a 1678 return "$this->filedir/$l1/$l2";
172dd12c 1679 }
1680
1aa01caf 1681 /**
bf9ffe27 1682 * Return path to file with given hash.
1aa01caf 1683 *
1684 * NOTE: must not be public, files in pool must not be modified
1685 *
d2b7803e 1686 * @param string $contenthash content hash
1aa01caf 1687 * @return string expected file location
1688 */
1689 protected function trash_path_from_hash($contenthash) {
1690 $l1 = $contenthash[0].$contenthash[1];
1691 $l2 = $contenthash[2].$contenthash[3];
d0b6f92a 1692 return "$this->trashdir/$l1/$l2";
1aa01caf 1693 }
1694
1695 /**
bf9ffe27
PS
1696 * Tries to recover missing content of file from trash.
1697 *
d2b7803e 1698 * @param stored_file $file stored_file instance
1aa01caf 1699 * @return bool success
1700 */
1701 public function try_content_recovery($file) {
1702 $contenthash = $file->get_contenthash();
1703 $trashfile = $this->trash_path_from_hash($contenthash).'/'.$contenthash;
1704 if (!is_readable($trashfile)) {
1705 if (!is_readable($this->trashdir.'/'.$contenthash)) {
1706 return false;
1707 }
1708 // nice, at least alternative trash file in trash root exists
1709 $trashfile = $this->trashdir.'/'.$contenthash;
1710 }
1711 if (filesize($trashfile) != $file->get_filesize() or sha1_file($trashfile) != $contenthash) {
1712 //weird, better fail early
1713 return false;
1714 }
1715 $contentdir = $this->path_from_hash($contenthash);
1716 $contentfile = $contentdir.'/'.$contenthash;
1717 if (file_exists($contentfile)) {
1718 //strange, no need to recover anything
1719 return true;
1720 }
1721 if (!is_dir($contentdir)) {
1722 if (!mkdir($contentdir, $this->dirpermissions, true)) {
1723 return false;
1724 }
1725 }
1726 return rename($trashfile, $contentfile);
1727 }
1728
172dd12c 1729 /**
bf9ffe27
PS
1730 * Marks pool file as candidate for deleting.
1731 *
1732 * DO NOT call directly - reserved for core!!
1733 *
172dd12c 1734 * @param string $contenthash
1735 */
1aa01caf 1736 public function deleted_file_cleanup($contenthash) {
172dd12c 1737 global $DB;
1738
18fa4f47 1739 if ($contenthash === sha1('')) {
e029dff4
PS
1740 // No need to delete empty content file with sha1('') content hash.
1741 return;
1742 }
1743
1aa01caf 1744 //Note: this section is critical - in theory file could be reused at the same
1745 // time, if this happens we can still recover the file from trash
1746 if ($DB->record_exists('files', array('contenthash'=>$contenthash))) {
1747 // file content is still used
1748 return;
1749 }
1750 //move content file to trash
1751 $contentfile = $this->path_from_hash($contenthash).'/'.$contenthash;
1752 if (!file_exists($contentfile)) {
1753 //weird, but no problem
172dd12c 1754 return;
1755 }
1aa01caf 1756 $trashpath = $this->trash_path_from_hash($contenthash);
1757 $trashfile = $trashpath.'/'.$contenthash;
1758 if (file_exists($trashfile)) {
1759 // we already have this content in trash, no need to move it there
1760 unlink($contentfile);
1761 return;
1762 }
1763 if (!is_dir($trashpath)) {
1764 mkdir($trashpath, $this->dirpermissions, true);
1765 }
1766 rename($contentfile, $trashfile);
1767 chmod($trashfile, $this->filepermissions); // fix permissions if needed
172dd12c 1768 }
1769
67233725
DC
1770 /**
1771 * When user referring to a moodle file, we build the reference field
1772 *
1773 * @param array $params
1774 * @return string
1775 */
1776 public static function pack_reference($params) {
1777 $params = (array)$params;
1778 $reference = array();
1779 $reference['contextid'] = is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT);
1780 $reference['component'] = is_null($params['component']) ? null : clean_param($params['component'], PARAM_COMPONENT);
1781 $reference['itemid'] = is_null($params['itemid']) ? null : clean_param($params['itemid'], PARAM_INT);
1782 $reference['filearea'] = is_null($params['filearea']) ? null : clean_param($params['filearea'], PARAM_AREA);
0e35ba6f 1783 $reference['filepath'] = is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH);
67233725
DC
1784 $reference['filename'] = is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE);
1785 return base64_encode(serialize($reference));
1786 }
1787
1788 /**
1789 * Unpack reference field
1790 *
1791 * @param string $str
0b2bfbd1 1792 * @param bool $cleanparams if set to true, array elements will be passed through {@link clean_param()}
483afa44 1793 * @throws file_reference_exception if the $str does not have the expected format
67233725
DC
1794 * @return array
1795 */
0b2bfbd1 1796 public static function unpack_reference($str, $cleanparams = false) {
6feae1d2
DM
1797 $decoded = base64_decode($str, true);
1798 if ($decoded === false) {
1799 throw new file_reference_exception(null, $str, null, null, 'Invalid base64 format');
1800 }
1801 $params = @unserialize($decoded); // hide E_NOTICE
1802 if ($params === false) {
1803 throw new file_reference_exception(null, $decoded, null, null, 'Not an unserializeable value');
1804 }
0b2bfbd1
MG
1805 if (is_array($params) && $cleanparams) {
1806 $params = array(
1807 'component' => is_null($params['component']) ? '' : clean_param($params['component'], PARAM_COMPONENT),
1808 'filearea' => is_null($params['filearea']) ? '' : clean_param($params['filearea'], PARAM_AREA),
1809 'itemid' => is_null($params['itemid']) ? 0 : clean_param($params['itemid'], PARAM_INT),
1810 'filename' => is_null($params['filename']) ? null : clean_param($params['filename'], PARAM_FILE),
1811 'filepath' => is_null($params['filepath']) ? null : clean_param($params['filepath'], PARAM_PATH),
1812 'contextid' => is_null($params['contextid']) ? null : clean_param($params['contextid'], PARAM_INT)
1813 );
1814 }
1815 return $params;
67233725
DC
1816 }
1817
1818 /**
483afa44
DM
1819 * Returns all aliases that refer to some stored_file via the given reference
1820 *
1821 * All repositories that provide access to a stored_file are expected to use
1822 * {@link self::pack_reference()}. This method can't be used if the given reference
1823 * does not use this format or if you are looking for references to an external file
1824 * (for example it can't be used to search for all aliases that refer to a given
1825 * Dropbox or Box.net file).
67233725 1826 *
0ad654dc
DM
1827 * Aliases in user draft areas are excluded from the returned list.
1828 *
1829 * @param string $reference identification of the referenced file
1830 * @return array of stored_file indexed by its pathnamehash
67233725 1831 */
0ad654dc 1832 public function search_references($reference) {
67233725 1833 global $DB;
0ad654dc
DM
1834
1835 if (is_null($reference)) {
1836 throw new coding_exception('NULL is not a valid reference to an external file');
1837 }
1838
483afa44
DM
1839 // Give {@link self::unpack_reference()} a chance to throw exception if the
1840 // reference is not in a valid format.
1841 self::unpack_reference($reference);
1842
0ad654dc
DM
1843 $referencehash = sha1($reference);
1844
3447100c 1845 $sql = "SELECT ".self::instance_sql_fields('f', 'r')."
67233725 1846 FROM {files} f
0ad654dc
DM
1847 JOIN {files_reference} r ON f.referencefileid = r.id
1848 JOIN {repository_instances} ri ON r.repositoryid = ri.id
1849 WHERE r.referencehash = ?
1850 AND (f.component <> ? OR f.filearea <> ?)";
67233725 1851
0ad654dc 1852 $rs = $DB->get_recordset_sql($sql, array($referencehash, 'user', 'draft'));
67233725
DC
1853 $files = array();
1854 foreach ($rs as $filerecord) {
0ad654dc 1855 $files[$filerecord->pathnamehash] = $this->get_file_instance($filerecord);
67233725
DC
1856 }
1857
1858 return $files;
1859 }
1860
1861 /**
483afa44
DM
1862 * Returns the number of aliases that refer to some stored_file via the given reference
1863 *
1864 * All repositories that provide access to a stored_file are expected to use
1865 * {@link self::pack_reference()}. This method can't be used if the given reference
1866 * does not use this format or if you are looking for references to an external file
1867 * (for example it can't be used to count aliases that refer to a given Dropbox or
1868 * Box.net file).
67233725 1869 *
0ad654dc
DM
1870 * Aliases in user draft areas are not counted.
1871 *
1872 * @param string $reference identification of the referenced file
67233725
DC
1873 * @return int
1874 */
0ad654dc 1875 public function search_references_count($reference) {
67233725 1876 global $DB;
0ad654dc
DM
1877
1878 if (is_null($reference)) {
1879 throw new coding_exception('NULL is not a valid reference to an external file');
1880 }
1881
483afa44
DM
1882 // Give {@link self::unpack_reference()} a chance to throw exception if the
1883 // reference is not in a valid format.
1884 self::unpack_reference($reference);
1885
0ad654dc
DM
1886 $referencehash = sha1($reference);
1887
67233725
DC
1888 $sql = "SELECT COUNT(f.id)
1889 FROM {files} f
0ad654dc
DM
1890 JOIN {files_reference} r ON f.referencefileid = r.id
1891 JOIN {repository_instances} ri ON r.repositoryid = ri.id
1892 WHERE r.referencehash = ?
1893 AND (f.component <> ? OR f.filearea <> ?)";
67233725 1894
483afa44 1895 return (int)$DB->count_records_sql($sql, array($referencehash, 'user', 'draft'));
67233725
DC
1896 }
1897
1898 /**
0ad654dc
DM
1899 * Returns all aliases that link to the given stored_file
1900 *
1901 * Aliases in user draft areas are excluded from the returned list.
67233725
DC
1902 *
1903 * @param stored_file $storedfile
0ad654dc 1904 * @return array of stored_file
67233725 1905 */
0ad654dc 1906 public function get_references_by_storedfile(stored_file $storedfile) {
67233725
DC
1907 global $DB;
1908
1909 $params = array();
1910 $params['contextid'] = $storedfile->get_contextid();
1911 $params['component'] = $storedfile->get_component();
1912 $params['filearea'] = $storedfile->get_filearea();
1913 $params['itemid'] = $storedfile->get_itemid();
1914 $params['filename'] = $storedfile->get_filename();
1915 $params['filepath'] = $storedfile->get_filepath();
67233725 1916
0ad654dc 1917 return $this->search_references(self::pack_reference($params));
67233725
DC
1918 }
1919
1920 /**
0ad654dc
DM
1921 * Returns the number of aliases that link to the given stored_file
1922 *
1923 * Aliases in user draft areas are not counted.
67233725
DC
1924 *
1925 * @param stored_file $storedfile
1926 * @return int
1927 */
0ad654dc 1928 public function get_references_count_by_storedfile(stored_file $storedfile) {
67233725
DC
1929 global $DB;
1930
1931 $params = array();
1932 $params['contextid'] = $storedfile->get_contextid();
1933 $params['component'] = $storedfile->get_component();
1934 $params['filearea'] = $storedfile->get_filearea();
1935 $params['itemid'] = $storedfile->get_itemid();
1936 $params['filename'] = $storedfile->get_filename();
1937 $params['filepath'] = $storedfile->get_filepath();
67233725 1938
0ad654dc 1939 return $this->search_references_count(self::pack_reference($params));
67233725
DC
1940 }
1941
14b7e500
MG
1942 /**
1943 * Updates all files that are referencing this file with the new contenthash
1944 * and filesize
1945 *
1946 * @param stored_file $storedfile
1947 */
1948 public function update_references_to_storedfile(stored_file $storedfile) {
ff37d63c 1949 global $CFG, $DB;
14b7e500
MG
1950 $params = array();
1951 $params['contextid'] = $storedfile->get_contextid();
1952 $params['component'] = $storedfile->get_component();
1953 $params['filearea'] = $storedfile->get_filearea();
1954 $params['itemid'] = $storedfile->get_itemid();
1955 $params['filename'] = $storedfile->get_filename();
1956 $params['filepath'] = $storedfile->get_filepath();
1957 $reference = self::pack_reference($params);
1958 $referencehash = sha1($reference);
1959
1960 $sql = "SELECT repositoryid, id FROM {files_reference}
898d4975
MG
1961 WHERE referencehash = ?";
1962 $rs = $DB->get_recordset_sql($sql, array($referencehash));
14b7e500
MG
1963
1964 $now = time();
1965 foreach ($rs as $record) {
1966 require_once($CFG->dirroot.'/repository/lib.php');
1967 $repo = repository::get_instance($record->repositoryid);
1968 $lifetime = $repo->get_reference_file_lifetime($reference);
1969 $this->update_references($record->id, $now, $lifetime,
1970 $storedfile->get_contenthash(), $storedfile->get_filesize(), 0);
1971 }
1972 $rs->close();
1973 }
1974
67233725
DC
1975 /**
1976 * Convert file alias to local file
1977 *
bc6f241c
MG
1978 * @throws moodle_exception if file could not be downloaded
1979 *
67233725 1980 * @param stored_file $storedfile a stored_file instances
bc6f241c 1981 * @param int $maxbytes throw an exception if file size is bigger than $maxbytes (0 means no limit)
fc4e8034 1982 * @return stored_file stored_file
67233725 1983 */
bc6f241c 1984 public function import_external_file(stored_file $storedfile, $maxbytes = 0) {
67233725 1985 global $CFG;
bc6f241c 1986 $storedfile->import_external_file_contents($maxbytes);
fc4e8034
DC
1987 $storedfile->delete_reference();
1988 return $storedfile;
67233725
DC
1989 }
1990
8177b7b9
DC
1991 /**
1992 * Return mimetype by given file pathname
1993 *
ae7f35b9
MG
1994 * If file has a known extension, we return the mimetype based on extension.
1995 * Otherwise (when possible) we try to get the mimetype from file contents.
8177b7b9 1996 *
4c2fcbfc
MG
1997 * @param string $pathname full path to the file
1998 * @param string $filename correct file name with extension, if omitted will be taken from $path
8177b7b9
DC
1999 * @return string
2000 */
4c2fcbfc
MG
2001 public static function mimetype($pathname, $filename = null) {
2002 if (empty($filename)) {
2003 $filename = $pathname;
2004 }
2005 $type = mimeinfo('type', $filename);
ae7f35b9 2006 if ($type === 'document/unknown' && class_exists('finfo') && file_exists($pathname)) {
8177b7b9 2007 $finfo = new finfo(FILEINFO_MIME_TYPE);
ae7f35b9 2008 $type = mimeinfo_from_type('type', $finfo->file($pathname));
8177b7b9 2009 }
ae7f35b9 2010 return $type;
8177b7b9
DC
2011 }
2012
172dd12c 2013 /**
2014 * Cron cleanup job.
2015 */
2016 public function cron() {
a881f970 2017 global $CFG, $DB;
bfaed432 2018 require_once($CFG->libdir.'/cronlib.php');
64f93798 2019
2e69ea4a
PS
2020 // find out all stale draft areas (older than 4 days) and purge them
2021 // those are identified by time stamp of the /. root dir
2022 mtrace('Deleting old draft files... ', '');
658b9372 2023 cron_trace_time_and_memory();
2e69ea4a
PS
2024 $old = time() - 60*60*24*4;
2025 $sql = "SELECT *
2026 FROM {files}
2027 WHERE component = 'user' AND filearea = 'draft' AND filepath = '/' AND filename = '.'
2028 AND timecreated < :old";
2029 $rs = $DB->get_recordset_sql($sql, array('old'=>$old));
2030 foreach ($rs as $dir) {
2031 $this->delete_area_files($dir->contextid, $dir->component, $dir->filearea, $dir->itemid);
2032 }
be981316 2033 $rs->close();
b5541735 2034 mtrace('done.');
64f93798 2035
9120a462
DM
2036 // remove orphaned preview files (that is files in the core preview filearea without
2037 // the existing original file)
2038 mtrace('Deleting orphaned preview files... ', '');
658b9372 2039 cron_trace_time_and_memory();
9120a462
DM
2040 $sql = "SELECT p.*
2041 FROM {files} p
2042 LEFT JOIN {files} o ON (p.filename = o.contenthash)
2043 WHERE p.contextid = ? AND p.component = 'core' AND p.filearea = 'preview' AND p.itemid = 0
f7eec6ce 2044 AND o.id IS NULL";
9120a462
DM
2045 $syscontext = context_system::instance();
2046 $rs = $DB->get_recordset_sql($sql, array($syscontext->id));
2047 foreach ($rs as $orphan) {
f7eec6ce
DM
2048 $file = $this->get_file_instance($orphan);
2049 if (!$file->is_directory()) {
2050 $file->delete();
2051 }
9120a462
DM
2052 }
2053 $rs->close();
2054 mtrace('done.');
2055
1aa01caf 2056 // remove trash pool files once a day
2057 // if you want to disable purging of trash put $CFG->fileslastcleanup=time(); into config.php
2058 if (empty($CFG->fileslastcleanup) or $CFG->fileslastcleanup < time() - 60*60*24) {
2059 require_once($CFG->libdir.'/filelib.php');
a881f970
SH
2060 // Delete files that are associated with a context that no longer exists.
2061 mtrace('Cleaning up files from deleted contexts... ', '');
658b9372 2062 cron_trace_time_and_memory();
a881f970
SH
2063 $sql = "SELECT DISTINCT f.contextid
2064 FROM {files} f
2065 LEFT OUTER JOIN {context} c ON f.contextid = c.id
2066 WHERE c.id IS NULL";
be981316
EL
2067 $rs = $DB->get_recordset_sql($sql);
2068 if ($rs->valid()) {
a881f970
SH
2069 $fs = get_file_storage();
2070 foreach ($rs as $ctx) {
2071 $fs->delete_area_files($ctx->contextid);
2072 }
2073 }
be981316 2074 $rs->close();
a881f970
SH
2075 mtrace('done.');
2076
1aa01caf 2077 mtrace('Deleting trash files... ', '');
658b9372 2078 cron_trace_time_and_memory();
1aa01caf 2079 fulldelete($this->trashdir);
2080 set_config('fileslastcleanup', time());
2081 mtrace('done.');
172dd12c 2082 }
2083 }
3447100c
DP
2084
2085 /**
2086 * Get the sql formated fields for a file instance to be created from a
2087 * {files} and {files_refernece} join.
2088 *
2089 * @param string $filesprefix the table prefix for the {files} table
2090 * @param string $filesreferenceprefix the table prefix for the {files_reference} table
2091 * @return string the sql to go after a SELECT
2092 */
2093 private static function instance_sql_fields($filesprefix, $filesreferenceprefix) {
2094 // Note, these fieldnames MUST NOT overlap between the two tables,
2095 // else problems like MDL-33172 occur.
2096 $filefields = array('contenthash', 'pathnamehash', 'contextid', 'component', 'filearea',
2097 'itemid', 'filepath', 'filename', 'userid', 'filesize', 'mimetype', 'status', 'source',
42aa6e15 2098 'author', 'license', 'timecreated', 'timemodified', 'sortorder', 'referencefileid');
3447100c 2099
42aa6e15
MG
2100 $referencefields = array('repositoryid' => 'repositoryid',
2101 'reference' => 'reference',
2102 'lastsync' => 'referencelastsync',
2103 'lifetime' => 'referencelifetime');
3447100c
DP
2104
2105 // id is specifically named to prevent overlaping between the two tables.
2106 $fields = array();
2107 $fields[] = $filesprefix.'.id AS id';
2108 foreach ($filefields as $field) {
2109 $fields[] = "{$filesprefix}.{$field}";
2110 }
2111
42aa6e15
MG
2112 foreach ($referencefields as $field => $alias) {
2113 $fields[] = "{$filesreferenceprefix}.{$field} AS {$alias}";
3447100c
DP
2114 }
2115
2116 return implode(', ', $fields);
2117 }
bf9ffe27 2118
d83ce953
DM
2119 /**
2120 * Returns the id of the record in {files_reference} that matches the passed repositoryid and reference
2121 *
2122 * If the record already exists, its id is returned. If there is no such record yet,
2123 * new one is created (using the lastsync and lifetime provided, too) and its id is returned.
2124 *
2125 * @param int $repositoryid
2126 * @param string $reference
2127 * @return int
2128 */
2129 private function get_or_create_referencefileid($repositoryid, $reference, $lastsync = null, $lifetime = null) {
2130 global $DB;
2131
2132 $id = $this->get_referencefileid($repositoryid, $reference, IGNORE_MISSING);
2133
2134 if ($id !== false) {
2135 // bah, that was easy
2136 return $id;
2137 }
2138
2139 // no such record yet, create one
2140 try {
2141 $id = $DB->insert_record('files_reference', array(
2142 'repositoryid' => $repositoryid,
2143 'reference' => $reference,
dccba8bc 2144 'referencehash' => sha1($reference),
d83ce953
DM
2145 'lastsync' => $lastsync,
2146 'lifetime' => $lifetime));
2147 } catch (dml_exception $e) {
2148 // if inserting the new record failed, chances are that the race condition has just
2149 // occured and the unique index did not allow to create the second record with the same
2150 // repositoryid + reference combo
2151 $id = $this->get_referencefileid($repositoryid, $reference, MUST_EXIST);
2152 }
2153
2154 return $id;
2155 }
2156
2157 /**
2158 * Returns the id of the record in {files_reference} that matches the passed parameters
2159 *
2160 * Depending on the required strictness, false can be returned. The behaviour is consistent
2161 * with standard DML methods.
2162 *
2163 * @param int $repositoryid
2164 * @param string $reference
2165 * @param int $strictness either {@link IGNORE_MISSING}, {@link IGNORE_MULTIPLE} or {@link MUST_EXIST}
2166 * @return int|bool
2167 */
2168 private function get_referencefileid($repositoryid, $reference, $strictness) {
2169 global $DB;
2170
0ad654dc
DM
2171 return $DB->get_field('files_reference', 'id',
2172 array('repositoryid' => $repositoryid, 'referencehash' => sha1($reference)), $strictness);
d83ce953 2173 }
14b7e500
MG
2174
2175 /**
2176 * Updates a reference to the external resource and all files that use it
2177 *
2178 * This function is called after synchronisation of an external file and updates the
2179 * contenthash, filesize and status of all files that reference this external file
2180 * as well as time last synchronised and sync lifetime (how long we don't need to call
2181 * synchronisation for this reference).
2182 *
2183 * @param int $referencefileid
2184 * @param int $lastsync
2185 * @param int $lifetime
2186 * @param string $contenthash
2187 * @param int $filesize
2188 * @param int $status 0 if ok or 666 if source is missing
2189 */
2190 public function update_references($referencefileid, $lastsync, $lifetime, $contenthash, $filesize, $status) {
2191 global $DB;
2192 $referencefileid = clean_param($referencefileid, PARAM_INT);
2193 $lastsync = clean_param($lastsync, PARAM_INT);
2194 $lifetime = clean_param($lifetime, PARAM_INT);
2195 validate_param($contenthash, PARAM_TEXT, NULL_NOT_ALLOWED);
2196 $filesize = clean_param($filesize, PARAM_INT);
2197 $status = clean_param($status, PARAM_INT);
2198 $params = array('contenthash' => $contenthash,
2199 'filesize' => $filesize,
2200 'status' => $status,
2201 'referencefileid' => $referencefileid,
2202 'lastsync' => $lastsync,
2203 'lifetime' => $lifetime);
2204 $DB->execute('UPDATE {files} SET contenthash = :contenthash, filesize = :filesize,
2205 status = :status, referencelastsync = :lastsync, referencelifetime = :lifetime
2206 WHERE referencefileid = :referencefileid', $params);
2207 $data = array('id' => $referencefileid, 'lastsync' => $lastsync, 'lifetime' => $lifetime);
2208 $DB->update_record('files_reference', (object)$data);
2209 }
d83ce953 2210}