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