gradelib MDL-19376 Fixed full view link
[moodle.git] / lib / file / file_storage.php
CommitLineData
25aebf09 1<?php
2
3// This file is part of Moodle - http://moodle.org/
4//
5// Moodle is free software: you can redistribute it and/or modify
6// it under the terms of the GNU General Public License as published by
7// the Free Software Foundation, either version 3 of the License, or
8// (at your option) any later version.
9//
10// Moodle is distributed in the hope that it will be useful,
11// but WITHOUT ANY WARRANTY; without even the implied warranty of
12// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13// GNU General Public License for more details.
14//
15// You should have received a copy of the GNU General Public License
16// along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17
18
19/**
20 * Core file storage class definition.
21 *
64a19b38 22 * @package moodlecore
23 * @subpackage file-storage
24 * @copyright 2008 Petr Skoda (http://skodak.org)
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
25aebf09 26 */
172dd12c 27
28require_once("$CFG->libdir/file/stored_file.php");
29
25aebf09 30/**
31 * File storage class used for low level access to stored files.
32 * Only owner of file area may use this class to access own files,
33 * for example only code in mod/assignment/* may access assignment
34 * attachments. When core needs to access files of modules it has
4cd9dfda 35 * to use file_browser class instead.
25aebf09 36 */
172dd12c 37class file_storage {
38 private $filedir;
39
40 /**
41 * Contructor
42 * @param string $filedir full path to pool directory
43 */
744b64ff 44 public function __construct($filedir) {
45 $this->filedir = $filedir;
172dd12c 46
47 // make sure the file pool directory exists
48 if (!is_dir($this->filedir)) {
49 if (!check_dir_exists($this->filedir, true, true)) {
145a0a31 50 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
172dd12c 51 }
52 // place warning file in file pool root
17d9269f 53 file_put_contents($this->filedir.'/warning.txt',
6c0e2d08 54 '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 55 }
56 }
57
744b64ff 58 /**
59 * Returns location of filedir (file pool)
25aebf09 60 * Do not use, this method is intended for stored_file instances.
6e73ac42 61 * @return string pathname
744b64ff 62 */
63 public function get_filedir() {
64 return $this->filedir;
65 }
66
172dd12c 67 /**
25aebf09 68 * Calculates sha1 hash of unique full path name information,
69 * this hash is a unique file identifier. This improves performance
70 * and overcomes db index size limits.
172dd12c 71 * @param int $contextid
72 * @param string $filearea
73 * @param int $itemid
74 * @param string $filepath
75 * @param string $filename
76 * @return string
77 */
78 public static function get_pathname_hash($contextid, $filearea, $itemid, $filepath, $filename) {
79 return sha1($contextid.$filearea.$itemid.$filepath.$filename);
80 }
81
82 /**
83 * Does this file exist?
84 * @param int $contextid
85 * @param string $filearea
86 * @param int $itemid
87 * @param string $filepath
88 * @param string $filename
89 * @return bool
90 */
91 public function file_exists($contextid, $filearea, $itemid, $filepath, $filename) {
92 $filepath = clean_param($filepath, PARAM_PATH);
93 $filename = clean_param($filename, PARAM_FILE);
94
95 if ($filename === '') {
96 $filename = '.';
97 }
98
99 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, $filename);
100 return $this->file_exists_by_hash($pathnamehash);
101 }
102
103 /**
104 * Does this file exist?
105 * @param string $pathnamehash
106 * @return bool
107 */
108 public function file_exists_by_hash($pathnamehash) {
109 global $DB;
110
111 return $DB->record_exists('files', array('pathnamehash'=>$pathnamehash));
112 }
113
114 /**
25aebf09 115 * Fetch file using local file id.
116 * Please do not rely on file ids, it is usually easier to use
117 * pathname hashes instead.
172dd12c 118 * @param int $fileid
119 * @return mixed stored_file instance if exists, false if not
120 */
121 public function get_file_by_id($fileid) {
122 global $DB;
123
124 if ($file_record = $DB->get_record('files', array('id'=>$fileid))) {
125 return new stored_file($this, $file_record);
126 } else {
127 return false;
128 }
129 }
130
131 /**
132 * Fetch file using local file full pathname hash
133 * @param string $pathnamehash
134 * @return mixed stored_file instance if exists, false if not
135 */
136 public function get_file_by_hash($pathnamehash) {
137 global $DB;
138
139 if ($file_record = $DB->get_record('files', array('pathnamehash'=>$pathnamehash))) {
140 return new stored_file($this, $file_record);
141 } else {
142 return false;
143 }
144 }
145
146 /**
25aebf09 147 * Fetch localy stored file.
172dd12c 148 * @param int $contextid
149 * @param string $filearea
150 * @param int $itemid
151 * @param string $filepath
152 * @param string $filename
153 * @return mixed stored_file instance if exists, false if not
154 */
155 public function get_file($contextid, $filearea, $itemid, $filepath, $filename) {
156 global $DB;
157
158 $filepath = clean_param($filepath, PARAM_PATH);
159 $filename = clean_param($filename, PARAM_FILE);
160
161 if ($filename === '') {
162 $filename = '.';
163 }
164
165 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, $filename);
166 return $this->get_file_by_hash($pathnamehash);
167 }
168
169 /**
170 * Returns all area files (optionally limited by itemid)
171 * @param int $contextid
172 * @param string $filearea
173 * @param int $itemid (all files if not specified)
174 * @param string $sort
175 * @param bool $includedirs
cd5be217 176 * @return array of stored_files indexed by pathanmehash
172dd12c 177 */
46fcbcf4 178 public function get_area_files($contextid, $filearea, $itemid=false, $sort="itemid, filepath, filename", $includedirs=true) {
172dd12c 179 global $DB;
180
181 $conditions = array('contextid'=>$contextid, 'filearea'=>$filearea);
182 if ($itemid !== false) {
183 $conditions['itemid'] = $itemid;
184 }
185
186 $result = array();
187 $file_records = $DB->get_records('files', $conditions, $sort);
188 foreach ($file_records as $file_record) {
46fcbcf4 189 if (!$includedirs and $file_record->filename === '.') {
172dd12c 190 continue;
191 }
cd5be217 192 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
172dd12c 193 }
194 return $result;
195 }
196
ee03a651 197 /**
198 * Returns all files and otionally directories
199 * @param int $contextid
200 * @param string $filearea
201 * @param int $itemid
202 * @param int $filepath directory path
203 * @param bool $recursive include all subdirectories
46fcbcf4 204 * @param bool $includedirs include files and directories
ee03a651 205 * @param string $sort
cd5be217 206 * @return array of stored_files indexed by pathanmehash
ee03a651 207 */
46fcbcf4 208 public function get_directory_files($contextid, $filearea, $itemid, $filepath, $recursive=false, $includedirs=true, $sort="filepath, filename") {
ee03a651 209 global $DB;
210
211 if (!$directory = $this->get_file($contextid, $filearea, $itemid, $filepath, '.')) {
212 return array();
213 }
214
215 if ($recursive) {
216
46fcbcf4 217 $dirs = $includedirs ? "" : "AND filename <> '.'";
ee03a651 218 $length = textlib_get_instance()->strlen($filepath);
219
220 $sql = "SELECT *
221 FROM {files}
222 WHERE contextid = :contextid AND filearea = :filearea AND itemid = :itemid
655bbf51 223 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 224 AND id <> :dirid
225 $dirs
226 ORDER BY $sort";
227 $params = array('contextid'=>$contextid, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
228
229 $files = array();
230 $dirs = array();
231 $file_records = $DB->get_records_sql($sql, $params);
232 foreach ($file_records as $file_record) {
233 if ($file_record->filename == '.') {
cd5be217 234 $dirs[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 235 } else {
cd5be217 236 $files[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 237 }
238 }
239 $result = array_merge($dirs, $files);
240
241 } else {
242 $result = array();
243 $params = array('contextid'=>$contextid, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'dirid'=>$directory->get_id());
244
245 $length = textlib_get_instance()->strlen($filepath);
246
46fcbcf4 247 if ($includedirs) {
ee03a651 248 $sql = "SELECT *
249 FROM {files}
250 WHERE contextid = :contextid AND filearea = :filearea
251 AND itemid = :itemid AND filename = '.'
655bbf51 252 AND ".$DB->sql_substr("filepath", 1, $length)." = :filepath
ee03a651 253 AND id <> :dirid
254 ORDER BY $sort";
255 $reqlevel = substr_count($filepath, '/') + 1;
256 $file_records = $DB->get_records_sql($sql, $params);
257 foreach ($file_records as $file_record) {
258 if (substr_count($file_record->filepath, '/') !== $reqlevel) {
259 continue;
260 }
cd5be217 261 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 262 }
263 }
264
265 $sql = "SELECT *
266 FROM {files}
267 WHERE contextid = :contextid AND filearea = :filearea AND itemid = :itemid
268 AND filepath = :filepath AND filename <> '.'
269 ORDER BY $sort";
270
271 $file_records = $DB->get_records_sql($sql, $params);
272 foreach ($file_records as $file_record) {
cd5be217 273 $result[$file_record->pathnamehash] = new stored_file($this, $file_record);
ee03a651 274 }
275 }
276
277 return $result;
278 }
279
172dd12c 280 /**
281 * Delete all area files (optionally limited by itemid)
282 * @param int $contextid
6311eb61 283 * @param string $filearea (all areas in context if not specified)
172dd12c 284 * @param int $itemid (all files if not specified)
285 * @return success
286 */
6311eb61 287 public function delete_area_files($contextid, $filearea=false, $itemid=false) {
172dd12c 288 global $DB;
289
6311eb61 290 $conditions = array('contextid'=>$contextid);
291 if ($filearea !== false) {
292 $conditions['filearea'] = $filearea;
293 }
172dd12c 294 if ($itemid !== false) {
295 $conditions['itemid'] = $itemid;
296 }
297
298 $success = true;
299
300 $file_records = $DB->get_records('files', $conditions);
301 foreach ($file_records as $file_record) {
302 $stored_file = new stored_file($this, $file_record);
303 $success = $stored_file->delete() && $success;
304 }
305
306 return $success;
307 }
308
309 /**
25aebf09 310 * Recursively creates directory
172dd12c 311 * @param int $contextid
312 * @param string $filearea
313 * @param int $itemid
314 * @param string $filepath
315 * @param string $filename
316 * @return bool success
317 */
318 public function create_directory($contextid, $filearea, $itemid, $filepath, $userid=null) {
319 global $DB;
320
321 // validate all parameters, we do not want any rubbish stored in database, right?
322 if (!is_number($contextid) or $contextid < 1) {
145a0a31 323 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 324 }
325
6c0e2d08 326 $filearea = clean_param($filearea, PARAM_ALPHAEXT);
172dd12c 327 if ($filearea === '') {
145a0a31 328 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 329 }
330
331 if (!is_number($itemid) or $itemid < 0) {
145a0a31 332 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 333 }
334
335 $filepath = clean_param($filepath, PARAM_PATH);
336 if (strpos($filepath, '/') !== 0 or strrpos($filepath, '/') !== strlen($filepath)-1) {
337 // path must start and end with '/'
145a0a31 338 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 339 }
340
341 $pathnamehash = $this->get_pathname_hash($contextid, $filearea, $itemid, $filepath, '.');
342
343 if ($dir_info = $this->get_file_by_hash($pathnamehash)) {
344 return $dir_info;
345 }
346
347 static $contenthash = null;
348 if (!$contenthash) {
b48f3e06 349 $this->add_string_to_pool('');
172dd12c 350 $contenthash = sha1('');
351 }
352
353 $now = time();
354
355 $dir_record = new object();
356 $dir_record->contextid = $contextid;
357 $dir_record->filearea = $filearea;
358 $dir_record->itemid = $itemid;
359 $dir_record->filepath = $filepath;
360 $dir_record->filename = '.';
361 $dir_record->contenthash = $contenthash;
362 $dir_record->filesize = 0;
363
364 $dir_record->timecreated = $now;
365 $dir_record->timemodified = $now;
366 $dir_record->mimetype = null;
367 $dir_record->userid = $userid;
368
369 $dir_record->pathnamehash = $pathnamehash;
370
371 $DB->insert_record('files', $dir_record);
372 $dir_info = $this->get_file_by_hash($pathnamehash);
373
374 if ($filepath !== '/') {
375 //recurse to parent dirs
376 $filepath = trim($filepath, '/');
377 $filepath = explode('/', $filepath);
378 array_pop($filepath);
379 $filepath = implode('/', $filepath);
380 $filepath = ($filepath === '') ? '/' : "/$filepath/";
381 $this->create_directory($contextid, $filearea, $itemid, $filepath, $userid);
382 }
383
384 return $dir_info;
385 }
386
387 /**
388 * Add new local file based on existing local file
389 * @param mixed $file_record object or array describing changes
390 * @param int $fid id of existing local file
391 * @return object stored_file instance
392 */
3501d96b 393 public function create_file_from_storedfile($file_record, $fid) {
172dd12c 394 global $DB;
395
8eb1e0a1 396 if ($fid instanceof stored_file) {
397 $fid = $fid->get_id();
398 }
399
ec8b711f 400 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
401
172dd12c 402 unset($file_record['id']);
403 unset($file_record['filesize']);
404 unset($file_record['contenthash']);
8eb1e0a1 405 unset($file_record['pathnamehash']);
172dd12c 406
407 $now = time();
408
ebcac6c6 409 if (!$newrecord = $DB->get_record('files', array('id'=>$fid))) {
145a0a31 410 throw new file_exception('storedfileproblem', 'File does not exist');
172dd12c 411 }
412
413 unset($newrecord->id);
414
415 foreach ($file_record as $key=>$value) {
416 // validate all parameters, we do not want any rubbish stored in database, right?
417 if ($key == 'contextid' and (!is_number($value) or $value < 1)) {
145a0a31 418 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 419 }
420
421 if ($key == 'filearea') {
6c0e2d08 422 $value = clean_param($value, PARAM_ALPHAEXT);
172dd12c 423 if ($value === '') {
145a0a31 424 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 425 }
426 }
427
428 if ($key == 'itemid' and (!is_number($value) or $value < 0)) {
145a0a31 429 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 430 }
431
432
433 if ($key == 'filepath') {
434 $value = clean_param($value, PARAM_PATH);
00c32c54 435 if (strpos($value, '/') !== 0 or strrpos($value, '/') !== strlen($value)-1) {
172dd12c 436 // path must start and end with '/'
145a0a31 437 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 438 }
439 }
440
441 if ($key == 'filename') {
442 $value = clean_param($value, PARAM_FILE);
443 if ($value === '') {
444 // path must start and end with '/'
145a0a31 445 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 446 }
447 }
448
449 $newrecord->$key = $value;
450 }
451
452 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
453
cd5be217 454 if ($newrecord->filename === '.') {
455 // special case - only this function supports directories ;-)
456 $directory = $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
457 // update the existing directory with the new data
458 $newrecord->id = $directory->get_id();
459 if (!$DB->update_record('files', $newrecord)) {
460 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
461 $newrecord->filepath, $newrecord->filename);
462 }
463 return new stored_file($this, $newrecord);
464 }
465
172dd12c 466 try {
467 $newrecord->id = $DB->insert_record('files', $newrecord);
468 } catch (database_exception $e) {
469 $newrecord->id = false;
470 }
471
472 if (!$newrecord->id) {
473 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
cd5be217 474 $newrecord->filepath, $newrecord->filename);
172dd12c 475 }
476
477 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
478
479 return new stored_file($this, $newrecord);
480 }
481
6e73ac42 482 /**
483 * Add new local file
484 * @param mixed $file_record object or array describing file
485 * @param string $path path to file or content of file
486 * @param array $options @see download_file_content() options
487 * @return object stored_file instance
488 */
489 public function create_file_from_url($file_record, $url, $options=null) {
ec8b711f 490
491 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
6e73ac42 492 $file_record = (object)$file_record; // we support arrays too
493
494 $headers = isset($options['headers']) ? $options['headers'] : null;
495 $postdata = isset($options['postdata']) ? $options['postdata'] : null;
496 $fullresponse = isset($options['fullresponse']) ? $options['fullresponse'] : false;
497 $timeout = isset($options['timeout']) ? $options['timeout'] : 300;
498 $connecttimeout = isset($options['connecttimeout']) ? $options['connecttimeout'] : 20;
499 $skipcertverify = isset($options['skipcertverify']) ? $options['skipcertverify'] : false;
500
501 // TODO: it might be better to add a new option to file file content to temp file,
502 // the problem here is that the size of file is limited by available memory
503
504 $content = download_file_content($url, $headers, $postdata, $fullresponse, $timeout, $connecttimeout, $skipcertverify);
505
506 if (!isset($file_record->filename)) {
507 $parts = explode('/', $url);
508 $filename = array_pop($parts);
509 $file_record->filename = clean_param($filename, PARAM_FILE);
510 }
511
512 return $this->create_file_from_string($file_record, $content);
513 }
514
172dd12c 515 /**
516 * Add new local file
517 * @param mixed $file_record object or array describing file
518 * @param string $path path to file or content of file
519 * @return object stored_file instance
520 */
521 public function create_file_from_pathname($file_record, $pathname) {
522 global $DB;
523
ec8b711f 524 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 525 $file_record = (object)$file_record; // we support arrays too
526
527 // validate all parameters, we do not want any rubbish stored in database, right?
528 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 529 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 530 }
531
6c0e2d08 532 $file_record->filearea = clean_param($file_record->filearea, PARAM_ALPHAEXT);
172dd12c 533 if ($file_record->filearea === '') {
145a0a31 534 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 535 }
536
537 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 538 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 539 }
540
541 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
542 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
543 // path must start and end with '/'
145a0a31 544 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 545 }
546
547 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
548 if ($file_record->filename === '') {
e1dcb950 549 // filename must not be empty
145a0a31 550 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 551 }
552
553 $now = time();
554
555 $newrecord = new object();
556
557 $newrecord->contextid = $file_record->contextid;
558 $newrecord->filearea = $file_record->filearea;
559 $newrecord->itemid = $file_record->itemid;
560 $newrecord->filepath = $file_record->filepath;
561 $newrecord->filename = $file_record->filename;
562
563 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
564 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
565 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
566 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
567
b48f3e06 568 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_file_to_pool($pathname);
172dd12c 569
570 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
571
572 try {
573 $newrecord->id = $DB->insert_record('files', $newrecord);
574 } catch (database_exception $e) {
575 $newrecord->id = false;
576 }
577
578 if (!$newrecord->id) {
579 if ($newfile) {
580 $this->mark_delete_candidate($newrecord->contenthash);
581 }
582 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
583 $newrecord->filepath, $newrecord->filename);
584 }
585
586 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
587
588 return new stored_file($this, $newrecord);
589 }
590
591 /**
592 * Add new local file
593 * @param mixed $file_record object or array describing file
594 * @param string $content content of file
595 * @return object stored_file instance
596 */
597 public function create_file_from_string($file_record, $content) {
598 global $DB;
599
ec8b711f 600 $file_record = (array)$file_record; //do not modify the submitted record, this cast unlinks objects
172dd12c 601 $file_record = (object)$file_record; // we support arrays too
602
603 // validate all parameters, we do not want any rubbish stored in database, right?
604 if (!is_number($file_record->contextid) or $file_record->contextid < 1) {
145a0a31 605 throw new file_exception('storedfileproblem', 'Invalid contextid');
172dd12c 606 }
607
6c0e2d08 608 $file_record->filearea = clean_param($file_record->filearea, PARAM_ALPHAEXT);
172dd12c 609 if ($file_record->filearea === '') {
145a0a31 610 throw new file_exception('storedfileproblem', 'Invalid filearea');
172dd12c 611 }
612
613 if (!is_number($file_record->itemid) or $file_record->itemid < 0) {
145a0a31 614 throw new file_exception('storedfileproblem', 'Invalid itemid');
172dd12c 615 }
616
617 $file_record->filepath = clean_param($file_record->filepath, PARAM_PATH);
618 if (strpos($file_record->filepath, '/') !== 0 or strrpos($file_record->filepath, '/') !== strlen($file_record->filepath)-1) {
619 // path must start and end with '/'
145a0a31 620 throw new file_exception('storedfileproblem', 'Invalid file path');
172dd12c 621 }
622
623 $file_record->filename = clean_param($file_record->filename, PARAM_FILE);
624 if ($file_record->filename === '') {
625 // path must start and end with '/'
145a0a31 626 throw new file_exception('storedfileproblem', 'Invalid file name');
172dd12c 627 }
628
629 $now = time();
630
631 $newrecord = new object();
632
633 $newrecord->contextid = $file_record->contextid;
634 $newrecord->filearea = $file_record->filearea;
635 $newrecord->itemid = $file_record->itemid;
636 $newrecord->filepath = $file_record->filepath;
637 $newrecord->filename = $file_record->filename;
638
639 $newrecord->timecreated = empty($file_record->timecreated) ? $now : $file_record->timecreated;
640 $newrecord->timemodified = empty($file_record->timemodified) ? $now : $file_record->timemodified;
641 $newrecord->mimetype = empty($file_record->mimetype) ? mimeinfo('type', $file_record->filename) : $file_record->mimetype;
642 $newrecord->userid = empty($file_record->userid) ? null : $file_record->userid;
643
b48f3e06 644 list($newrecord->contenthash, $newrecord->filesize, $newfile) = $this->add_string_to_pool($content);
172dd12c 645
646 $newrecord->pathnamehash = $this->get_pathname_hash($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->filename);
647
648 try {
649 $newrecord->id = $DB->insert_record('files', $newrecord);
650 } catch (database_exception $e) {
651 $newrecord->id = false;
652 }
653
654 if (!$newrecord->id) {
655 if ($newfile) {
656 $this->mark_delete_candidate($newrecord->contenthash);
657 }
658 throw new stored_file_creation_exception($newrecord->contextid, $newrecord->filearea, $newrecord->itemid,
659 $newrecord->filepath, $newrecord->filename);
660 }
661
662 $this->create_directory($newrecord->contextid, $newrecord->filearea, $newrecord->itemid, $newrecord->filepath, $newrecord->userid);
663
664 return new stored_file($this, $newrecord);
665 }
666
797f19e8 667 /**
668 * Creates new image file from existing.
669 * @param mixed $file_record object or array describing new file
670 * @param mixed file id or stored file object
671 * @param int $newwidth in pixels
672 * @param int $newheight in pixels
673 * @param bool $keepaspectratio
674 * @param int $quality depending on image type 0-100 for jpeg, 0-9 (0 means no comppression) for png
675 * @return object stored_file instance
676 */
677 public function convert_image($file_record, $fid, $newwidth=null, $newheight=null, $keepaspectratio=true, $quality=null) {
678 global $DB;
679
680 if ($fid instanceof stored_file) {
681 $fid = $fid->get_id();
682 }
683
684 $file_record = (array)$file_record; // we support arrays too, do not modify the submitted record!
685
686 if (!$file = $this->get_file_by_id($fid)) { // make sure file really exists and we we correct data
687 throw new file_exception('storedfileproblem', 'File does not exist');
688 }
689
690 if (!$imageinfo = $file->get_imageinfo()) {
691 throw new file_exception('storedfileproblem', 'File is not an image');
692 }
693
694 if (!isset($file_record['filename'])) {
695 $file_record['filename'] == $file->get_filename();
696 }
697
698 if (!isset($file_record['mimetype'])) {
699 $file_record['mimetype'] = mimeinfo('type', $file_record['filename']);
700 }
701
702 $width = $imageinfo['width'];
703 $height = $imageinfo['height'];
704 $mimetype = $imageinfo['mimetype'];
705
706 if ($keepaspectratio) {
707 if (0 >= $newwidth and 0 >= $newheight) {
708 // no sizes specified
709 $newwidth = $width;
710 $newheight = $height;
711
712 } else if (0 < $newwidth and 0 < $newheight) {
713 $xheight = ($newwidth*($height/$width));
714 if ($xheight < $newheight) {
715 $newheight = (int)$xheight;
716 } else {
717 $newwidth = (int)($newheight*($width/$height));
718 }
719
720 } else if (0 < $newwidth) {
721 $newheight = (int)($newwidth*($height/$width));
722
723 } else { //0 < $newheight
724 $newwidth = (int)($newheight*($width/$height));
725 }
726
727 } else {
728 if (0 >= $newwidth) {
729 $newwidth = $width;
730 }
731 if (0 >= $newheight) {
732 $newheight = $height;
733 }
734 }
735
736 $img = imagecreatefromstring($file->get_content());
737 if ($height != $newheight or $width != $newwidth) {
738 $newimg = imagecreatetruecolor($newwidth, $newheight);
739 if (!imagecopyresized($newimg, $img, 0, 0, 0, 0, $newwidth, $newheight, $width, $height)) {
740 // weird
741 throw new file_exception('storedfileproblem', 'Can not resize image');
742 }
743 imagedestroy($img);
744 $img = $newimg;
745 }
746
747 ob_start();
748 switch ($file_record['mimetype']) {
749 case 'image/gif':
750 imagegif($img);
751 break;
752
753 case 'image/jpeg':
754 if (is_null($quality)) {
755 imagejpeg($img);
756 } else {
757 imagejpeg($img, NULL, $quality);
758 }
759 break;
760
761 case 'image/png':
8bd49ec0 762 $quality = (int)$quality;
797f19e8 763 imagepng($img, NULL, $quality, NULL);
764 break;
765
766 default:
767 throw new file_exception('storedfileproblem', 'Unsupported mime type');
768 }
769
770 $content = ob_get_contents();
771 ob_end_clean();
772 imagedestroy($img);
773
774 if (!$content) {
775 throw new file_exception('storedfileproblem', 'Can not convert image');
776 }
777
778 return $this->create_file_from_string($file_record, $content);
779 }
780
172dd12c 781 /**
782 * Add file content to sha1 pool
783 * @param string $pathname path to file
784 * @param string sha1 hash of content if known (performance only)
785 * @return array(contenthash, filesize, newfile)
786 */
b48f3e06 787 public function add_file_to_pool($pathname, $contenthash=null) {
172dd12c 788 if (!is_readable($pathname)) {
145a0a31 789 throw new file_exception('storedfilecannotread');
172dd12c 790 }
791
792 if (is_null($contenthash)) {
793 $contenthash = sha1_file($pathname);
794 }
795
796 $filesize = filesize($pathname);
797
798 $hashpath = $this->path_from_hash($contenthash);
799 $hashfile = "$hashpath/$contenthash";
800
801 if (file_exists($hashfile)) {
802 if (filesize($hashfile) !== $filesize) {
803 throw new file_pool_content_exception($contenthash);
804 }
805 $newfile = false;
806
807 } else {
808 if (!check_dir_exists($hashpath, true, true)) {
145a0a31 809 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
172dd12c 810 }
811 $newfile = true;
812
6c0e2d08 813 if (!copy($pathname, $hashfile)) {
145a0a31 814 throw new file_exception('storedfilecannotread');
172dd12c 815 }
172dd12c 816
817 if (filesize($hashfile) !== $filesize) {
818 @unlink($hashfile);
819 throw new file_pool_content_exception($contenthash);
820 }
821 }
822
823
824 return array($contenthash, $filesize, $newfile);
825 }
826
827 /**
828 * Add string content to sha1 pool
829 * @param string $content file content - binary string
830 * @return array(contenthash, filesize, newfile)
831 */
b48f3e06 832 public function add_string_to_pool($content) {
172dd12c 833 $contenthash = sha1($content);
834 $filesize = strlen($content); // binary length
835
836 $hashpath = $this->path_from_hash($contenthash);
837 $hashfile = "$hashpath/$contenthash";
838
839
840 if (file_exists($hashfile)) {
841 if (filesize($hashfile) !== $filesize) {
842 throw new file_pool_content_exception($contenthash);
843 }
844 $newfile = false;
845
846 } else {
847 if (!check_dir_exists($hashpath, true, true)) {
145a0a31 848 throw new file_exception('storedfilecannotcreatefiledirs'); // permission trouble
172dd12c 849 }
850 $newfile = true;
851
6c0e2d08 852 file_put_contents($hashfile, $content);
172dd12c 853
854 if (filesize($hashfile) !== $filesize) {
855 @unlink($hashfile);
856 throw new file_pool_content_exception($contenthash);
857 }
858 }
859
860 return array($contenthash, $filesize, $newfile);
861 }
862
863 /**
864 * Return path to file with given hash
865 *
17d9269f 866 * NOTE: must not be public, files in pool must not be modified
172dd12c 867 *
868 * @param string $contenthash
869 * @return string expected file location
870 */
17d9269f 871 protected function path_from_hash($contenthash) {
172dd12c 872 $l1 = $contenthash[0].$contenthash[1];
873 $l2 = $contenthash[2].$contenthash[3];
874 $l3 = $contenthash[4].$contenthash[5];
875 return "$this->filedir/$l1/$l2/$l3";
876 }
877
878 /**
879 * Marks pool file as candidate for deleting
880 * @param string $contenthash
881 */
882 public function mark_delete_candidate($contenthash) {
883 global $DB;
884
885 if ($DB->record_exists('files_cleanup', array('contenthash'=>$contenthash))) {
886 return;
887 }
888 $rec = new object();
889 $rec->contenthash = $contenthash;
890 $DB->insert_record('files_cleanup', $rec);
891 }
892
893 /**
894 * Cron cleanup job.
895 */
896 public function cron() {
897 global $DB;
898
899 //TODO: there is a small chance that reused files might be deleted
900 // if this function takes too long we should add some table locking here
901
902 $sql = "SELECT 1 AS id, fc.contenthash
903 FROM {files_cleanup} fc
904 LEFT JOIN {files} f ON f.contenthash = fc.contenthash
905 WHERE f.id IS NULL";
906 while ($hash = $DB->get_record_sql($sql, null, true)) {
907 $file = $this->path_from_hash($hash->contenthash).'/'.$hash->contenthash;
908 @unlink($file);
909 $DB->delete_records('files_cleanup', array('contenthash'=>$hash->contenthash));
910 }
911 }
912}