Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

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