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
use Psr\Http\Message\StreamInterface;
18
 
19
/**
20
 * File system class used for low level access to real files in filedir.
21
 *
22
 * @package   core_files
23
 * @category  files
24
 * @copyright 2017 Andrew Nicols <andrew@nicols.co.uk>
25
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26
 */
27
abstract class file_system {
28
 
29
    /**
30
     * Output the content of the specified stored file.
31
     *
32
     * Note, this is different to get_content() as it uses the built-in php
33
     * readfile function which is more efficient.
34
     *
35
     * @param stored_file $file The file to serve.
36
     * @return void
37
     */
38
    public function readfile(stored_file $file) {
39
        if ($this->is_file_readable_locally_by_storedfile($file, false)) {
40
            $path = $this->get_local_path_from_storedfile($file, false);
41
        } else {
42
            $path = $this->get_remote_path_from_storedfile($file);
43
        }
44
        if (readfile_allow_large($path, $file->get_filesize()) === false) {
45
            throw new file_exception('storedfilecannotreadfile', $file->get_filename());
46
        }
47
    }
48
 
49
    /**
50
     * Get the full path on disk for the specified stored file.
51
     *
52
     * Note: This must return a consistent path for the file's contenthash
53
     * and the path _will_ be in a standard local format.
54
     * Streamable paths will not work.
55
     * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
56
     *
57
     * The $fetchifnotfound allows you to determine the expected path of the file.
58
     *
59
     * @param stored_file $file The file to serve.
60
     * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
61
     * @return string full path to pool file with file content
62
     */
63
    public function get_local_path_from_storedfile(stored_file $file, $fetchifnotfound = false) {
64
        return $this->get_local_path_from_hash($file->get_contenthash(), $fetchifnotfound);
65
    }
66
 
67
    /**
68
     * Get a remote filepath for the specified stored file.
69
     *
70
     * This is typically either the same as the local filepath, or it is a streamable resource.
71
     *
72
     * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
73
     *
74
     * @param stored_file $file The file to serve.
75
     * @return string full path to pool file with file content
76
     */
77
    public function get_remote_path_from_storedfile(stored_file $file) {
78
        return $this->get_remote_path_from_hash($file->get_contenthash(), false);
79
    }
80
 
81
    /**
82
     * Get the full path for the specified hash, including the path to the filedir.
83
     *
84
     * Note: This must return a consistent path for the file's contenthash
85
     * and the path _will_ be in a standard local format.
86
     * Streamable paths will not work.
87
     * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
88
     *
89
     * The $fetchifnotfound allows you to determine the expected path of the file.
90
     *
91
     * @param string $contenthash The content hash
92
     * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
93
     * @return string The full path to the content file
94
     */
95
    abstract protected function get_local_path_from_hash($contenthash, $fetchifnotfound = false);
96
 
97
    /**
98
     * Get the full path for the specified hash, including the path to the filedir.
99
     *
100
     * This is typically either the same as the local filepath, or it is a streamable resource.
101
     *
102
     * See https://secure.php.net/manual/en/wrappers.php for further information on valid wrappers.
103
     *
104
     * @param string $contenthash The content hash
105
     * @return string The full path to the content file
106
     */
107
    abstract protected function get_remote_path_from_hash($contenthash);
108
 
109
    /**
110
     * Determine whether the file is present on the file system somewhere.
111
     * A local copy of the file _will_ be fetched if $fetchifnotfound is tree.
112
     *
113
     * The $fetchifnotfound allows you to determine the expected path of the file.
114
     *
115
     * @param stored_file $file The file to ensure is available.
116
     * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
117
     * @return bool
118
     */
119
    public function is_file_readable_locally_by_storedfile(stored_file $file, $fetchifnotfound = false) {
120
        if (!$file->get_filesize()) {
121
            // Files with empty size are either directories or empty.
122
            // We handle these virtually.
123
            return true;
124
        }
125
 
126
        // Check to see if the file is currently readable.
127
        $path = $this->get_local_path_from_storedfile($file, $fetchifnotfound);
128
        if (is_readable($path)) {
129
            return true;
130
        }
131
 
132
        return false;
133
    }
134
 
135
    /**
136
     * Determine whether the file is present on the local file system somewhere.
137
     *
138
     * @param stored_file $file The file to ensure is available.
139
     * @return bool
140
     */
141
    public function is_file_readable_remotely_by_storedfile(stored_file $file) {
142
        if (!$file->get_filesize()) {
143
            // Files with empty size are either directories or empty.
144
            // We handle these virtually.
145
            return true;
146
        }
147
 
148
        $path = $this->get_remote_path_from_storedfile($file, false);
149
        if (is_readable($path)) {
150
            return true;
151
        }
152
 
153
        return false;
154
    }
155
 
156
    /**
157
     * Determine whether the file is present on the file system somewhere given
158
     * the contenthash.
159
     *
160
     * @param string $contenthash The contenthash of the file to check.
161
     * @param bool $fetchifnotfound Whether to attempt to fetch from the remote path if not found.
162
     * @return bool
163
     */
164
    public function is_file_readable_locally_by_hash($contenthash, $fetchifnotfound = false) {
165
        if ($contenthash === file_storage::hash_from_string('')) {
166
            // Files with empty size are either directories or empty.
167
            // We handle these virtually.
168
            return true;
169
        }
170
 
171
        // This is called by file_storage::content_exists(), and in turn by the repository system.
172
        $path = $this->get_local_path_from_hash($contenthash, $fetchifnotfound);
173
 
174
        // Note - it is not possible to perform a content recovery safely from a hash alone.
175
        return is_readable($path);
176
    }
177
 
178
    /**
179
     * Determine whether the file is present locally on the file system somewhere given
180
     * the contenthash.
181
     *
182
     * @param string $contenthash The contenthash of the file to check.
183
     * @return bool
184
     */
185
    public function is_file_readable_remotely_by_hash($contenthash) {
186
        if ($contenthash === file_storage::hash_from_string('')) {
187
            // Files with empty size are either directories or empty.
188
            // We handle these virtually.
189
            return true;
190
        }
191
 
192
        $path = $this->get_remote_path_from_hash($contenthash, false);
193
 
194
        // Note - it is not possible to perform a content recovery safely from a hash alone.
195
        return is_readable($path);
196
    }
197
 
198
    /**
199
     * Copy content of file to given pathname.
200
     *
201
     * @param stored_file $file The file to be copied
202
     * @param string $target real path to the new file
203
     * @return bool success
204
     */
205
    abstract public function copy_content_from_storedfile(stored_file $file, $target);
206
 
207
    /**
208
     * Remove the file with the specified contenthash.
209
     *
210
     * Note, if overriding this function, you _must_ check that the file is
211
     * no longer in use - see {check_file_usage}.
212
     *
213
     * DO NOT call directly - reserved for core!!
214
     *
215
     * @param string $contenthash
216
     */
217
    abstract public function remove_file($contenthash);
218
 
219
    /**
220
     * Check whether a file is removable.
221
     *
222
     * This must be called prior to file removal.
223
     *
224
     * @param string $contenthash
225
     * @return bool
226
     */
227
    protected static function is_file_removable($contenthash) {
228
        global $DB;
229
 
230
        if ($contenthash === file_storage::hash_from_string('')) {
231
            // No need to delete files without content.
232
            return false;
233
        }
234
 
235
        // Note: This section is critical - in theory file could be reused at the same time, if this
236
        // happens we can still recover the file from trash.
237
        // Technically this is the responsibility of the file_storage API, but as this method is public, we go belt-and-braces.
238
        if ($DB->record_exists('files', array('contenthash' => $contenthash))) {
239
            // File content is still used.
240
            return false;
241
        }
242
 
243
        return true;
244
    }
245
 
246
    /**
247
     * Get the content of the specified stored file.
248
     *
249
     * Generally you will probably want to use readfile() to serve content,
250
     * and where possible you should see if you can use
251
     * get_content_file_handle and work with the file stream instead.
252
     *
253
     * @param stored_file $file The file to retrieve
254
     * @return string The full file content
255
     */
256
    public function get_content(stored_file $file) {
257
        if (!$file->get_filesize()) {
258
            // Directories are empty. Empty files are not worth fetching.
259
            return '';
260
        }
261
 
262
        $source = $this->get_remote_path_from_storedfile($file);
263
        return file_get_contents($source);
264
    }
265
 
266
    /**
267
     * List contents of archive.
268
     *
269
     * @param stored_file $file The archive to inspect
270
     * @param file_packer $packer file packer instance
271
     * @return array of file infos
272
     */
273
    public function list_files($file, file_packer $packer) {
274
        $archivefile = $this->get_local_path_from_storedfile($file, true);
275
        return $packer->list_files($archivefile);
276
    }
277
 
278
    /**
279
     * Extract file to given file path (real OS filesystem), existing files are overwritten.
280
     *
281
     * @param stored_file $file The archive to inspect
282
     * @param file_packer $packer File packer instance
283
     * @param string $pathname Target directory
284
     * @param file_progress $progress progress indicator callback or null if not required
285
     * @return array|bool List of processed files; false if error
286
     */
287
    public function extract_to_pathname(stored_file $file, file_packer $packer, $pathname, file_progress $progress = null) {
288
        $archivefile = $this->get_local_path_from_storedfile($file, true);
289
        return $packer->extract_to_pathname($archivefile, $pathname, null, $progress);
290
    }
291
 
292
    /**
293
     * Extract file to given file path (real OS filesystem), existing files are overwritten.
294
     *
295
     * @param stored_file $file The archive to inspect
296
     * @param file_packer $packer file packer instance
297
     * @param int $contextid context ID
298
     * @param string $component component
299
     * @param string $filearea file area
300
     * @param int $itemid item ID
301
     * @param string $pathbase path base
302
     * @param int $userid user ID
303
     * @param file_progress $progress Progress indicator callback or null if not required
304
     * @return array|bool list of processed files; false if error
305
     */
306
    public function extract_to_storage(stored_file $file, file_packer $packer, $contextid,
307
            $component, $filearea, $itemid, $pathbase, $userid = null, file_progress $progress = null) {
308
 
309
        // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
310
        $archivefile = $this->get_local_path_from_storedfile($file, true);
311
        return $packer->extract_to_storage($archivefile, $contextid,
312
                $component, $filearea, $itemid, $pathbase, $userid, $progress);
313
    }
314
 
315
    /**
316
     * Add file/directory into archive.
317
     *
318
     * @param stored_file $file The file to archive
319
     * @param file_archive $filearch file archive instance
320
     * @param string $archivepath pathname in archive
321
     * @return bool success
322
     */
323
    public function add_storedfile_to_archive(stored_file $file, file_archive $filearch, $archivepath) {
324
        if ($file->is_directory()) {
325
            return $filearch->add_directory($archivepath);
326
        } else {
327
            // Since we do not know which extractor we have, and whether it supports remote paths, use a local path here.
328
            return $filearch->add_file_from_pathname($archivepath, $this->get_local_path_from_storedfile($file, true));
329
        }
330
    }
331
 
332
    /**
333
     * Adds this file path to a curl request (POST only).
334
     *
335
     * @param stored_file $file The file to add to the curl request
336
     * @param curl $curlrequest The curl request object
337
     * @param string $key What key to use in the POST request
338
     * @return void
339
     * This needs the fullpath for the storedfile :/
340
     * Can this be achieved in some other fashion?
341
     */
342
    public function add_to_curl_request(stored_file $file, &$curlrequest, $key) {
343
        // Note: curl_file_create does not work with remote paths.
344
        $path = $this->get_local_path_from_storedfile($file, true);
345
        $curlrequest->_tmp_file_post_params[$key] = curl_file_create($path, null, $file->get_filename());
346
    }
347
 
348
    /**
349
     * Returns information about image.
350
     * Information is determined from the file content
351
     *
352
     * @param stored_file $file The file to inspect
353
     * @return mixed array with width, height and mimetype; false if not an image
354
     */
355
    public function get_imageinfo(stored_file $file) {
356
        if (!$this->is_image_from_storedfile($file)) {
357
            return false;
358
        }
359
 
360
        $hash = $file->get_contenthash();
361
        $cache = cache::make('core', 'file_imageinfo');
362
        $info = $cache->get($hash);
363
        if ($info !== false) {
364
            return $info;
365
        }
366
 
367
        // Whilst get_imageinfo_from_path can use remote paths, it must download the entire file first.
368
        // It is more efficient to use a local file when possible.
369
        $info = $this->get_imageinfo_from_path($this->get_local_path_from_storedfile($file, true));
370
        $cache->set($hash, $info);
371
        return $info;
372
    }
373
 
374
    /**
375
     * Attempt to determine whether the specified file is likely to be an
376
     * image.
377
     * Since this relies upon the mimetype stored in the files table, there
378
     * may be times when this information is not 100% accurate.
379
     *
380
     * @param stored_file $file The file to check
381
     * @return bool
382
     */
383
    public function is_image_from_storedfile(stored_file $file) {
384
        if (!$file->get_filesize()) {
385
            // An empty file cannot be an image.
386
            return false;
387
        }
388
 
389
        $mimetype = $file->get_mimetype();
390
        if (!preg_match('|^image/|', $mimetype)) {
391
            // The mimetype does not include image.
392
            return false;
393
        }
394
 
395
        // If it looks like an image, and it smells like an image, perhaps it's an image!
396
        return true;
397
    }
398
 
399
    /**
400
     * Returns image information relating to the specified path or URL.
401
     *
402
     * @param string $path The full path of the image file.
403
     * @return array|bool array that containing width, height, and mimetype or false if cannot get the image info.
404
     */
405
    protected function get_imageinfo_from_path($path) {
406
        $imagemimetype = file_storage::mimetype_from_file($path);
407
        $issvgimage = file_is_svg_image_from_mimetype($imagemimetype);
408
 
409
        if (!$issvgimage) {
410
            $imageinfo = getimagesize($path);
411
            if (!is_array($imageinfo)) {
412
                return false; // Nothing to process, the file was not recognised as image by GD.
413
            }
414
            $image = [
415
                    'width' => $imageinfo[0],
416
                    'height' => $imageinfo[1],
417
                    'mimetype' => image_type_to_mime_type($imageinfo[2]),
418
            ];
419
        } else {
420
            // Since SVG file is actually an XML file, GD cannot handle.
421
            $svgcontent = @simplexml_load_file($path);
422
            if (!$svgcontent) {
423
                // Cannot parse the file.
424
                return false;
425
            }
426
            $svgattrs = $svgcontent->attributes();
427
 
428
            if (!empty($svgattrs->viewBox)) {
429
                // We have viewBox.
430
                $viewboxval = explode(' ', $svgattrs->viewBox);
431
                $width = intval($viewboxval[2]);
432
                $height = intval($viewboxval[3]);
433
            } else {
434
                // Get the width.
435
                if (!empty($svgattrs->width) && intval($svgattrs->width) > 0) {
436
                    $width = intval($svgattrs->width);
437
                } else {
438
                    // Default width.
439
                    $width = 800;
440
                }
441
                // Get the height.
442
                if (!empty($svgattrs->height) && intval($svgattrs->height) > 0) {
443
                    $height = intval($svgattrs->height);
444
                } else {
445
                    // Default width.
446
                    $height = 600;
447
                }
448
            }
449
 
450
            $image = [
451
                    'width' => $width,
452
                    'height' => $height,
453
                    'mimetype' => $imagemimetype,
454
            ];
455
        }
456
 
457
        if (empty($image['width']) or empty($image['height']) or empty($image['mimetype'])) {
458
            // GD can not parse it, sorry.
459
            return false;
460
        }
461
        return $image;
462
    }
463
 
464
    /**
465
     * Serve file content using X-Sendfile header.
466
     * Please make sure that all headers are already sent and the all
467
     * access control checks passed.
468
     *
469
     * This alternate method to xsendfile() allows an alternate file system
470
     * to use the full file metadata and avoid extra lookups.
471
     *
472
     * @param stored_file $file The file to send
473
     * @return bool success
474
     */
475
    public function xsendfile_file(stored_file $file): bool {
476
        return $this->xsendfile($file->get_contenthash());
477
    }
478
 
479
    /**
480
     * Serve file content using X-Sendfile header.
481
     * Please make sure that all headers are already sent and the all
482
     * access control checks passed.
483
     *
484
     * @param string $contenthash The content hash of the file to be served
485
     * @return bool success
486
     */
487
    public function xsendfile($contenthash) {
488
        global $CFG;
489
        require_once($CFG->libdir . "/xsendfilelib.php");
490
 
491
        return xsendfile($this->get_remote_path_from_hash($contenthash));
492
    }
493
 
494
    /**
495
     * Returns true if filesystem is configured to support xsendfile.
496
     *
497
     * @return bool
498
     */
499
    public function supports_xsendfile() {
500
        global $CFG;
501
        return !empty($CFG->xsendfile);
502
    }
503
 
504
    /**
505
     * Validate that the content hash matches the content hash of the file on disk.
506
     *
507
     * @param string $contenthash The current content hash to validate
508
     * @param string $pathname The path to the file on disk
509
     * @return array The content hash (it might change) and file size
510
     */
511
    protected function validate_hash_and_file_size($contenthash, $pathname) {
512
        global $CFG;
513
 
514
        if (!is_readable($pathname)) {
515
            throw new file_exception('storedfilecannotread', '', $pathname);
516
        }
517
 
518
        $filesize = filesize($pathname);
519
        if ($filesize === false) {
520
            throw new file_exception('storedfilecannotread', '', $pathname);
521
        }
522
 
523
        if (is_null($contenthash)) {
524
            $contenthash = file_storage::hash_from_path($pathname);
525
        } else if ($CFG->debugdeveloper) {
526
            $filehash = file_storage::hash_from_path($pathname);
527
            if ($filehash === false) {
528
                throw new file_exception('storedfilecannotread', '', $pathname);
529
            }
530
            if ($filehash !== $contenthash) {
531
                // Hopefully this never happens, if yes we need to fix calling code.
532
                debugging("Invalid contenthash submitted for file $pathname", DEBUG_DEVELOPER);
533
                $contenthash = $filehash;
534
            }
535
        }
536
        if ($contenthash === false) {
537
            throw new file_exception('storedfilecannotread', '', $pathname);
538
        }
539
 
540
        if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
541
            // Did the file change or is file_storage::hash_from_path() borked for this file?
542
            clearstatcache();
543
            $contenthash = file_storage::hash_from_path($pathname);
544
            $filesize    = filesize($pathname);
545
 
546
            if ($contenthash === false or $filesize === false) {
547
                throw new file_exception('storedfilecannotread', '', $pathname);
548
            }
549
            if ($filesize > 0 and $contenthash === file_storage::hash_from_string('')) {
550
                // This is very weird...
551
                throw new file_exception('storedfilecannotread', '', $pathname);
552
            }
553
        }
554
 
555
        return [$contenthash, $filesize];
556
    }
