Proyectos de Subversion Moodle

Rev

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