Proyectos de Subversion Moodle

Rev

| Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * This plugin is used to access files on server file system
19
 *
20
 * @since Moodle 2.0
21
 * @package    repository_filesystem
22
 * @copyright  2010 Dongsheng Cai {@link http://dongsheng.org}
23
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
24
 */
25
require_once($CFG->dirroot . '/repository/lib.php');
26
require_once($CFG->libdir . '/filelib.php');
27
 
28
/**
29
 * repository_filesystem class
30
 *
31
 * Create a repository from your local filesystem
32
 * *NOTE* for security issue, we use a fixed repository path
33
 * which is %moodledata%/repository
34
 *
35
 * @package    repository
36
 * @copyright  2009 Dongsheng Cai {@link http://dongsheng.org}
37
 * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
38
 */
39
class repository_filesystem extends repository {
40
 
41
    /**
42
     * The subdirectory of the instance.
43
     *
44
     * @var string
45
     */
46
    protected $subdir;
47
 
48
    /**
49
     * Constructor
50
     *
51
     * @param int $repositoryid repository ID
52
     * @param int $context context ID
53
     * @param array $options
54
     */
55
    public function __construct($repositoryid, $context = SYSCONTEXTID, $options = array()) {
56
        parent::__construct($repositoryid, $context, $options);
57
        $this->subdir = $this->get_option('fs_path');
58
    }
59
 
60
    /**
61
     * Get the list of files and directories in that repository.
62
     *
63
     * @param string $fullpath Path to explore. This is assembled by {@link self::build_node_path()}.
64
     * @param string $page Page number.
65
     * @return array List of files and folders.
66
     */
67
    public function get_listing($fullpath = '', $page = '') {
68
        global $OUTPUT;
69
 
70
        $list = array(
71
            'list' => array(),
72
            'manage' => false,
73
            'dynload' => true,
74
            'nologin' => true,
75
            'path' => array()
76
        );
77
 
78
        // We analyse the path to extract what to browse.
79
        $fullpath = empty($fullpath) ? $this->build_node_path('root') : $fullpath;
80
        $trail = explode('|', $fullpath);
81
        $trail = array_pop($trail);
82
        list($mode, $path, $unused) = $this->explode_node_path($trail);
83
 
84
        // Is that a search?
85
        if ($mode === 'search') {
86
            return $this->search($path, $page);
87
        }
88
 
89
        // Cleaning up the requested path.
90
        $path = trim($path, '/');
91
        if (!$this->is_in_repository($path)) {
92
            // In case of doubt on the path, reset to default.
93
            $path = '';
94
        }
95
        $rootpath = $this->get_rootpath();
96
        $abspath = rtrim($rootpath . $path, '/') . '/';
97
 
98
        // Retrieve list of files and directories and sort them.
99
        $fileslist = array();
100
        $dirslist = array();
101
        if ($dh = opendir($abspath)) {
102
            while (false !== ($file = readdir($dh))) {
103
                if ($file != '.' and $file != '..') {
104
                    if (is_file($abspath . $file)) {
105
                        $fileslist[] = $file;
106
                    } else {
107
                        $dirslist[] = $file;
108
                    }
109
                }
110
            }
111
            closedir($dh);
112
        }
113
        core_collator::asort($fileslist, core_collator::SORT_NATURAL);
114
        core_collator::asort($dirslist, core_collator::SORT_NATURAL);
115
 
116
        // Fill the results.
117
        foreach ($dirslist as $file) {
118
            $list['list'][] = $this->build_node($rootpath, $path, $file, true, $fullpath);
119
        }
120
        foreach ($fileslist as $file) {
121
            $list['list'][] = $this->build_node($rootpath, $path, $file, false, $fullpath);
122
        }
123
 
124
        $list['path'] = $this->build_breadcrumb($fullpath);
125
        $list['list'] = array_filter($list['list'], array($this, 'filter'));
126
 
127
        return $list;
128
    }
129
 
130
    /**
131
     * Search files in repository.
132
     *
133
     * This search works by walking through the directories returning the files that match. Once
134
     * the limit of files is reached the walk stops. Whenever more files are requested, the walk
135
     * starts from the beginning until it reaches an additional set of files to return.
136
     *
137
     * @param string $query The query string.
138
     * @param int $page The page number.
139
     * @return mixed
140
     */
141
    public function search($query, $page = 1) {
142
        global $OUTPUT, $SESSION;
143
 
144
        $query = core_text::strtolower($query);
145
        $remainingdirs = 1000;
146
        $remainingobjects = 5000;
147
        $perpage = 50;
148
 
149
        // Because the repository API is weird, the first page is 0, but it should be 1.
150
        if (!$page) {
151
            $page = 1;
152
        }
153
 
154
        // Initialise the session variable in which we store the search related things.
155
        if (!isset($SESSION->repository_filesystem_search)) {
156
            $SESSION->repository_filesystem_search = array();
157
        }
158
 
159
        // Restore, or initialise the session search variables.
160
        if ($page <= 1) {
161
            $SESSION->repository_filesystem_search['query'] = $query;
162
            $SESSION->repository_filesystem_search['from'] = 0;
163
            $from = 0;
164
        } else {
165
            // Yes, the repository does not send the query again...
166
            $query = $SESSION->repository_filesystem_search['query'];
167
            $from = (int) $SESSION->repository_filesystem_search['from'];
168
        }
169
        $limit = $from + $perpage;
170
        $searchpath = $this->build_node_path('search', $query);
171
 
172
        // Pre-search initialisation.
173
        $rootpath = $this->get_rootpath();
174
        $found = 0;
175
        $toexplore = array('');
176
 
177
        // Retrieve list of matching files and directories.
178
        $matches = array();
179
        while (($path = array_shift($toexplore)) !== null) {
180
            $remainingdirs--;
181
 
182
            if ($objects = scandir($rootpath . $path)) {
183
                foreach ($objects as $object) {
184
                    $objectabspath = $rootpath . $path . $object;
185
                    if ($object == '.' || $object == '..') {
186
                        continue;
187
                    }
188
 
189
                    $remainingobjects--;
190
                    $isdir = is_dir($objectabspath);
191
 
192
                    // It is a match!
193
                    if (strpos(core_text::strtolower($object), $query) !== false) {
194
                        $found++;
195
                        $matches[] = array($path, $object, $isdir);
196
 
197
                        // That's enough, no need to find more.
198
                        if ($found >= $limit) {
199
                            break 2;
200
                        }
201
                    }
202
 
203
                    // I've seen enough files, I give up!
204
                    if ($remainingobjects <= 0) {
205
                        break 2;
206
                    }
207
 
208
                    // Add the directory to things to explore later.
209
                    if ($isdir) {
210
                        $toexplore[] = $path . trim($object, '/') . '/';
211
                    }
212
                }
213
            }
214
 
215
            if ($remainingdirs <= 0) {
216
                break;
217
            }
218
        }
219
 
220
        // Extract the results from all the matches.
221
        $matches = array_slice($matches, $from, $perpage);
222
 
223
        // If we didn't reach our limits of browsing, and we appear to still have files to find.
224
        if ($remainingdirs > 0 && $remainingobjects > 0 && count($matches) >= $perpage) {
225
            $SESSION->repository_filesystem_search['from'] = $limit;
226
            $pages = -1;
227
 
228
        // We reached the end of the repository, or our limits.
229
        } else {
230
            $SESSION->repository_filesystem_search['from'] = 0;
231
            $pages = 0;
232
        }
233
 
234
        // Organise the nodes.
235
        $results = array();
236
        foreach ($matches as $match) {
237
            list($path, $name, $isdir) = $match;
238
            $results[] = $this->build_node($rootpath, $path, $name, $isdir, $searchpath);
239
        }
240
 
241
        $list = array();
242
        $list['list'] = array_filter($results, array($this, 'filter'));
243
        $list['dynload'] = true;
244
        $list['nologin'] = true;
245
        $list['page'] = $page;
246
        $list['pages'] = $pages;
247
        $list['path'] = $this->build_breadcrumb($searchpath);
248
 
249
        return $list;
250
    }
251
 
252
    /**
253
     * Build the breadcrumb from a full path.
254
     *
255
     * @param string $path A path generated by {@link self::build_node_path()}.
256
     * @return array
257
     */
258
    protected function build_breadcrumb($path) {
259
        $breadcrumb = array(array(
260
            'name' => get_string('root', 'repository_filesystem'),
261
            'path' => $this->build_node_path('root')
262
        ));
263
 
264
        $crumbs = explode('|', $path);
265
        $trail = '';
266
 
267
        foreach ($crumbs as $crumb) {
268
            list($mode, $nodepath, $display) = $this->explode_node_path($crumb);
269
            switch ($mode) {
270
                case 'search':
271
                    $breadcrumb[] = array(
272
                        'name' => get_string('searchresults', 'repository_filesystem'),
273
                        'path' => $this->build_node_path($mode, $nodepath, $display, $trail),
274
                    );
275
                    break;
276
 
277
                case 'browse':
278
                    $breadcrumb[] = array(
279
                        'name' => $display,
280
                        'path' => $this->build_node_path($mode, $nodepath, $display, $trail),
281
                    );
282
                    break;
283
            }
284
 
285
            $lastcrumb = end($breadcrumb);
286
            $trail = $lastcrumb['path'];
287
        }
288
 
289
        return $breadcrumb;
290
    }
291
 
292
    /**
293
     * Build a file or directory node.
294
     *
295
     * @param string $rootpath The absolute path to the repository.
296
     * @param string $path The relative path of the object
297
     * @param string $name The name of the object
298
     * @param string $isdir Is the object a directory?
299
     * @param string $rootnodepath The node leading to this node (for breadcrumb).
300
     * @return array
301
     */
302
    protected function build_node($rootpath, $path, $name, $isdir, $rootnodepath) {
303
        global $OUTPUT;
304
 
305
        $relpath = trim($path, '/') . '/' . $name;
306
        $abspath = $rootpath . $relpath;
307
        $node = array(
308
            'title' => $name,
309
            'datecreated' => filectime($abspath),
310
            'datemodified' => filemtime($abspath),
311
        );
312
 
313
        if ($isdir) {
314
            $node['children'] = array();
315
            $node['thumbnail'] = $OUTPUT->image_url(file_folder_icon())->out(false);
316
            $node['path'] = $this->build_node_path('browse', $relpath, $name, $rootnodepath);
317
 
318
        } else {
319
            $node['source'] = $relpath;
320
            $node['size'] = filesize($abspath);
321
            $node['thumbnail'] = $OUTPUT->image_url(file_extension_icon($name))->out(false);
322
            $node['icon'] = $OUTPUT->image_url(file_extension_icon($name))->out(false);
323
            $node['path'] = $relpath;
324
 
325
            if (file_extension_in_typegroup($name, 'image') && ($imageinfo = @getimagesize($abspath))) {
326
                // This means it is an image and we can return dimensions and try to generate thumbnail/icon.
327
                $token = $node['datemodified'] . $node['size']; // To prevent caching by browser.
328
                $node['realthumbnail'] = $this->get_thumbnail_url($relpath, 'thumb', $token)->out(false);
329
                $node['realicon'] = $this->get_thumbnail_url($relpath, 'icon', $token)->out(false);
330
                $node['image_width'] = $imageinfo[0];
331
                $node['image_height'] = $imageinfo[1];
332
            }
333
        }
334
 
335
        return $node;
336
    }
337
 
338
    /**
339
     * Build the path to a browsable node.
340
     *
341
     * @param string $mode The type of browse mode.
342
     * @param string $realpath The path, or similar.
343
     * @param string $display The way to display the node.
344
     * @param string $root The path preceding this node.
345
     * @return string
346
     */
347
    protected function build_node_path($mode, $realpath = '', $display = '', $root = '') {
348
        $path = $mode . ':' . base64_encode($realpath) . ':' . base64_encode($display);
349
        if (!empty($root)) {
350
            $path = $root . '|' . $path;
351
        }
352
        return $path;
353
    }
354
 
355
    /**
356
     * Extract information from a node path.
357
     *
358
     * Note, this should not include preceding paths.
359
     *
360
     * @param string $path The path of the node.
361
     * @return array Contains the mode, the relative path, and the display text.
362
     */
363
    protected function explode_node_path($path) {
364
        list($mode, $realpath, $display) = explode(':', $path);
365
        return array(
366
            $mode,
367
            base64_decode($realpath),
368
            base64_decode($display)
369
        );
370
    }
371
 
372
    /**
373
     * To check whether the user is logged in.
374
     *
375
     * @return bool
376
     */
377
    public function check_login() {
378
        return true;
379
    }
380
 
381
    /**
382
     * Show the login screen, if required.
383
     *
384
     * @return string
385
     */
386
    public function print_login() {
387
        return true;
388
    }
389
 
390
    /**
391
     * Is it possible to do a global search?
392
     *
393
     * @return bool
394
     */
395
    public function global_search() {
396
        return false;
397
    }
398
 
399
    /**
400
     * Return file path.
401
     * @return array
402
     */
403
    public function get_file($file, $title = '') {
404
        global $CFG;
405
        $file = ltrim($file, '/');
406
        if (!$this->is_in_repository($file)) {
407
            throw new repository_exception('Invalid file requested.');
408
        }
409
        $file = $this->get_rootpath() . $file;
410
 
411
        // This is a hack to prevent move_to_file deleting files in local repository.
412
        $CFG->repository_no_delete = true;
413
        return array('path' => $file, 'url' => '');
414
    }
415
 
416
    /**
417
     * Return the source information
418
     *
419
     * @param stdClass $filepath
420
     * @return string|null
421
     */
422
    public function get_file_source_info($filepath) {
423
        return $filepath;
424
    }
425
 
426
    /**
427
     * Logout from repository instance
428
     *
429
     * @return string
430
     */
431
    public function logout() {
432
        return true;
433
    }
434
 
435
    /**
436
     * Return names of the instance options.
437
     *
438
     * @return array
439
     */
440
    public static function get_instance_option_names() {
441
        return array('fs_path', 'relativefiles');
442
    }
443
 
444
    /**
445
     * Save settings for repository instance
446
     *
447
     * @param array $options settings
448
     * @return bool
449
     */
450
    public function set_option($options = array()) {
451
        $options['fs_path'] = clean_param($options['fs_path'], PARAM_PATH);
452
        $options['relativefiles'] = clean_param($options['relativefiles'], PARAM_INT);
453
        $ret = parent::set_option($options);
454
        return $ret;
455
    }
456
 
457
    /**
458
     * Edit/Create Instance Settings Moodle form
459
     *
460
     * @param moodleform $mform Moodle form (passed by reference)
461
     */
462
    public static function instance_config_form($mform) {
463
        global $CFG;
464
        if (has_capability('moodle/site:config', context_system::instance())) {
465
            $path = $CFG->dataroot . '/repository/';
466
            if (!is_dir($path)) {
467
                mkdir($path, $CFG->directorypermissions, true);
468
            }
469
            if ($handle = opendir($path)) {
470
                $fieldname = get_string('path', 'repository_filesystem');
471
                $choices = array();
472
                while (false !== ($file = readdir($handle))) {
473
                    if (is_dir($path . $file) && $file != '.' && $file != '..') {
474
                        $choices[$file] = $file;
475
                        $fieldname = '';
476
                    }
477
                }
478
                if (empty($choices)) {
479
                    $mform->addElement('static', '', '', get_string('nosubdir', 'repository_filesystem', $path));
480
                    $mform->addElement('hidden', 'fs_path', '');
481
                    $mform->setType('fs_path', PARAM_PATH);
482
                } else {
483
                    $mform->addElement('select', 'fs_path', $fieldname, $choices);
484
                    $mform->addElement('static', null, '',  get_string('information', 'repository_filesystem', $path));
485
                }
486
                closedir($handle);
487
            }
488
            $mform->addElement('checkbox', 'relativefiles', get_string('relativefiles', 'repository_filesystem'),
489
                get_string('relativefiles_desc', 'repository_filesystem'));
490
            $mform->setType('relativefiles', PARAM_INT);
491
 
492
        } else {
493
            $mform->addElement('static', null, '',  get_string('nopermissions', 'error', get_string('configplugin',
494
                'repository_filesystem')));
495
            return false;
496
        }
497
    }
498
 
499
    /**
500
     * Create an instance for this plug-in
501
     *
502
     * @static
503
     * @param string $type the type of the repository
504
     * @param int $userid the user id
505
     * @param stdClass $context the context
506
     * @param array $params the options for this instance
507
     * @param int $readonly whether to create it readonly or not (defaults to not)
508
     * @return mixed
509
     */
510
    public static function create($type, $userid, $context, $params, $readonly=0) {
511
        if (has_capability('moodle/site:config', context_system::instance())) {
512
            return parent::create($type, $userid, $context, $params, $readonly);
513
        } else {
514
            require_capability('moodle/site:config', context_system::instance());
515
            return false;
516
        }
517
    }
518
 
519
    /**
520
     * Validate repository plugin instance form
521
     *
522
     * @param moodleform $mform moodle form
523
     * @param array $data form data
524
     * @param array $errors errors
525
     * @return array errors
526
     */
527
    public static function instance_form_validation($mform, $data, $errors) {
528
        $fspath = clean_param(trim($data['fs_path'], '/'), PARAM_PATH);
529
        if (empty($fspath) && !is_numeric($fspath)) {
530
            $errors['fs_path'] = get_string('invalidadminsettingname', 'error', 'fs_path');
531
        }
532
        return $errors;
533
    }
534
 
535
    /**
536
     * User cannot use the external link to dropbox
537
     *
538
     * @return int
539
     */
540
    public function supported_returntypes() {
541
        return FILE_INTERNAL | FILE_REFERENCE;
542
    }
543
 
544
    /**
545
     * Return human readable reference information
546
     *
547
     * @param string $reference value of DB field files_reference.reference
548
     * @param int $filestatus status of the file, 0 - ok, 666 - source missing
549
     * @return string
550
     */
551
    public function get_reference_details($reference, $filestatus = 0) {
552
        $details = $this->get_name().': '.$reference;
553
        if ($filestatus) {
554
            return get_string('lostsource', 'repository', $details);
555
        } else {
556
            return $details;
557
        }
558
    }
559
 
560
    public function sync_reference(stored_file $file) {
561
        if ($file->get_referencelastsync() + 60 > time()) {
562
            // Does not cost us much to synchronise within our own filesystem, check every 1 minute.
563
            return false;
564
        }
565
        static $issyncing = false;
566
        if ($issyncing) {
567
            // Avoid infinite recursion when calling $file->get_filesize() and get_contenthash().
568
            return false;
569
        }
570
        $filepath = $this->get_rootpath() . ltrim($file->get_reference(), '/');
571
        if ($this->is_in_repository($file->get_reference()) && file_exists($filepath) && is_readable($filepath)) {
572
            $fs = get_file_storage();
573
            $issyncing = true;
574
            if (file_extension_in_typegroup($filepath, 'web_image')) {
575
                $contenthash = file_storage::hash_from_path($filepath);
576
                if ($file->get_contenthash() == $contenthash) {
577
                    // File did not change since the last synchronisation.
578
                    $filesize = filesize($filepath);
579
                } else {
580
                    // Copy file into moodle filepool (used to generate an image thumbnail).
581
                    $file->set_timemodified(filemtime($filepath));
582
                    $file->set_synchronised_content_from_file($filepath);
583
                    return true;
584
                }
585
            } else {
586
                // Update only file size so file will NOT be copied into moodle filepool.
587
                if ($file->compare_to_string('') || !$file->compare_to_path($filepath)) {
588
                    // File is not synchronized or the file has changed.
589
                    $contenthash = file_storage::hash_from_string('');
590
                } else {
591
                    // File content was synchronised and has not changed since then, leave it.
592
                    $contenthash = null;
593
                }
594
                $filesize = filesize($filepath);
595
            }
596
            $issyncing = false;
597
            $modified = filemtime($filepath);
598
            $file->set_synchronized($contenthash, $filesize, 0, $modified);
599
        } else {
600
            $file->set_missingsource();
601
        }
602
        return true;
603
    }
604
 
605
    /**
606
     * Repository method to serve the referenced file
607
     *
608
     * @see send_stored_file
609
     *
610
     * @param stored_file $storedfile the file that contains the reference
611
     * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
612
     * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
613
     * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
614
     * @param array $options additional options affecting the file serving
615
     */
616
    public function send_file($storedfile, $lifetime=null , $filter=0, $forcedownload=false, array $options = null) {
617
        $reference = $storedfile->get_reference();
618
        $file = $this->get_rootpath() . ltrim($reference, '/');
619
        if ($this->is_in_repository($reference) && is_readable($file)) {
620
            $filename = $storedfile->get_filename();
621
            if ($options && isset($options['filename'])) {
622
                $filename = $options['filename'];
623
            }
624
            $dontdie = ($options && isset($options['dontdie']));
625
            send_file($file, $filename, $lifetime , $filter, false, $forcedownload, '', $dontdie);
626
        } else {
627
            send_file_not_found();
628
        }
629
    }
630
 
631
    /**
632
     * Is this repository accessing private data?
633
     *
634
     * @return bool
635
     */
636
    public function contains_private_data() {
637
        return false;
638
    }
639
 
640
    /**
641
     * Return the rootpath of this repository instance.
642
     *
643
     * Trim() is a necessary step to ensure that the subdirectory is not '/'.
644
     *
645
     * @return string path
646
     * @throws repository_exception If the subdir is unsafe, or invalid.
647
     */
648
    public function get_rootpath() {
649
        global $CFG;
650
        $subdir = clean_param(trim($this->subdir, '/'), PARAM_PATH);
651
        $path = $CFG->dataroot . '/repository/' . $this->subdir . '/';
652
        if ((empty($this->subdir) && !is_numeric($this->subdir)) || $subdir != $this->subdir || !is_dir($path)) {
653
            throw new repository_exception('The instance is not properly configured, invalid path.');
654
        }
655
        return $path;
656
    }
657
 
658
    /**
659
     * Checks if $path is part of this repository.
660
     *
661
     * Try to prevent $path hacks such as ../ .
662
     *
663
     * We do not use clean_param(, PARAM_PATH) here because it also trims down some
664
     * characters that are allowed, like < > ' . But we do ensure that the directory
665
     * is safe by checking that it starts with $rootpath.
666
     *
667
     * @param string $path relative path to a file or directory in the repo.
668
     * @return boolean false when not.
669
     */
670
    protected function is_in_repository($path) {
671
        $rootpath = $this->get_rootpath();
672
        if (strpos(realpath($rootpath . $path), realpath($rootpath)) !== 0) {
673
            return false;
674
        }
675
        return true;
676
    }
677
 
678
    /**
679
     * Returns url of thumbnail file.
680
     *
681
     * @param string $filepath current path in repository (dir and filename)
682
     * @param string $thumbsize 'thumb' or 'icon'
683
     * @param string $token identifier of the file contents - to prevent browser from caching changed file
684
     * @return moodle_url
685
     */
686
    protected function get_thumbnail_url($filepath, $thumbsize, $token) {
687
        return moodle_url::make_pluginfile_url($this->context->id, 'repository_filesystem', $thumbsize, $this->id,
688
                '/' . trim($filepath, '/') . '/', $token);
689
    }
690
 
691
    /**
692
     * Returns the stored thumbnail file, generates it if not present.
693
     *
694
     * @param string $filepath current path in repository (dir and filename)
695
     * @param string $thumbsize 'thumb' or 'icon'
696
     * @return null|stored_file
697
     */
698
    public function get_thumbnail($filepath, $thumbsize) {
699
        global $CFG;
700
 
701
        $filepath = trim($filepath, '/');
702
        $origfile = $this->get_rootpath() . $filepath;
703
        // As thumbnail filename we use original file content hash.
704
        if (!$this->is_in_repository($filepath) || !($filecontents = @file_get_contents($origfile))) {
705
            // File is not found or is not readable.
706
            return null;
707
        }
708
        $filename = file_storage::hash_from_string($filecontents);
709
 
710
        // Try to get generated thumbnail for this file.
711
        $fs = get_file_storage();
712
        if (!($file = $fs->get_file(SYSCONTEXTID, 'repository_filesystem', $thumbsize, $this->id, '/' . $filepath . '/',
713
                $filename))) {
714
            // Thumbnail not found . Generate and store thumbnail.
715
            require_once($CFG->libdir . '/gdlib.php');
716
            if ($thumbsize === 'thumb') {
717
                $size = 90;
718
            } else {
719
                $size = 24;
720
            }
721
            if (!$data = generate_image_thumbnail_from_string($filecontents, $size, $size)) {
722
                // Generation failed.
723
                return null;
724
            }
725
            $record = array(
726
                'contextid' => SYSCONTEXTID,
727
                'component' => 'repository_filesystem',
728
                'filearea' => $thumbsize,
729
                'itemid' => $this->id,
730
                'filepath' => '/' . $filepath . '/',
731
                'filename' => $filename,
732
            );
733
            $file = $fs->create_file_from_string($record, $data);
734
        }
735
        return $file;
736
    }
737
 
738
    /**
739
     * Run in cron for particular repository instance. Removes thumbnails for deleted/modified files.
740
     *
741
     * @param stored_file[] $storedfiles
742
     */
743
    public function remove_obsolete_thumbnails($storedfiles) {
744
        // Group found files by filepath ('filepath' in Moodle file storage is dir+name in filesystem repository).
745
        $files = array();
746
        foreach ($storedfiles as $file) {
747
            if (!isset($files[$file->get_filepath()])) {
748
                $files[$file->get_filepath()] = array();
749
            }
750
            $files[$file->get_filepath()][] = $file;
751
        }
752
 
753
        // Loop through all files and make sure the original exists and has the same contenthash.
754
        $deletedcount = 0;
755
        foreach ($files as $filepath => $filesinpath) {
756
            if ($filecontents = @file_get_contents($this->get_rootpath() . trim($filepath, '/'))) {
757
                // The 'filename' in Moodle file storage is contenthash of the file in filesystem repository.
758
                $filename = file_storage::hash_from_string($filecontents);
759
                foreach ($filesinpath as $file) {
760
                    if ($file->get_filename() !== $filename && $file->get_filename() !== '.') {
761
                        // Contenthash does not match, this is an old thumbnail.
762
                        $deletedcount++;
763
                        $file->delete();
764
                    }
765
                }
766
            } else {
767
                // Thumbnail exist but file not.
768
                foreach ($filesinpath as $file) {
769
                    if ($file->get_filename() !== '.') {
770
                        $deletedcount++;
771
                    }
772
                    $file->delete();
773
                }
774
            }
775
        }
776
        if ($deletedcount) {
777
            mtrace(" instance {$this->id}: deleted $deletedcount thumbnails");
778
        }
779
    }
780
 
781
    /**
782
     *  Gets a file relative to this file in the repository and sends it to the browser.
783
     *
784
     * @param stored_file $mainfile The main file we are trying to access relative files for.
785
     * @param string $relativepath the relative path to the file we are trying to access.
786
     */
787
    public function send_relative_file(stored_file $mainfile, $relativepath) {
788
        global $CFG;
789
        // Check if this repository is allowed to use relative linking.
790
        $allowlinks = $this->supports_relative_file();
791
        if (!empty($allowlinks)) {
792
            // Get path to the mainfile.
793
            $mainfilepath = $mainfile->get_source();
794
 
795
            // Strip out filename from the path.
796
            $filename = $mainfile->get_filename();
797
            $basepath = strstr($mainfilepath, $filename, true);
798
 
799
            $fullrelativefilepath = realpath($this->get_rootpath().$basepath.$relativepath);
800
 
801
            // Sanity check to make sure this path is inside this repository and the file exists.
802
            if (strpos($fullrelativefilepath, realpath($this->get_rootpath())) === 0 && file_exists($fullrelativefilepath)) {
803
                send_file($fullrelativefilepath, basename($relativepath), null, 0);
804
            }
805
        }
806
        send_file_not_found();
807
    }
808
 
809
    /**
810
     * helper function to check if the repository supports send_relative_file.
811
     *
812
     * @return true|false
813
     */
814
    public function supports_relative_file() {
815
        return $this->get_option('relativefiles');
816
    }
817
}
818
 
819
/**
820
 * Generates and sends the thumbnail for an image in filesystem.
821
 *
822
 * @param stdClass $course course object
823
 * @param stdClass $cm course module object
824
 * @param stdClass $context context object
825
 * @param string $filearea file area
826
 * @param array $args extra arguments
827
 * @param bool $forcedownload whether or not force download
828
 * @param array $options additional options affecting the file serving
829
 * @return bool
830
 */
831
function repository_filesystem_pluginfile($course, $cm, $context, $filearea, $args, $forcedownload, array $options=array()) {
832
    global $OUTPUT, $CFG;
833
    // Allowed filearea is either thumb or icon - size of the thumbnail.
834
    if ($filearea !== 'thumb' && $filearea !== 'icon') {
835
        return false;
836
    }
837
 
838
    // As itemid we pass repository instance id.
839
    $itemid = array_shift($args);
840
    // Filename is some token that we can ignore (used only to make sure browser does not serve cached copy when file is changed).
841
    array_pop($args);
842
    // As filepath we use full filepath (dir+name) of the file in this instance of filesystem repository.
843
    $filepath = implode('/', $args);
844
 
845
    // Make sure file exists in the repository and is accessible.
846
    $repo = repository::get_repository_by_id($itemid, $context);
847
    $repo->check_capability();
848
    // Find stored or generated thumbnail.
849
    if (!($file = $repo->get_thumbnail($filepath, $filearea))) {
850
        // Generation failed, redirect to default icon for file extension.
851
        // Do not use redirect() here because is not compatible with webservice/pluginfile.php.
852
        header('Location: ' . $OUTPUT->image_url(file_extension_icon($file)));
853
    }
854
    // The thumbnails should not be changing much, but maybe the default lifetime is too long.
855
    $lifetime = $CFG->filelifetime;
856
    if ($lifetime > 60*10) {
857
        $lifetime = 60*10;
858
    }
859
    send_stored_file($file, $lifetime, 0, $forcedownload, $options);
860
}