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