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