MDL-14589 course cat areas not writable
[moodle.git] / lib / file / file_storage.php
CommitLineData
172dd12c 1<?php //$Id$
2
3require_once("$CFG->libdir/file/stored_file.php");
4
5class file_storage {
6 private $filedir;
7
8 /**
9 * Contructor
10 * @param string $filedir full path to pool directory
11 */
12 public function __construct() {
13 global $CFG;
14 if (isset($CFG->filedir)) {
15 $this->filedir = $CFG->filedir;
16 } else {
17 $this->filedir = $CFG->dataroot.'/filedir';
18 }
19
20 // make sure the file pool directory exists
21 if (!is_dir($this->filedir)) {
22 if (!check_dir_exists($this->filedir, true, true)) {
23 throw new file_exception('localfilecannotcreatefiledirs'); // permission trouble
24 }
25 // place warning file in file pool root
17d9269f 26 file_put_contents($this->filedir.'/warning.txt',
6c0e2d08 27 '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.');
172dd12c 28 }
29 }
30
31 /**
32 * Calculates sha1 hash of unique full path name information
33 * @param int $contextid
34 * @param string $filearea
35 * @param int $itemid
36 * @param string $filepath
37 * @param string $filename
38 * @return string
39 */
40 public static function get_pathname_hash($contextid, $filearea, $itemid, $filepath, $filename) {
41 return sha1($contextid.$filearea.$itemid.$filepath.$filename);
42 }
43
44 /**
45 * Does this file exist?
46 * @param int $contextid
47 * @param string $filearea
48 * @param int $itemid
49 * @param string $filepath
50 * @param string $filename
51 * @return bool
52 */
53 public function file_exists($contextid, $filearea, $itemid, $filepath, $filename) {
54 $filepath = clean_param($filepath, PARAM_PATH);
55 $filename = clean_param($filename, PARAM_FILE);
56
57 if ($filename === '') {
58 $filename = '.';
59 }
60
61 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, $filename);
62 return $this->file_exists_by_hash($pathnamehash);
63 }
64
65 /**
66 * Does this file exist?
67 * @param string $pathnamehash
68 * @return bool
69 */
70 public function file_exists_by_hash($pathnamehash) {
71 global $DB;
72
73 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
74 }
75
76 /**
77 * Fetch file using local file id
78 * @param int $fileid
79 * @return mixed stored_file instance if exists, false if not
80 */
81 public function get_file_by_id($fileid) {
82 global $DB;
83
84 if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
85 return new stored_file($this, $file_record);
86 } else {
87 return false;
88 }
89 }
90
91 /**
92 * Fetch file using local file full pathname hash
93 * @param string $pathnamehash
94 * @return mixed stored_file instance if exists, false if not
95 */
96 public function get_file_by_hash($pathnamehash) {
97 global $DB;
98
99 if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
100 return new stored_file($this, $file_record);
101 } else {
102 return false;
103 }
104 }
105
106 /**
107 * Fetch file
108 * @param int $contextid
109 * @param string $filearea
110 * @param int $itemid
111 * @param string $filepath
112 * @param string $filename
113 * @return mixed stored_file instance if exists, false if not
114 */
115 public function get_file($contextid, $filearea, $itemid, $filepath, $filename) {
116 global $DB;
117
118 $filepath = clean_param($filepath, PARAM_PATH);
119 $filename = clean_param($filename, PARAM_FILE);
120
121 if ($filename === '') {
122 $filename = '.';
123 }
124
125 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, $filename);
126 return $this->get_file_by_hash($pathnamehash);
127 }
128
129 /**
130 * Returns all area files (optionally limited by itemid)
131 * @param int $contextid
132 * @param string $filearea
133 * @param int $itemid (all files if not specified)
134 * @param string $sort
135 * @param bool $includedirs
136 * @return array of stored_files
137 */
138 public function get_area_files($contextid, $filearea, $itemid=false, $sort="itemid, filepath, filename", $inludedirs=true) {
139 global $DB;
140
141 $conditions = array('contextid'=>$contextid, 'filearea'=>$filearea);
142 if ($itemid !== false) {
143 $conditions['itemid'] = $itemid;
144 }
145
146 $result = array();
147 $file_records = $DB->get_records('files', $conditions, $sort);
148 foreach ($file_records as $file_record) {
149 if (!$inludedirs and $file_record->filename === '.') {
150 continue;
151 }
152 $result[] = new stored_file($this, $file_record);
153 }
154 return $result;
155 }
156
157 /**
158 * Delete all area files (optionally limited by itemid)
159 * @param int $contextid
160 * @param string $filearea
161 * @param int $itemid (all files if not specified)
162 * @return success
163 */
164 public function delete_area_files($contextid, $filearea, $itemid=false) {
165 global $DB;
166
167 $conditions = array('contextid'=>$contextid, 'filearea'=>$filearea);
168 if ($itemid !== false) {
169 $conditions['itemid'] = $itemid;
170 }
171
172 $success = true;
173
174 $file_records = $DB->get_records('files', $conditions);
175 foreach ($file_records as $file_record) {
176 $stored_file = new stored_file($this, $file_record);
177 $success = $stored_file->delete() && $success;
178 }
179
180 return $success;
181 }
182
183 /**
184 * Recursively creates director
185 * @param int $contextid
186 * @param string $filearea
187 * @param int $itemid
188 * @param string $filepath
189 * @param string $filename
190 * @return bool success
191 */
192 public function create_directory($contextid, $filearea, $itemid, $filepath, $userid=null) {
193 global $DB;
194
195 // validate all parameters, we do not want any rubbish stored in database, right?
196 if (!is_number($contextid) or $contextid < 1) {
197 throw new file_exception('localfileproblem', 'Invalid contextid');
198 }
199
6c0e2d08 200 $filearea = clean_param($filearea, PARAM_ALPHAEXT);
172dd12c 201 if ($filearea === '') {
202 throw new file_exception('localfileproblem', 'Invalid filearea');
203 }
204
205 if (!is_number($itemid) or $itemid < 0) {
206 throw new file_exception('localfileproblem', 'Invalid itemid');
207 }
208
209 $filepath = clean_param($filepath, PARAM_PATH);
210 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
211 // path must start and end with '/'
212 throw new file_exception('localfileproblem', 'Invalid file path');
213 }
214
215 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, '.');
216
217 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
218 return $dir_info;
219 }
220
221 static $contenthash = null;
222 if (!$contenthash) {
223 $this->add_to_pool_string('');
224 $contenthash = sha1('');
225 }
226
227 $now = time();
228
229 $dir_record = new object();
230 $dir_record->contextid = $contextid;
231 $dir_record->filearea = $filearea;
232 $dir_record->itemid = $itemid;
233 $dir_record->filepath = $filepath;
234 $dir_record->filename = '.';
235 $dir_record->contenthash = $contenthash;
236 $dir_record->filesize = 0;
237
238 $dir_record->timecreated = $now;
239 $dir_record->timemodified = $now;
240 $dir_record->mimetype = null;
241 $dir_record->userid = $userid;
242
243 $dir_record->pathnamehash = $pathnamehash;
244
245 $DB->insert_record('files', $dir_record);
246 $dir_info = $this->get_file_by_hash($pathnamehash);
247
248 if ($filepath !== '/') {
249 //recurse to parent dirs
250 $filepath = trim($filepath, '/');
251 $filepath = explode('/', $filepath);
252 array_pop($filepath);
253 $filepath = implode('/', $filepath);
254 $filepath = ($filepath === '') ? '/' : "/$filepath/";
255 $this->create_directory($contextid, $filearea, $itemid, $filepath, $userid);
256 }
257
258 return $dir_info;
259 }
260
261 /**
262 * Add new local file based on existing local file
263 * @param mixed $file_record object or array describing changes
264 * @param int $fid id of existing local file
265 * @return object stored_file instance
266 */
267 public function create_file_from_localfile($file_record, $fid) {
268 global $DB;
269
270 $file_record = (array)$file_record; // we support arrays too
271 unset($file_record['id']);
272 unset($file_record['filesize']);
273 unset($file_record['contenthash']);
274
275 $now = time();
276
277 if ($newrecord = $DB->get_record('files', array('id'=>$fid))) {
278 throw new file_exception('localfileproblem', 'File does not exist');
279 }
280
281 unset($newrecord->id);
282
283 foreach ($file_record as $key=>$value) {
284 // validate all parameters, we do not want any rubbish stored in database, right?
285 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
286 throw new file_exception('localfileproblem', 'Invalid contextid');
287 }
288
289 if ($key == 'filearea') {
6c0e2d08 290 $value = clean_param($value, PARAM_ALPHAEXT);
172dd12c 291 if ($value === '') {
292 throw new file_exception('localfileproblem', 'Invalid filearea');
293 }
294 }
295
296 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
297 throw new file_exception('localfileproblem', 'Invalid itemid');
298 }
299
300
301 if ($key == 'filepath') {
302 $value = clean_param($value, PARAM_PATH);
303 if (strpos($value, '/') !== 0 or strpos($value, '/') !== strlen($value)-1) {
304 // path must start and end with '/'
305 throw new file_exception('localfileproblem', 'Invalid file path');
306 }
307 }
308
309 if ($key == 'filename') {
310 $value = clean_param($value, PARAM_FILE);
311 if ($value === '') {
312 // path must start and end with '/'
313 throw new file_exception('localfileproblem', 'Invalid file name');
314 }
315 }
316
317 $newrecord->$key = $value;
318 }
319
320 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
321
322 try {
323 $newrecord->id = $DB->insert_record('files', $newrecord);
324 } catch (database_exception $e) {
325 $newrecord->id = false;
326 }
327
328 if (!$newrecord->id) {
329 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
330 $newrecord->filepath, $newrecord->filename);
331 }
332
333 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
334
335 return new stored_file($this, $newrecord);
336 }
337
338 /**
339 * Add new local file
340 * @param mixed $file_record object or array describing file
341 * @param string $path path to file or content of file
342 * @return object stored_file instance
343 */
344 public function create_file_from_pathname($file_record, $pathname) {
345 global $DB;
346
347 $file_record = (object)$file_record; // we support arrays too
348
349 // validate all parameters, we do not want any rubbish stored in database, right?
350 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
351 throw new file_exception('localfileproblem', 'Invalid contextid');
352 }
353
6c0e2d08 354 $file_record->filearea = clean_param($file_record->filearea, PARAM_ALPHAEXT);
172dd12c 355 if ($file_record->filearea === '') {
356 throw new file_exception('localfileproblem', 'Invalid filearea');
357 }
358
359 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
360 throw new file_exception('localfileproblem', 'Invalid itemid');
361 }
362
363 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
364 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
365 // path must start and end with '/'
366 throw new file_exception('localfileproblem', 'Invalid file path');
367 }
368
369 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
370 if ($file_record->filename === '') {
371 // path must start and end with '/'
372 throw new file_exception('localfileproblem', 'Invalid file name');
373 }
374
375 $now = time();
376
377 $newrecord = new object();
378
379 $newrecord->contextid = $file_record->contextid;
380 $newrecord->filearea = $file_record->filearea;
381 $newrecord->itemid = $file_record->itemid;
382 $newrecord->filepath = $file_record->filepath;
383 $newrecord->filename = $file_record->filename;
384
385 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
386 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
387 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
388 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
389
390 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_to_pool_pathname($pathname);
391
392 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
393
394 try {
395 $newrecord->id = $DB->insert_record('files', $newrecord);
396 } catch (database_exception $e) {
397 $newrecord->id = false;
398 }
399
400 if (!$newrecord->id) {
401 if ($newfile) {
402 $this->mark_delete_candidate($newrecord->contenthash);
403 }
404 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
405 $newrecord->filepath, $newrecord->filename);
406 }
407
408 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
409
410 return new stored_file($this, $newrecord);
411 }
412
413 /**
414 * Add new local file
415 * @param mixed $file_record object or array describing file
416 * @param string $content content of file
417 * @return object stored_file instance
418 */
419 public function create_file_from_string($file_record, $content) {
420 global $DB;
421
422 $file_record = (object)$file_record; // we support arrays too
423
424 // validate all parameters, we do not want any rubbish stored in database, right?
425 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
426 throw new file_exception('localfileproblem', 'Invalid contextid');
427 }
428
6c0e2d08 429 $file_record->filearea = clean_param($file_record->filearea, PARAM_ALPHAEXT);
172dd12c 430 if ($file_record->filearea === '') {
431 throw new file_exception('localfileproblem', 'Invalid filearea');
432 }
433
434 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
435 throw new file_exception('localfileproblem', 'Invalid itemid');
436 }
437
438 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
439 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
440 // path must start and end with '/'
441 throw new file_exception('localfileproblem', 'Invalid file path');
442 }
443
444 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
445 if ($file_record->filename === '') {
446 // path must start and end with '/'
447 throw new file_exception('localfileproblem', 'Invalid file name');
448 }
449
450 $now = time();
451
452 $newrecord = new object();
453
454 $newrecord->contextid = $file_record->contextid;
455 $newrecord->filearea = $file_record->filearea;
456 $newrecord->itemid = $file_record->itemid;
457 $newrecord->filepath = $file_record->filepath;
458 $newrecord->filename = $file_record->filename;
459
460 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
461 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
462 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
463 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
464
465 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_to_pool_string($content);
466
467 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
468
469 try {
470 $newrecord->id = $DB->insert_record('files', $newrecord);
471 } catch (database_exception $e) {
472 $newrecord->id = false;
473 }
474
475 if (!$newrecord->id) {
476 if ($newfile) {
477 $this->mark_delete_candidate($newrecord->contenthash);
478 }
479 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
480 $newrecord->filepath, $newrecord->filename);
481 }
482
483 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
484
485 return new stored_file($this, $newrecord);
486 }
487
488 /**
489 * Add file content to sha1 pool
490 * @param string $pathname path to file
491 * @param string sha1 hash of content if known (performance only)
492 * @return array(contenthash, filesize, newfile)
493 */
494 public function add_to_pool_pathname($pathname, $contenthash=null) {
495 if (!is_readable($pathname)) {
496 throw new file_exception('localfilecannotread');
497 }
498
499 if (is_null($contenthash)) {
500 $contenthash = sha1_file($pathname);
501 }
502
503 $filesize = filesize($pathname);
504
505 $hashpath = $this->path_from_hash($contenthash);
506 $hashfile = "$hashpath/$contenthash";
507
508 if (file_exists($hashfile)) {
509 if (filesize($hashfile) !== $filesize) {
510 throw new file_pool_content_exception($contenthash);
511 }
512 $newfile = false;
513
514 } else {
515 if (!check_dir_exists($hashpath, true, true)) {
516 throw new file_exception('localfilecannotcreatefiledirs'); // permission trouble
517 }
518 $newfile = true;
519
6c0e2d08 520 if (!copy($pathname, $hashfile)) {
521 throw new file_exception('localfilecannotread');
172dd12c 522 }
172dd12c 523
524 if (filesize($hashfile) !== $filesize) {
525 @unlink($hashfile);
526 throw new file_pool_content_exception($contenthash);
527 }
528 }
529
530
531 return array($contenthash, $filesize, $newfile);
532 }
533
534 /**
535 * Add string content to sha1 pool
536 * @param string $content file content - binary string
537 * @return array(contenthash, filesize, newfile)
538 */
539 public function add_to_pool_string($content) {
540 $contenthash = sha1($content);
541 $filesize = strlen($content); // binary length
542
543 $hashpath = $this->path_from_hash($contenthash);
544 $hashfile = "$hashpath/$contenthash";
545
546
547 if (file_exists($hashfile)) {
548 if (filesize($hashfile) !== $filesize) {
549 throw new file_pool_content_exception($contenthash);
550 }
551 $newfile = false;
552
553 } else {
554 if (!check_dir_exists($hashpath, true, true)) {
555 throw new file_exception('localfilecannotcreatefiledirs'); // permission trouble
556 }
557 $newfile = true;
558
6c0e2d08 559 file_put_contents($hashfile, $content);
172dd12c 560
561 if (filesize($hashfile) !== $filesize) {
562 @unlink($hashfile);
563 throw new file_pool_content_exception($contenthash);
564 }
565 }
566
567 return array($contenthash, $filesize, $newfile);
568 }
569
570 /**
571 * Return path to file with given hash
572 *
17d9269f 573 * NOTE: must not be public, files in pool must not be modified
172dd12c 574 *
575 * @param string $contenthash
576 * @return string expected file location
577 */
17d9269f 578 protected function path_from_hash($contenthash) {
172dd12c 579 $l1 = $contenthash[0].$contenthash[1];
580 $l2 = $contenthash[2].$contenthash[3];
581 $l3 = $contenthash[4].$contenthash[5];
582 return "$this->filedir/$l1/$l2/$l3";
583 }
584
585 /**
586 * Marks pool file as candidate for deleting
587 * @param string $contenthash
588 */
589 public function mark_delete_candidate($contenthash) {
590 global $DB;
591
592 if ($DB->record_exists('files_cleanup', array('contenthash'=>$contenthash))) {
593 return;
594 }
595 $rec = new object();
596 $rec->contenthash = $contenthash;
597 $DB->insert_record('files_cleanup', $rec);
598 }
599
600 /**
601 * Cron cleanup job.
602 */
603 public function cron() {
604 global $DB;
605
606 //TODO: there is a small chance that reused files might be deleted
607 // if this function takes too long we should add some table locking here
608
609 $sql = "SELECT 1 AS id, fc.contenthash
610 FROM {files_cleanup} fc
611 LEFT JOIN {files} f ON f.contenthash = fc.contenthash
612 WHERE f.id IS NULL";
613 while ($hash = $DB->get_record_sql($sql, null, true)) {
614 $file = $this->path_from_hash($hash->contenthash).'/'.$hash->contenthash;
615 @unlink($file);
616 $DB->delete_records('files_cleanup', array('contenthash'=>$hash->contenthash));
617 }
618 }
619}