557
 
558
    /**
559
     * Add the supplied file to the file system.
560
     *
561
     * Note: If overriding this function, it is advisable to store the file
562
     * in the path returned by get_local_path_from_hash as there may be
563
     * subsequent uses of the file in the same request.
564
     *
565
     * @param string $pathname Path to file currently on disk
566
     * @param string $contenthash SHA1 hash of content if known (performance only)
567
     * @return array (contenthash, filesize, newfile)
568
     */
569
    abstract public function add_file_from_path($pathname, $contenthash = null);
570
 
571
    /**
572
     * Add a file with the supplied content to the file system.
573
     *
574
     * Note: If overriding this function, it is advisable to store the file
575
     * in the path returned by get_local_path_from_hash as there may be
576
     * subsequent uses of the file in the same request.
577
     *
578
     * @param string $content file content - binary string
579
     * @return array (contenthash, filesize, newfile)
580
     */
581
    abstract public function add_file_from_string($content);
582
 
583
    /**
584
     * Returns file handle - read only mode, no writing allowed into pool files!
585
     *
586
     * When you want to modify a file, create a new file and delete the old one.
587
     *
588
     * @param stored_file $file The file to retrieve a handle for
589
     * @param int $type Type of file handle (FILE_HANDLE_xx constant)
590
     * @return resource file handle
591
     */
