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