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