592
    public function get_content_file_handle(stored_file $file, $type = stored_file::FILE_HANDLE_FOPEN) {
593
        if ($type === stored_file::FILE_HANDLE_GZOPEN) {
594
            // Local file required for gzopen.
595
            $path = $this->get_local_path_from_storedfile($file, true);
596
        } else {
597
            $path = $this->get_remote_path_from_storedfile($file);
598
        }
599
 
600
        return self::get_file_handle_for_path($path, $type);
601
    }
602
 
603
    /**
604
     * Return a file handle for the specified path.
605
     *
606
     * This abstraction should be used when overriding get_content_file_handle in a new file system.
607
     *
608
     * @param string $path The path to the file. This shoudl be any type of path that fopen and gzopen accept.
609
     * @param int $type Type of file handle (FILE_HANDLE_xx constant)
610
     * @return resource
611
     * @throws coding_exception When an unexpected type of file handle is requested
612
     */
613
    protected static function get_file_handle_for_path($path, $type = stored_file::FILE_HANDLE_FOPEN) {
614
        switch ($type) {
615
            case stored_file::FILE_HANDLE_FOPEN:
616
                // Binary reading.
617
                return fopen($path, 'rb');
618
            case stored_file::FILE_HANDLE_GZOPEN:
619
                // Binary reading of file in gz format.
620
                return gzopen($path, 'rb');
621
            default:
622
                throw new coding_exception('Unexpected file handle type');
623
        }
624
    }
625
 
626
    /**
627
     * Get a PSR7 Stream for the specified file which implements the PSR Message StreamInterface.
628
     *
629
     * @param stored_file $file
630
     * @return StreamInterface
631
     */
632
    public function get_psr_stream(stored_file $file): StreamInterface {
633
        return \GuzzleHttp\Psr7\Utils::streamFor($this->get_content_file_handle($file));
634
    }
635
 
636
    /**
637
     * Retrieve the mime information for the specified stored file.
638
     *
639
     * @param string $contenthash
640
     * @param string $filename
641
     * @return string The MIME type.
642
     */
643
    public function mimetype_from_hash($contenthash, $filename) {
644
        $pathname = $this->get_local_path_from_hash($contenthash);
645
        $mimetype = file_storage::mimetype($pathname, $filename);
646
 
647
        if ($mimetype === 'document/unknown' && !$this->is_file_readable_locally_by_hash($contenthash)) {
648
            // The type is unknown, but the full checks weren't completed because the file isn't locally available.
649
            // Ensure we have a local copy and try again.
650
            $pathname = $this->get_local_path_from_hash($contenthash, true);
651
            $mimetype = file_storage::mimetype_from_file($pathname);
652
        }
653
 
654
        return $mimetype;
655
    }
656
 
657
    /**
658
     * Retrieve the mime information for the specified stored file.
659
     *
660
     * @param stored_file $file The stored file to retrieve mime information for
661
     * @return string The MIME type.
662
     */
663
    public function mimetype_from_storedfile($file) {
664
        if (!$file->get_filesize()) {
665
            // Files with an empty filesize are treated as directories and have no mimetype.
666
            return null;
667
        }
668
        return $this->mimetype_from_hash($file->get_contenthash(), $file->get_filename());
669
    }
670
 
671
    /**
672
     * Run any periodic tasks which must be performed.
673
     */
674
    public function cron() {
675
    }
676
}