Proyectos de Subversion Moodle

Rev

Rev 11 | | 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
 * Functions for file handling.
19
 *
20
 * @package   core_files
21
 * @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)
22
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23
 */
24
 
25
defined('MOODLE_INTERNAL') || die();
26
 
27
/**
28
 * BYTESERVING_BOUNDARY - string unique string constant.
29
 */
30
define('BYTESERVING_BOUNDARY', 's1k2o3d4a5k6s7');
31
 
32
 
33
/**
34
 * Do not process file merging when working with draft area files.
35
 */
36
define('IGNORE_FILE_MERGE', -1);
37
 
38
/**
39
 * Unlimited area size constant
40
 */
41
define('FILE_AREA_MAX_BYTES_UNLIMITED', -1);
42
 
43
/**
44
 * Capacity of the draft area bucket when using the leaking bucket technique to limit the draft upload rate.
45
 */
46
define('DRAFT_AREA_BUCKET_CAPACITY', 50);
47
 
48
/**
49
 * Leaking rate of the draft area bucket when using the leaking bucket technique to limit the draft upload rate.
50
 */
51
define('DRAFT_AREA_BUCKET_LEAK', 0.2);
52
 
53
require_once("$CFG->libdir/filestorage/file_exceptions.php");
54
require_once("$CFG->libdir/filestorage/file_storage.php");
55
require_once("$CFG->libdir/filestorage/zip_packer.php");
56
require_once("$CFG->libdir/filebrowser/file_browser.php");
57
 
58
/**
59
 * Encodes file serving url
60
 *
61
 * @deprecated use moodle_url factory methods instead
62
 *
63
 * @todo MDL-31071 deprecate this function
64
 * @global stdClass $CFG
65
 * @param string $urlbase
66
 * @param string $path /filearea/itemid/dir/dir/file.exe
67
 * @param bool $forcedownload
68
 * @param bool $https https url required
69
 * @return string encoded file url
70
 */
71
function file_encode_url($urlbase, $path, $forcedownload=false, $https=false) {
72
    global $CFG;
73
 
74
//TODO: deprecate this
75
 
76
    if ($CFG->slasharguments) {
77
        $parts = explode('/', $path);
78
        $parts = array_map('rawurlencode', $parts);
79
        $path  = implode('/', $parts);
80
        $return = $urlbase.$path;
81
        if ($forcedownload) {
82
            $return .= '?forcedownload=1';
83
        }
84
    } else {
85
        $path = rawurlencode($path);
86
        $return = $urlbase.'?file='.$path;
87
        if ($forcedownload) {
88
            $return .= '&amp;forcedownload=1';
89
        }
90
    }
91
 
92
    if ($https) {
93
        $return = str_replace('http://', 'https://', $return);
94
    }
95
 
96
    return $return;
97
}
98
 
99
/**
100
 * Detects if area contains subdirs,
101
 * this is intended for file areas that are attached to content
102
 * migrated from 1.x where subdirs were allowed everywhere.
103
 *
104
 * @param context $context
105
 * @param string $component
106
 * @param string $filearea
107
 * @param string $itemid
108
 * @return bool
109
 */
110
function file_area_contains_subdirs(context $context, $component, $filearea, $itemid) {
111
    global $DB;
112
 
113
    if (!isset($itemid)) {
114
        // Not initialised yet.
115
        return false;
116
    }
117
 
118
    // Detect if any directories are already present, this is necessary for content upgraded from 1.x.
119
    $select = "contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid AND filepath <> '/' AND filename = '.'";
120
    $params = array('contextid'=>$context->id, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid);
121
    return $DB->record_exists_select('files', $select, $params);
122
}
123
 
124
/**
125
 * Prepares 'editor' formslib element from data in database
126
 *
127
 * The passed $data record must contain field foobar, foobarformat and optionally foobartrust. This
128
 * function then copies the embedded files into draft area (assigning itemids automatically),
129
 * creates the form element foobar_editor and rewrites the URLs so the embedded images can be
130
 * displayed.
131
 * In your mform definition, you must have an 'editor' element called foobar_editor. Then you call
132
 * your mform's set_data() supplying the object returned by this function.
133
 *
134
 * @category files
135
 * @param stdClass $data database field that holds the html text with embedded media
136
 * @param string $field the name of the database field that holds the html text with embedded media
137
 * @param array $options editor options (like maxifiles, maxbytes etc.)
138
 * @param stdClass $context context of the editor
139
 * @param string $component
140
 * @param string $filearea file area name
141
 * @param int $itemid item id, required if item exists
142
 * @return stdClass modified data object
143
 */
144
function file_prepare_standard_editor($data, $field, array $options, $context=null, $component=null, $filearea=null, $itemid=null) {
145
    $options = (array)$options;
146
    if (!isset($options['trusttext'])) {
147
        $options['trusttext'] = false;
148
    }
149
    if (!isset($options['forcehttps'])) {
150
        $options['forcehttps'] = false;
151
    }
152
    if (!isset($options['subdirs'])) {
153
        $options['subdirs'] = false;
154
    }
155
    if (!isset($options['maxfiles'])) {
156
        $options['maxfiles'] = 0; // no files by default
157
    }
158
    if (!isset($options['noclean'])) {
159
        $options['noclean'] = false;
160
    }
161
 
162
    //sanity check for passed context. This function doesn't expect $option['context'] to be set
163
    //But this function is called before creating editor hence, this is one of the best places to check
164
    //if context is used properly. This check notify developer that they missed passing context to editor.
165
    if (isset($context) && !isset($options['context'])) {
166
        //if $context is not null then make sure $option['context'] is also set.
167
        debugging('Context for editor is not set in editoroptions. Hence editor will not respect editor filters', DEBUG_DEVELOPER);
168
    } else if (isset($options['context']) && isset($context)) {
169
        //If both are passed then they should be equal.
170
        if ($options['context']->id != $context->id) {
171
            $exceptionmsg = 'Editor context ['.$options['context']->id.'] is not equal to passed context ['.$context->id.']';
172
            throw new coding_exception($exceptionmsg);
173
        }
174
    }
175
 
176
    if (is_null($itemid) or is_null($context)) {
177
        $contextid = null;
178
        $itemid = null;
179
        if (!isset($data)) {
180
            $data = new stdClass();
181
        }
182
        if (!isset($data->{$field})) {
183
            $data->{$field} = '';
184
        }
185
        if (!isset($data->{$field.'format'})) {
186
            $data->{$field.'format'} = editors_get_preferred_format();
187
        }
188
        if (!$options['noclean']) {
189
            if ($data->{$field.'format'} != FORMAT_MARKDOWN) {
190
                $data->{$field} = clean_text($data->{$field}, $data->{$field . 'format'});
191
            }
192
        }
193
 
194
    } else {
195
        if ($options['trusttext']) {
196
            // noclean ignored if trusttext enabled
197
            if (!isset($data->{$field.'trust'})) {
198
                $data->{$field.'trust'} = 0;
199
            }
200
            $data = trusttext_pre_edit($data, $field, $context);
201
        } else {
202
            if (!$options['noclean']) {
203
                // We do not have a way to sanitise Markdown texts,
204
                // luckily editors for this format should not have XSS problems.
205
                if ($data->{$field.'format'} != FORMAT_MARKDOWN) {
206
                    $data->{$field} = clean_text($data->{$field}, $data->{$field.'format'});
207
                }
208
            }
209
        }
210
        $contextid = $context->id;
211
    }
212
 
213
    if ($options['maxfiles'] != 0) {
214
        $draftid_editor = file_get_submitted_draft_itemid($field);
215
        $currenttext = file_prepare_draft_area($draftid_editor, $contextid, $component, $filearea, $itemid, $options, $data->{$field});
216
        $data->{$field.'_editor'} = array('text'=>$currenttext, 'format'=>$data->{$field.'format'}, 'itemid'=>$draftid_editor);
217
    } else {
218
        $data->{$field.'_editor'} = array('text'=>$data->{$field}, 'format'=>$data->{$field.'format'}, 'itemid'=>0);
219
    }
220
 
221
    return $data;
222
}
223
 
224
/**
225
 * Prepares the content of the 'editor' form element with embedded media files to be saved in database
226
 *
227
 * This function moves files from draft area to the destination area and
228
 * encodes URLs to the draft files so they can be safely saved into DB. The
229
 * form has to contain the 'editor' element named foobar_editor, where 'foobar'
230
 * is the name of the database field to hold the wysiwyg editor content. The
231
 * editor data comes as an array with text, format and itemid properties. This
232
 * function automatically adds $data properties foobar, foobarformat and
233
 * foobartrust, where foobar has URL to embedded files encoded.
234
 *
235
 * @category files
236
 * @param stdClass $data raw data submitted by the form
237
 * @param string $field name of the database field containing the html with embedded media files
238
 * @param array $options editor options (trusttext, subdirs, maxfiles, maxbytes etc.)
239
 * @param stdClass $context context, required for existing data
240
 * @param string $component file component
241
 * @param string $filearea file area name
242
 * @param int $itemid item id, required if item exists
243
 * @return stdClass modified data object
244
 */
245
function file_postupdate_standard_editor($data, $field, array $options, $context, $component=null, $filearea=null, $itemid=null) {
246
    $options = (array)$options;
247
    if (!isset($options['trusttext'])) {
248
        $options['trusttext'] = false;
249
    }
250
    if (!isset($options['forcehttps'])) {
251
        $options['forcehttps'] = false;
252
    }
253
    if (!isset($options['subdirs'])) {
254
        $options['subdirs'] = false;
255
    }
256
    if (!isset($options['maxfiles'])) {
257
        $options['maxfiles'] = 0; // no files by default
258
    }
259
    if (!isset($options['maxbytes'])) {
260
        $options['maxbytes'] = 0; // unlimited
261
    }
262
    if (!isset($options['removeorphaneddrafts'])) {
263
        $options['removeorphaneddrafts'] = false; // Don't remove orphaned draft files by default.
264
    }
265
 
266
    if ($options['trusttext']) {
267
        $data->{$field.'trust'} = trusttext_trusted($context);
268
    } else {
269
        $data->{$field.'trust'} = 0;
270
    }
271
 
272
    $editor = $data->{$field.'_editor'};
273
 
274
    if ($options['maxfiles'] == 0 or is_null($filearea) or is_null($itemid) or empty($editor['itemid'])) {
275
        $data->{$field} = $editor['text'];
276
    } else {
277
        // Clean the user drafts area of any files not referenced in the editor text.
278
        if ($options['removeorphaneddrafts']) {
279
            file_remove_editor_orphaned_files($editor);
280
        }
281
        $data->{$field} = file_save_draft_area_files($editor['itemid'], $context->id, $component, $filearea, $itemid, $options, $editor['text'], $options['forcehttps']);
282
    }
283
    $data->{$field.'format'} = $editor['format'];
284
 
285
    return $data;
286
}
287
 
288
/**
289
 * Saves text and files modified by Editor formslib element
290
 *
291
 * @category files
292
 * @param stdClass $data $database entry field
293
 * @param string $field name of data field
294
 * @param array $options various options
295
 * @param stdClass $context context - must already exist
296
 * @param string $component
297
 * @param string $filearea file area name
298
 * @param int $itemid must already exist, usually means data is in db
299
 * @return stdClass modified data obejct
300
 */
301
function file_prepare_standard_filemanager($data, $field, array $options, $context=null, $component=null, $filearea=null, $itemid=null) {
302
    $options = (array)$options;
303
    if (!isset($options['subdirs'])) {
304
        $options['subdirs'] = false;
305
    }
306
    if (is_null($itemid) or is_null($context)) {
307
        $itemid = null;
308
        $contextid = null;
309
    } else {
310
        $contextid = $context->id;
311
    }
312
 
313
    $draftid_editor = file_get_submitted_draft_itemid($field.'_filemanager');
314
    file_prepare_draft_area($draftid_editor, $contextid, $component, $filearea, $itemid, $options);
315
    $data->{$field.'_filemanager'} = $draftid_editor;
316
 
317
    return $data;
318
}
319
 
320
/**
321
 * Saves files modified by File manager formslib element
322
 *
323
 * @todo MDL-31073 review this function
324
 * @category files
325
 * @param stdClass $data $database entry field
326
 * @param string $field name of data field
327
 * @param array $options various options
328
 * @param stdClass $context context - must already exist
329
 * @param string $component
330
 * @param string $filearea file area name
331
 * @param int $itemid must already exist, usually means data is in db
332
 * @return stdClass modified data obejct
333
 */
334
function file_postupdate_standard_filemanager($data, $field, array $options, $context, $component, $filearea, $itemid) {
335
    $options = (array)$options;
336
    if (!isset($options['subdirs'])) {
337
        $options['subdirs'] = false;
338
    }
339
    if (!isset($options['maxfiles'])) {
340
        $options['maxfiles'] = -1; // unlimited
341
    }
342
    if (!isset($options['maxbytes'])) {
343
        $options['maxbytes'] = 0; // unlimited
344
    }
345
 
346
    if (empty($data->{$field.'_filemanager'})) {
347
        $data->$field = '';
348
 
349
    } else {
350
        file_save_draft_area_files($data->{$field.'_filemanager'}, $context->id, $component, $filearea, $itemid, $options);
351
        $fs = get_file_storage();
352
 
353
        if ($fs->get_area_files($context->id, $component, $filearea, $itemid)) {
354
            $data->$field = '1'; // TODO: this is an ugly hack (skodak)
355
        } else {
356
            $data->$field = '';
357
        }
358
    }
359
 
360
    return $data;
361
}
362
 
363
/**
364
 * Generate a draft itemid
365
 *
366
 * @category files
367
 * @global moodle_database $DB
368
 * @global stdClass $USER
369
 * @return int a random but available draft itemid that can be used to create a new draft
370
 * file area.
371
 */
372
function file_get_unused_draft_itemid() {
373
    global $DB, $USER;
374
 
375
    if (isguestuser() or !isloggedin()) {
376
        // guests and not-logged-in users can not be allowed to upload anything!!!!!!
377
        throw new \moodle_exception('noguest');
378
    }
379
 
380
    $contextid = context_user::instance($USER->id)->id;
381
 
382
    $fs = get_file_storage();
383
    $draftitemid = rand(1, 999999999);
384
    while ($files = $fs->get_area_files($contextid, 'user', 'draft', $draftitemid)) {
385
        $draftitemid = rand(1, 999999999);
386
    }
387
 
388
    return $draftitemid;
389
}
390
 
391
/**
392
 * Initialise a draft file area from a real one by copying the files. A draft
393
 * area will be created if one does not already exist. Normally you should
394
 * get $draftitemid by calling file_get_submitted_draft_itemid('elementname');
395
 *
396
 * @category files
397
 * @global stdClass $CFG
398
 * @global stdClass $USER
399
 * @param int $draftitemid the id of the draft area to use, or 0 to create a new one, in which case this parameter is updated.
400
 * @param int $contextid This parameter and the next two identify the file area to copy files from.
401
 * @param string $component
402
 * @param string $filearea helps indentify the file area.
403
 * @param int $itemid helps identify the file area. Can be null if there are no files yet.
404
 * @param array $options text and file options ('subdirs'=>false, 'forcehttps'=>false)
405
 * @param string $text some html content that needs to have embedded links rewritten to point to the draft area.
406
 * @return string|null returns string if $text was passed in, the rewritten $text is returned. Otherwise NULL.
407
 */
1441 ariadna 408
function file_prepare_draft_area(&$draftitemid, $contextid, $component, $filearea, $itemid, ?array $options=null, $text=null) {
1 efrain 409
    global $CFG, $USER;
410
 
411
    $options = (array)$options;
412
    if (!isset($options['subdirs'])) {
413
        $options['subdirs'] = false;
414
    }
415
    if (!isset($options['forcehttps'])) {
416
        $options['forcehttps'] = false;
417
    }
418
 
419
    $usercontext = context_user::instance($USER->id);
420
    $fs = get_file_storage();
421
 
422
    if (empty($draftitemid)) {
423
        // create a new area and copy existing files into
424
        $draftitemid = file_get_unused_draft_itemid();
425
        $file_record = array('contextid'=>$usercontext->id, 'component'=>'user', 'filearea'=>'draft', 'itemid'=>$draftitemid);
426
        if (!is_null($itemid) and $files = $fs->get_area_files($contextid, $component, $filearea, $itemid)) {
427
            foreach ($files as $file) {
428
                if ($file->is_directory() and $file->get_filepath() === '/') {
429
                    // we need a way to mark the age of each draft area,
430
                    // by not copying the root dir we force it to be created automatically with current timestamp
431
                    continue;
432
                }
433
                if (!$options['subdirs'] and ($file->is_directory() or $file->get_filepath() !== '/')) {
434
                    continue;
435
                }
436
                $draftfile = $fs->create_file_from_storedfile($file_record, $file);
437
                // XXX: This is a hack for file manager (MDL-28666)
438
                // File manager needs to know the original file information before copying
439
                // to draft area, so we append these information in mdl_files.source field
440
                // {@link file_storage::search_references()}
441
                // {@link file_storage::search_references_count()}
442
                $sourcefield = $file->get_source();
443
                $newsourcefield = new stdClass;
444
                $newsourcefield->source = $sourcefield;
445
                $original = new stdClass;
446
                $original->contextid = $contextid;
447
                $original->component = $component;
448
                $original->filearea  = $filearea;
449
                $original->itemid    = $itemid;
450
                $original->filename  = $file->get_filename();
451
                $original->filepath  = $file->get_filepath();
452
                $newsourcefield->original = file_storage::pack_reference($original);
453
                $draftfile->set_source(serialize($newsourcefield));
454
                // End of file manager hack
455
            }
456
        }
457
        if (!is_null($text)) {
458
            // at this point there should not be any draftfile links yet,
459
            // because this is a new text from database that should still contain the @@pluginfile@@ links
460
            // this happens when developers forget to post process the text
461
            $text = str_replace("\"$CFG->wwwroot/draftfile.php", "\"$CFG->wwwroot/brokenfile.php#", $text);
462
        }
463
    } else {
464
        // nothing to do
465
    }
466
 
467
    if (is_null($text)) {
468
        return null;
469
    }
470
 
471
    // relink embedded files - editor can not handle @@PLUGINFILE@@ !
472
    return file_rewrite_pluginfile_urls($text, 'draftfile.php', $usercontext->id, 'user', 'draft', $draftitemid, $options);
473
}
474
 
475
/**
476
 * Convert encoded URLs in $text from the @@PLUGINFILE@@/... form to an actual URL.
477
 * Passing a new option reverse = true in the $options var will make the function to convert actual URLs in $text to encoded URLs
478
 * in the @@PLUGINFILE@@ form.
479
 *
480
 * @param   string  $text The content that may contain ULRs in need of rewriting.
481
 * @param   string  $file The script that should be used to serve these files. pluginfile.php, draftfile.php, etc.
482
 * @param   int     $contextid This parameter and the next two identify the file area to use.
483
 * @param   string  $component
484
 * @param   string  $filearea helps identify the file area.
485
 * @param   ?int    $itemid helps identify the file area.
486
 * @param   array   $options
487
 *          bool    $options.forcehttps Force the user of https
488
 *          bool    $options.reverse Reverse the behaviour of the function
489
 *          mixed   $options.includetoken Use a token for authentication. True for current user, int value for other user id.
490
 *          string  The processed text.
491
 */
1441 ariadna 492
function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $filearea, $itemid, ?array $options=null) {
1 efrain 493
    global $CFG, $USER;
494
 
495
    $options = (array)$options;
496
    if (!isset($options['forcehttps'])) {
497
        $options['forcehttps'] = false;
498
    }
499
 
500
    $baseurl = "{$CFG->wwwroot}/{$file}";
501
    if (!empty($options['includetoken'])) {
502
        $userid = $options['includetoken'] === true ? $USER->id : $options['includetoken'];
503
        $token = get_user_key('core_files', $userid);
504
        $finalfile = basename($file);
505
        $tokenfile = "token{$finalfile}";
506
        $file = substr($file, 0, strlen($file) - strlen($finalfile)) . $tokenfile;
507
        $baseurl = "{$CFG->wwwroot}/{$file}";
508
 
509
        if (!$CFG->slasharguments) {
510
            $baseurl .= "?token={$token}&file=";
511
        } else {
512
            $baseurl .= "/{$token}";
513
        }
514
    }
515
 
516
    $baseurl .= "/{$contextid}/{$component}/{$filearea}/";
517
 
518
    if ($itemid !== null) {
519
        $baseurl .= "$itemid/";
520
    }
521
 
522
    if ($options['forcehttps']) {
523
        $baseurl = str_replace('http://', 'https://', $baseurl);
524
    }
525
 
526
    if (!empty($options['reverse'])) {
527
        return str_replace($baseurl, '@@PLUGINFILE@@/', $text ?? '');
528
    } else {
529
        return str_replace('@@PLUGINFILE@@/', $baseurl, $text ?? '');
530
    }
531
}
532
 
533
/**
534
 * Returns information about files in a draft area.
535
 *
536
 * @global stdClass $CFG
537
 * @global stdClass $USER
538
 * @param int $draftitemid the draft area item id.
539
 * @param string $filepath path to the directory from which the information have to be retrieved.
540
 * @return array with the following entries:
541
 *      'filecount' => number of files in the draft area.
542
 *      'filesize' => total size of the files in the draft area.
543
 *      'foldercount' => number of folders in the draft area.
544
 *      'filesize_without_references' => total size of the area excluding file references.
545
 * (more information will be added as needed).
546
 */
547
function file_get_draft_area_info($draftitemid, $filepath = '/') {
548
    global $USER;
549
 
550
    $usercontext = context_user::instance($USER->id);
551
    return file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid, $filepath);
552
}
553
 
554
/**
555
 * Returns information about files in an area.
556
 *
557
 * @param int $contextid context id
558
 * @param string $component component
559
 * @param string $filearea file area name
560
 * @param int $itemid item id or all files if not specified
561
 * @param string $filepath path to the directory from which the information have to be retrieved.
562
 * @return array with the following entries:
563
 *      'filecount' => number of files in the area.
564
 *      'filesize' => total size of the files in the area.
565
 *      'foldercount' => number of folders in the area.
566
 *      'filesize_without_references' => total size of the area excluding file references.
567
 * @since Moodle 3.4
568
 */
569
function file_get_file_area_info($contextid, $component, $filearea, $itemid = 0, $filepath = '/') {
570
 
571
    $fs = get_file_storage();
572
 
573
    $results = array(
574
        'filecount' => 0,
575
        'foldercount' => 0,
576
        'filesize' => 0,
577
        'filesize_without_references' => 0
578
    );
579
 
580
    $draftfiles = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath, true, true);
581
 
582
    foreach ($draftfiles as $file) {
583
        if ($file->is_directory()) {
584
            $results['foldercount'] += 1;
585
        } else {
586
            $results['filecount'] += 1;
587
        }
588
 
589
        $filesize = $file->get_filesize();
590
        $results['filesize'] += $filesize;
591
        if (!$file->is_external_file()) {
592
            $results['filesize_without_references'] += $filesize;
593
        }
594
    }
595
 
596
    return $results;
597
}
598
 
599
/**
600
 * Returns whether a draft area has exceeded/will exceed its size limit.
601
 *
602
 * Please note that the unlimited value for $areamaxbytes is -1 {@link FILE_AREA_MAX_BYTES_UNLIMITED}, not 0.
603
 *
604
 * @param int $draftitemid the draft area item id.
605
 * @param int $areamaxbytes the maximum size allowed in this draft area.
606
 * @param int $newfilesize the size that would be added to the current area.
607
 * @param bool $includereferences true to include the size of the references in the area size.
608
 * @return bool true if the area will/has exceeded its limit.
609
 * @since Moodle 2.4
610
 */
611
function file_is_draft_area_limit_reached($draftitemid, $areamaxbytes, $newfilesize = 0, $includereferences = false) {
612
    if ($areamaxbytes != FILE_AREA_MAX_BYTES_UNLIMITED) {
613
        $draftinfo = file_get_draft_area_info($draftitemid);
614
        $areasize = $draftinfo['filesize_without_references'];
615
        if ($includereferences) {
616
            $areasize = $draftinfo['filesize'];
617
        }
618
        if ($areasize + $newfilesize > $areamaxbytes) {
619
            return true;
620
        }
621
    }
622
    return false;
623
}
624
 
625
/**
626
 * Returns whether a user has reached their draft area upload rate.
627
 *
628
 * @param int $userid The user id
629
 * @return bool
630
 */
631
function file_is_draft_areas_limit_reached(int $userid): bool {
632
    global $CFG;
633
 
634
    $capacity = $CFG->draft_area_bucket_capacity ?? DRAFT_AREA_BUCKET_CAPACITY;
635
    $leak = $CFG->draft_area_bucket_leak ?? DRAFT_AREA_BUCKET_LEAK;
636
 
637
    $since = time() - floor($capacity / $leak); // The items that were in the bucket before this time are already leaked by now.
638
                                                // We are going to be a bit generous to the user when using the leaky bucket
639
                                                // algorithm below. We are going to assume that the bucket is empty at $since.
640
                                                // We have to do an assumption here unless we really want to get ALL user's draft
641
                                                // items without any limit and put all of them in the leaking bucket.
642
                                                // I decided to favour performance over accuracy here.
643
 
644
    $fs = get_file_storage();
645
    $items = $fs->get_user_draft_items($userid, $since);
646
    $items = array_reverse($items); // So that the items are sorted based on time in the ascending direction.
647
 
648
    // We only need to store the time that each element in the bucket is going to leak. So $bucket is array of leaking times.
649
    $bucket = [];
650
    foreach ($items as $item) {
651
        $now = $item->timemodified;
652
        // First let's see if items can be dropped from the bucket as a result of leakage.
653
        while (!empty($bucket) && ($now >= $bucket[0])) {
654
            array_shift($bucket);
655
        }
656
 
657
        // Calculate the time that the new item we put into the bucket will be leaked from it, and store it into the bucket.
658
        if ($bucket) {
659
            $bucket[] = max($bucket[count($bucket) - 1], $now) + (1 / $leak);
660
        } else {
661
            $bucket[] = $now + (1 / $leak);
662
        }
663
    }
664
 
665
    // Recalculate the bucket's content based on the leakage until now.
666
    $now = time();
667
    while (!empty($bucket) && ($now >= $bucket[0])) {
668
        array_shift($bucket);
669
    }
670
 
671
    return count($bucket) >= $capacity;
672
}
673
 
674
/**
675
 * Get used space of files
676
 * @global moodle_database $DB
677
 * @global stdClass $USER
678
 * @return int total bytes
679
 */
680
function file_get_user_used_space() {
681
    global $DB, $USER;
682
 
683
    $usercontext = context_user::instance($USER->id);
684
    $sql = "SELECT SUM(files1.filesize) AS totalbytes FROM {files} files1
685
            JOIN (SELECT contenthash, filename, MAX(id) AS id
686
            FROM {files}
687
            WHERE contextid = ? AND component = ? AND filearea != ?
688
            GROUP BY contenthash, filename) files2 ON files1.id = files2.id";
689
    $params = array('contextid'=>$usercontext->id, 'component'=>'user', 'filearea'=>'draft');
690
    $record = $DB->get_record_sql($sql, $params);
691
    return (int)$record->totalbytes;
692
}
693
 
694
/**
695
 * Convert any string to a valid filepath
696
 * @todo review this function
697
 * @param string $str
698
 * @return string path
699
 */
700
function file_correct_filepath($str) { //TODO: what is this? (skodak) - No idea (Fred)
701
    if ($str == '/' or empty($str)) {
702
        return '/';
703
    } else {
704
        return '/'.trim($str, '/').'/';
705
    }
706
}
707
 
708
/**
709
 * Generate a folder tree of draft area of current USER recursively
710
 *
711
 * @todo MDL-31073 use normal return value instead, this does not fit the rest of api here (skodak)
712
 * @param int $draftitemid
713
 * @param string $filepath
714
 * @param mixed $data
715
 */
716
function file_get_drafarea_folders($draftitemid, $filepath, &$data) {
717
    global $USER, $OUTPUT, $CFG;
718
    $data->children = array();
719
    $context = context_user::instance($USER->id);
720
    $fs = get_file_storage();
721
    if ($files = $fs->get_directory_files($context->id, 'user', 'draft', $draftitemid, $filepath, false)) {
722
        foreach ($files as $file) {
723
            if ($file->is_directory()) {
724
                $item = new stdClass();
725
                $item->sortorder = $file->get_sortorder();
726
                $item->filepath = $file->get_filepath();
727
 
728
                $foldername = explode('/', trim($item->filepath, '/'));
729
                $item->fullname = trim(array_pop($foldername), '/');
730
 
731
                $item->id = uniqid();
732
                file_get_drafarea_folders($draftitemid, $item->filepath, $item);
733
                $data->children[] = $item;
734
            } else {
735
                continue;
736
            }
737
        }
738
    }
739
}
740
 
741
/**
742
 * Listing all files (including folders) in current path (draft area)
743
 * used by file manager
744
 * @param int $draftitemid
745
 * @param string $filepath
746
 * @return stdClass
747
 */
748
function file_get_drafarea_files($draftitemid, $filepath = '/') {
749
    global $USER, $OUTPUT, $CFG;
750
 
751
    $context = context_user::instance($USER->id);
752
    $fs = get_file_storage();
753
 
754
    $data = new stdClass();
755
    $data->path = array();
756
    $data->path[] = array('name'=>get_string('files'), 'path'=>'/');
757
 
758
    // will be used to build breadcrumb
759
    $trail = '/';
760
    if ($filepath !== '/') {
761
        $filepath = file_correct_filepath($filepath);
762
        $parts = explode('/', $filepath);
763
        foreach ($parts as $part) {
764
            if ($part != '' && $part != null) {
765
                $trail .= ($part.'/');
766
                $data->path[] = array('name'=>$part, 'path'=>$trail);
767
            }
768
        }
769
    }
770
 
771
    $list = array();
772
    $maxlength = 12;
773
    if ($files = $fs->get_directory_files($context->id, 'user', 'draft', $draftitemid, $filepath, false)) {
774
        foreach ($files as $file) {
775
            $item = new stdClass();
776
            $item->filename = $file->get_filename();
777
            $item->filepath = $file->get_filepath();
778
            $item->fullname = trim($item->filename, '/');
779
            $filesize = $file->get_filesize();
780
            $item->size = $filesize ? $filesize : null;
781
            $item->filesize = $filesize ? display_size($filesize) : '';
782
 
783
            $item->sortorder = $file->get_sortorder();
784
            $item->author = $file->get_author();
785
            $item->license = $file->get_license();
786
            $item->datemodified = $file->get_timemodified();
787
            $item->datecreated = $file->get_timecreated();
788
            $item->isref = $file->is_external_file();
789
            if ($item->isref && $file->get_status() == 666) {
790
                $item->originalmissing = true;
791
            }
792
            // find the file this draft file was created from and count all references in local
793
            // system pointing to that file
794
            $source = @unserialize($file->get_source() ?? '');
795
            if (isset($source->original)) {
796
                $item->refcount = $fs->search_references_count($source->original);
797
            }
798
 
799
            if ($file->is_directory()) {
800
                $item->filesize = 0;
801
                $item->icon = $OUTPUT->image_url(file_folder_icon())->out(false);
802
                $item->type = 'folder';
803
                $foldername = explode('/', trim($item->filepath, '/'));
804
                $item->fullname = trim(array_pop($foldername), '/');
805
                $item->thumbnail = $OUTPUT->image_url(file_folder_icon())->out(false);
806
            } else {
807
                // do NOT use file browser here!
808
                $item->mimetype = get_mimetype_description($file);
809
                if (file_extension_in_typegroup($file->get_filename(), 'archive')) {
810
                    $item->type = 'zip';
811
                } else {
812
                    $item->type = 'file';
813
                }
814
                $itemurl = moodle_url::make_draftfile_url($draftitemid, $item->filepath, $item->filename);
815
                $item->url = $itemurl->out();
816
                $item->icon = $OUTPUT->image_url(file_file_icon($file))->out(false);
817
                $item->thumbnail = $OUTPUT->image_url(file_file_icon($file))->out(false);
818
 
819
                // The call to $file->get_imageinfo() fails with an exception if the file can't be read on the file system.
820
                // We still want to add such files to the list, so the owner can view and delete them if needed. So, we only call
821
                // get_imageinfo() on files that can be read, and we also spoof the file status based on whether it was found.
822
                // We'll use the same status types used by stored_file->get_status(), where 0 = OK. 1 = problem, as these will be
823
                // used by the widget to display a warning about the problem files.
824
                // The value of stored_file->get_status(), and the file record are unaffected by this. It's only superficially set.
825
                $item->status = $fs->get_file_system()->is_file_readable_remotely_by_storedfile($file) ? 0 : 1;
826
                if ($item->status == 0) {
827
                    if ($imageinfo = $file->get_imageinfo()) {
828
                        $item->realthumbnail = $itemurl->out(false, array('preview' => 'thumb',
829
                            'oid' => $file->get_timemodified()));
830
                        $item->realicon = $itemurl->out(false, array('preview' => 'tinyicon', 'oid' => $file->get_timemodified()));
831
                        $item->image_width = $imageinfo['width'];
832
                        $item->image_height = $imageinfo['height'];
833
                    }
834
                }
835
            }
836
            $list[] = $item;
837
        }
838
    }
839
    $data->itemid = $draftitemid;
840
    $data->list = $list;
841
    return $data;
842
}
843
 
844
/**
845
 * Returns all of the files in the draftarea.
846
 *
847
 * @param  int $draftitemid The draft item ID
848
 * @param  string $filepath path for the uploaded files.
849
 * @return array An array of files associated with this draft item id.
850
 */
851
function file_get_all_files_in_draftarea(int $draftitemid, string $filepath = '/'): array {
852
    $files = [];
853
    $draftfiles = file_get_drafarea_files($draftitemid, $filepath);
854
    file_get_drafarea_folders($draftitemid, $filepath, $draftfiles);
855
 
856
    if (!empty($draftfiles)) {
857
        foreach ($draftfiles->list as $draftfile) {
1441 ariadna 858
            if ($draftfile->type !== 'folder') {
1 efrain 859
                $files[] = $draftfile;
860
            }
861
        }
862
 
863
        if (isset($draftfiles->children)) {
864
            foreach ($draftfiles->children as $draftfile) {
865
                $files = array_merge($files, file_get_all_files_in_draftarea($draftitemid, $draftfile->filepath));
866
            }
867
        }
868
    }
869
    return $files;
870
}
871
 
872
/**
873
 * Returns draft area itemid for a given element.
874
 *
875
 * @category files
876
 * @param string $elname name of formlib editor element, or a hidden form field that stores the draft area item id, etc.
877
 * @return int the itemid, or 0 if there is not one yet.
878
 */
879
function file_get_submitted_draft_itemid($elname) {
880
    // this is a nasty hack, ideally all new elements should use arrays here or there should be a new parameter
881
    if (!isset($_REQUEST[$elname])) {
882
        return 0;
883
    }
884
    if (is_array($_REQUEST[$elname])) {
885
        $param = optional_param_array($elname, 0, PARAM_INT);
886
        if (!empty($param['itemid'])) {
887
            $param = $param['itemid'];
888
        } else {
889
            debugging('Missing itemid, maybe caused by unset maxfiles option', DEBUG_DEVELOPER);
890
            return false;
891
        }
892
 
893
    } else {
894
        $param = optional_param($elname, 0, PARAM_INT);
895
    }
896
 
897
    if ($param) {
898
        require_sesskey();
899
    }
900
 
901
    return $param;
902
}
903
 
904
/**
905
 * Restore the original source field from draft files
906
 *
907
 * Do not use this function because it makes field files.source inconsistent
908
 * for draft area files. This function will be deprecated in 2.6
909
 *
910
 * @param stored_file $storedfile This only works with draft files
911
 * @return stored_file
912
 */
913
function file_restore_source_field_from_draft_file($storedfile) {
914
    $source = @unserialize($storedfile->get_source() ?? '');
915
    if (!empty($source)) {
916
        if (is_object($source)) {
917
            $restoredsource = $source->source;
918
            $storedfile->set_source($restoredsource);
919
        } else {
920
            throw new moodle_exception('invalidsourcefield', 'error');
921
        }
922
    }
923
    return $storedfile;
924
}
925
 
926
/**
927
 * Removes those files from the user drafts filearea which are not referenced in the editor text.
928
 *
929
 * @param array $editor The online text editor element from the submitted form data.
930
 */
931
function file_remove_editor_orphaned_files($editor) {
932
    global $CFG, $USER;
933
 
934
    // Find those draft files included in the text, and generate their hashes.
935
    $context = context_user::instance($USER->id);
936
    $baseurl = $CFG->wwwroot . '/draftfile.php/' . $context->id . '/user/draft/' . $editor['itemid'] . '/';
937
    $pattern = "/" . preg_quote($baseurl, '/') . "(.+?)[\?\"'<>\s:\\\\]/";
938
    preg_match_all($pattern, $editor['text'], $matches);
939
    $usedfilehashes = [];
940
    foreach ($matches[1] as $matchedfilename) {
941
        $matchedfilename = urldecode($matchedfilename);
942
        $usedfilehashes[] = \file_storage::get_pathname_hash($context->id, 'user', 'draft', $editor['itemid'], '/',
943
                                                             $matchedfilename);
944
    }
945
 
946
    // Now, compare the hashes of all draft files, and remove those which don't match used files.
947
    $fs = get_file_storage();
948
    $files = $fs->get_area_files($context->id, 'user', 'draft', $editor['itemid'], 'id', false);
949
    foreach ($files as $file) {
950
        $tmphash = $file->get_pathnamehash();
951
        if (!in_array($tmphash, $usedfilehashes)) {
952
            $file->delete();
953
        }
954
    }
955
}
956
 
957
/**
958
 * Finds all draft areas used in a textarea and copies the files into the primary textarea. If a user copies and pastes
959
 * content from another draft area it's possible for a single textarea to reference multiple draft areas.
960
 *
961
 * @category files
962
 * @param int $draftitemid the id of the primary draft area.
963
 *            When set to -1 (probably, by a WebService) it won't process file merging, keeping the original state of the file area.
964
 * @param int $usercontextid the user's context id.
965
 * @param string $text some html content that needs to have files copied to the correct draft area.
966
 * @param bool $forcehttps force https urls.
967
 *
968
 * @return string $text html content modified with new draft links
969
 */
970
function file_merge_draft_areas($draftitemid, $usercontextid, $text, $forcehttps = false) {
971
    if (is_null($text)) {
972
        return null;
973
    }
974
 
975
    // Do not merge files, leave it as it was.
976
    if ($draftitemid === IGNORE_FILE_MERGE) {
977
        return null;
978
    }
979
 
980
    $urls = extract_draft_file_urls_from_text($text, $forcehttps, $usercontextid, 'user', 'draft');
981
 
982
    // No draft areas to rewrite.
983
    if (empty($urls)) {
984
        return $text;
985
    }
986
 
987
    foreach ($urls as $url) {
988
        // Do not process the "home" draft area.
989
        if ($url['itemid'] == $draftitemid) {
990
            continue;
991
        }
992
 
993
        // Decode the filename.
994
        $filename = urldecode($url['filename']);
995
 
996
        // Copy the file.
997
        file_copy_file_to_file_area($url, $filename, $draftitemid);
998
 
999
        // Rewrite draft area.
1000
        $text = file_replace_file_area_in_text($url, $draftitemid, $text, $forcehttps);
1001
    }
1002
    return $text;
1003
}
1004
 
1005
/**
1006
 * Rewrites a file area in arbitrary text.
1007
 *
1008
 * @param array $file General information about the file.
1009
 * @param int $newid The new file area itemid.
1010
 * @param string $text The text to rewrite.
1011
 * @param bool $forcehttps force https urls.
1012
 * @return string The rewritten text.
1013
 */
1014
function file_replace_file_area_in_text($file, $newid, $text, $forcehttps = false) {
1015
    global $CFG;
1016
 
1017
    $wwwroot = $CFG->wwwroot;
1018
    if ($forcehttps) {
1019
        $wwwroot = str_replace('http://', 'https://', $wwwroot);
1020
    }
1021
 
1022
    $search = [
1023
        $wwwroot,
1024
        $file['urlbase'],
1025
        $file['contextid'],
1026
        $file['component'],
1027
        $file['filearea'],
1028
        $file['itemid'],
1029
        $file['filename']
1030
    ];
1031
    $replace = [
1032
        $wwwroot,
1033
        $file['urlbase'],
1034
        $file['contextid'],
1035
        $file['component'],
1036
        $file['filearea'],
1037
        $newid,
1038
        $file['filename']
1039
    ];
1040
 
1041
    $text = str_ireplace( implode('/', $search), implode('/', $replace), $text);
1042
    return $text;
1043
}
1044
 
1045
/**
1046
 * Copies a file from one file area to another.
1047
 *
1048
 * @param array $file Information about the file to be copied.
1049
 * @param string $filename The filename.
1050
 * @param int $itemid The new file area.
1051
 */
1052
function file_copy_file_to_file_area($file, $filename, $itemid) {
1053
    $fs = get_file_storage();
1054
 
1055
    // Load the current file in the old draft area.
1056
    $fileinfo = array(
1057
        'component' => $file['component'],
1058
        'filearea' => $file['filearea'],
1059
        'itemid' => $file['itemid'],
1060
        'contextid' => $file['contextid'],
1061
        'filepath' => '/',
1062
        'filename' => $filename
1063
    );
1064
    $oldfile = $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'],
1065
        $fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);
1066
    $newfileinfo = array(
1067
        'component' => $file['component'],
1068
        'filearea' => $file['filearea'],
1069
        'itemid' => $itemid,
1070
        'contextid' => $file['contextid'],
1071
        'filepath' => '/',
1072
        'filename' => $filename
1073
    );
1074
 
1075
    $newcontextid = $newfileinfo['contextid'];
1076
    $newcomponent = $newfileinfo['component'];
1077
    $newfilearea = $newfileinfo['filearea'];
1078
    $newitemid = $newfileinfo['itemid'];
1079
    $newfilepath = $newfileinfo['filepath'];
1080
    $newfilename = $newfileinfo['filename'];
1081
 
1082
    // Check if the file exists.
1083
    if (!$fs->file_exists($newcontextid, $newcomponent, $newfilearea, $newitemid, $newfilepath, $newfilename)) {
1084
        $fs->create_file_from_storedfile($newfileinfo, $oldfile);
1085
    }
1086
}
1087
 
1088
/**
1089
 * Saves files from a draft file area to a real one (merging the list of files).
1090
 * Can rewrite URLs in some content at the same time if desired.
1091
 *
1092
 * @category files
1093
 * @global stdClass $USER
1094
 * @param int $draftitemid the id of the draft area to use. Normally obtained
1095
 *      from file_get_submitted_draft_itemid('elementname') or similar.
1096
 *      When set to -1 (probably, by a WebService) it won't process file merging, keeping the original state of the file area.
1097
 * @param int $contextid This parameter and the next two identify the file area to save to.
1098
 * @param string $component
1099
 * @param string $filearea indentifies the file area.
1100
 * @param int $itemid helps identifies the file area.
1101
 * @param array $options area options (subdirs=>false, maxfiles=-1, maxbytes=0)
1102
 * @param string $text some html content that needs to have embedded links rewritten
1103
 *      to the @@PLUGINFILE@@ form for saving in the database.
1104
 * @param bool $forcehttps force https urls.
1105
 * @return string|null if $text was passed in, the rewritten $text is returned. Otherwise NULL.
1106
 */
1441 ariadna 1107
function file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, ?array $options=null, $text=null, $forcehttps=false) {
1 efrain 1108
    global $USER;
1109
 
1110
    // Do not merge files, leave it as it was.
1111
    if ($draftitemid === IGNORE_FILE_MERGE) {
1112
        // Safely return $text, no need to rewrite pluginfile because this is mostly comming from an external client like the app.
1113
        return $text;
1114
    }
1115
 
1116
    if ($itemid === false) {
1117
        // Catch a potentially dangerous coding error.
1118
        throw new coding_exception('file_save_draft_area_files was called with $itemid false. ' .
1119
                "This suggests a bug, because it would wipe all ($contextid, $component, $filearea) files.");
1120
    }
1121
 
1122
    $usercontext = context_user::instance($USER->id);
1123
    $fs = get_file_storage();
1124
 
1125
    $options = (array)$options;
1126
    if (!isset($options['subdirs'])) {
1127
        $options['subdirs'] = false;
1128
    }
1129
    if (!isset($options['maxfiles'])) {
1130
        $options['maxfiles'] = -1; // unlimited
1131
    }
1132
    if (!isset($options['maxbytes']) || $options['maxbytes'] == USER_CAN_IGNORE_FILE_SIZE_LIMITS) {
1133
        $options['maxbytes'] = 0; // unlimited
1134
    }
1135
    if (!isset($options['areamaxbytes'])) {
1136
        $options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED; // Unlimited.
1137
    }
1138
    $allowreferences = true;
1139
    if (isset($options['return_types']) && !($options['return_types'] & (FILE_REFERENCE | FILE_CONTROLLED_LINK))) {
1140
        // we assume that if $options['return_types'] is NOT specified, we DO allow references.
1141
        // this is not exactly right. BUT there are many places in code where filemanager options
1142
        // are not passed to file_save_draft_area_files()
1143
        $allowreferences = false;
1144
    }
1145
 
1146
    // Check if the user has copy-pasted from other draft areas. Those files will be located in different draft
1147
    // areas and need to be copied into the current draft area.
1148
    $text = file_merge_draft_areas($draftitemid, $usercontext->id, $text, $forcehttps);
1149
 
1150
    // Check if the draft area has exceeded the authorised limit. This should never happen as validation
1151
    // should have taken place before, unless the user is doing something nauthly. If so, let's just not save
1152
    // anything at all in the next area.
1153
    if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {
1154
        return null;
1155
    }
1156
 
1157
    $draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');
1158
    $oldfiles   = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id');
1159
 
1160
    // One file in filearea means it is empty (it has only top-level directory '.').
1161
    if (count($draftfiles) > 1 || count($oldfiles) > 1) {
1162
        // we have to merge old and new files - we want to keep file ids for files that were not changed
1163
        // we change time modified for all new and changed files, we keep time created as is
1164
 
1165
        $newhashes = array();
1166
        $filecount = 0;
1167
        $context = context::instance_by_id($contextid, MUST_EXIST);
1168
        foreach ($draftfiles as $file) {
1169
            if (!$options['subdirs'] && $file->get_filepath() !== '/') {
1170
                continue;
1171
            }
1172
            if (!$allowreferences && $file->is_external_file()) {
1173
                continue;
1174
            }
1175
            if (!$file->is_directory()) {
1176
                // Check to see if this file was uploaded by someone who can ignore the file size limits.
1177
                $fileusermaxbytes = get_user_max_upload_file_size($context, $options['maxbytes'], 0, 0, $file->get_userid());
1178
                if ($fileusermaxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS
1179
                        && ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize())) {
1180
                    // Oversized file.
1181
                    continue;
1182
                }
1183
                if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) {
1184
                    // more files - should not get here at all
1185
                    continue;
1186
                }
1187
                $filecount++;
1188
            }
1189
            $newhash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $file->get_filepath(), $file->get_filename());
1190
            $newhashes[$newhash] = $file;
1191
        }
1192
 
1193
        // Loop through oldfiles and decide which we need to delete and which to update.
1194
        // After this cycle the array $newhashes will only contain the files that need to be added.
1195
        foreach ($oldfiles as $oldfile) {
1196
            $oldhash = $oldfile->get_pathnamehash();
1197
            if (!isset($newhashes[$oldhash])) {
1198
                // delete files not needed any more - deleted by user
1199
                $oldfile->delete();
1200
                continue;
1201
            }
1202
 
1203
            $newfile = $newhashes[$oldhash];
1204
            // Now we know that we have $oldfile and $newfile for the same path.
1205
            // Let's check if we can update this file or we need to delete and create.
1206
            if ($newfile->is_directory()) {
1207
                // Directories are always ok to just update.
1208
            } else if (($source = @unserialize($newfile->get_source() ?? '')) && isset($source->original)) {
1209
                // File has the 'original' - we need to update the file (it may even have not been changed at all).
1210
                $original = file_storage::unpack_reference($source->original);
1211
                if ($original['filename'] !== $oldfile->get_filename() || $original['filepath'] !== $oldfile->get_filepath()) {
1212
                    // Very odd, original points to another file. Delete and create file.
1213
                    $oldfile->delete();
1214
                    continue;
1215
                }
1216
            } else {
1217
                // The same file name but absence of 'original' means that file was deteled and uploaded again.
1218
                // By deleting and creating new file we properly manage all existing references.
1219
                $oldfile->delete();
1220
                continue;
1221
            }
1222
 
1223
            // status changed, we delete old file, and create a new one
1224
            if ($oldfile->get_status() != $newfile->get_status()) {
1225
                // file was changed, use updated with new timemodified data
1226
                $oldfile->delete();
1227
                // This file will be added later
1228
                continue;
1229
            }
1230
 
1231
            // Updated author
1232
            if ($oldfile->get_author() != $newfile->get_author()) {
1233
                $oldfile->set_author($newfile->get_author());
1234
            }
1235
            // Updated license
1236
            if ($oldfile->get_license() != $newfile->get_license()) {
1237
                $oldfile->set_license($newfile->get_license());
1238
            }
1239
 
1240
            // Updated file source
1241
            // Field files.source for draftarea files contains serialised object with source and original information.
1242
            // We only store the source part of it for non-draft file area.
1243
            $newsource = $newfile->get_source();
1244
            if ($source = @unserialize($newfile->get_source() ?? '')) {
1245
                $newsource = $source->source;
1246
            }
1247
            if ($oldfile->get_source() !== $newsource) {
1248
                $oldfile->set_source($newsource);
1249
            }
1250
 
1251
            // Updated sort order
1252
            if ($oldfile->get_sortorder() != $newfile->get_sortorder()) {
1253
                $oldfile->set_sortorder($newfile->get_sortorder());
1254
            }
1255
 
1256
            // Update file timemodified
1257
            if ($oldfile->get_timemodified() != $newfile->get_timemodified()) {
1258
                $oldfile->set_timemodified($newfile->get_timemodified());
1259
            }
1260
 
1261
            // Replaced file content
1262
            if (!$oldfile->is_directory() &&
1263
                    ($oldfile->get_contenthash() != $newfile->get_contenthash() ||
1264
                    $oldfile->get_filesize() != $newfile->get_filesize() ||
1265
                    $oldfile->get_referencefileid() != $newfile->get_referencefileid() ||
1266
                    $oldfile->get_userid() != $newfile->get_userid())) {
1267
                $oldfile->replace_file_with($newfile);
1268
            }
1269
 
1270
            // unchanged file or directory - we keep it as is
1271
            unset($newhashes[$oldhash]);
1272
        }
1273
 
1274
        // Add fresh file or the file which has changed status
1275
        // the size and subdirectory tests are extra safety only, the UI should prevent it
1276
        foreach ($newhashes as $file) {
1277
            $file_record = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'timemodified'=>time());
1278
            if ($source = @unserialize($file->get_source() ?? '')) {
1279
                // Field files.source for draftarea files contains serialised object with source and original information.
1280
                // We only store the source part of it for non-draft file area.
1281
                $file_record['source'] = $source->source;
1282
            }
1283
 
1284
            if ($file->is_external_file()) {
1285
                $repoid = $file->get_repository_id();
1286
                if (!empty($repoid)) {
1287
                    $context = context::instance_by_id($contextid, MUST_EXIST);
1288
                    $repo = repository::get_repository_by_id($repoid, $context);
1289
                    if (!empty($options)) {
1290
                        $repo->options = $options;
1291
                    }
1292
                    $file_record['repositoryid'] = $repoid;
1293
                    // This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved
1294
                    // to the file store. E.g. transfer ownership of the file to a system account etc.
1295
                    $reference = $repo->reference_file_selected($file->get_reference(), $context, $component, $filearea, $itemid);
1296
 
1297
                    $file_record['reference'] = $reference;
1298
                }
1299
            }
1300
 
1301
            $fs->create_file_from_storedfile($file_record, $file);
1302
        }
1303
    }
1304
 
1305
    // note: do not purge the draft area - we clean up areas later in cron,
1306
    //       the reason is that user might press submit twice and they would loose the files,
1307
    //       also sometimes we might want to use hacks that save files into two different areas
1308
 
1309
    if (is_null($text)) {
1310
        return null;
1311
    } else {
1312
        return file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps);
1313
    }
1314
}
1315
 
1316
/**
1441 ariadna 1317
 * Clear a draft area.
1318
 *
1319
 * @param int $draftitemid Id of the draft area to clear.
1320
 * @return boolean success
1321
 */
1322
function file_clear_draft_area(int $draftitemid): bool {
1323
    global $USER;
1324
    $fs = get_file_storage();
1325
    $usercontext = context_user::instance($USER->id);
1326
    return $fs->delete_area_files($usercontext->id, 'user', 'draft', $draftitemid);
1327
}
1328
 
1329
/**
1 efrain 1330
 * Convert the draft file area URLs in some content to @@PLUGINFILE@@ tokens
1331
 * ready to be saved in the database. Normally, this is done automatically by
1332
 * {@link file_save_draft_area_files()}.
1333
 *
1334
 * @category files
1335
 * @param string $text the content to process.
1336
 * @param int $draftitemid the draft file area the content was using.
1337
 * @param bool $forcehttps whether the content contains https URLs. Default false.
1338
 * @return string the processed content.
1339
 */
1340
function file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps = false) {
1341
    global $CFG, $USER;
1342
 
1343
    $usercontext = context_user::instance($USER->id);
1344
 
1345
    $wwwroot = $CFG->wwwroot;
1346
    if ($forcehttps) {
1347
        $wwwroot = str_replace('http://', 'https://', $wwwroot);
1348
    }
1349
 
1350
    // relink embedded files if text submitted - no absolute links allowed in database!
1351
    $text = str_ireplace("$wwwroot/draftfile.php/$usercontext->id/user/draft/$draftitemid/", '@@PLUGINFILE@@/', $text);
1352
 
1353
    if (strpos($text, 'draftfile.php?file=') !== false) {
1354
        $matches = array();
1355
        preg_match_all("!$wwwroot/draftfile.php\?file=%2F{$usercontext->id}%2Fuser%2Fdraft%2F{$draftitemid}%2F[^'\",&<>|`\s:\\\\]+!iu", $text, $matches);
1356
        if ($matches) {
1357
            foreach ($matches[0] as $match) {
1358
                $replace = str_ireplace('%2F', '/', $match);
1359
                $text = str_replace($match, $replace, $text);
1360
            }
1361
        }
1362
        $text = str_ireplace("$wwwroot/draftfile.php?file=/$usercontext->id/user/draft/$draftitemid/", '@@PLUGINFILE@@/', $text);
1363
    }
1364
 
1365
    return $text;
1366
}
1367
 
1368
/**
1369
 * Set file sort order
1370
 *
1371
 * @global moodle_database $DB
1372
 * @param int $contextid the context id
1373
 * @param string $component file component
1374
 * @param string $filearea file area.
1375
 * @param int $itemid itemid.
1376
 * @param string $filepath file path.
1377
 * @param string $filename file name.
1378
 * @param int $sortorder the sort order of file.
1379
 * @return bool
1380
 */
1381
function file_set_sortorder($contextid, $component, $filearea, $itemid, $filepath, $filename, $sortorder) {
1382
    global $DB;
1383
    $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename);
1384
    if ($file_record = $DB->get_record('files', $conditions)) {
1385
        $sortorder = (int)$sortorder;
1386
        $file_record->sortorder = $sortorder;
1387
        $DB->update_record('files', $file_record);
1388
        return true;
1389
    }
1390
    return false;
1391
}
1392
 
1393
/**
1394
 * reset file sort order number to 0
1395
 * @global moodle_database $DB
1396
 * @param int $contextid the context id
1397
 * @param string $component
1398
 * @param string $filearea file area.
1399
 * @param int|bool $itemid itemid.
1400
 * @return bool
1401
 */
1402
function file_reset_sortorder($contextid, $component, $filearea, $itemid=false) {
1403
    global $DB;
1404
 
1405
    $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
1406
    if ($itemid !== false) {
1407
        $conditions['itemid'] = $itemid;
1408
    }
1409
 
1410
    $file_records = $DB->get_records('files', $conditions);
1411
    foreach ($file_records as $file_record) {
1412
        $file_record->sortorder = 0;
1413
        $DB->update_record('files', $file_record);
1414
    }
1415
    return true;
1416
}
1417
 
1418
/**
1419
 * Returns description of upload error
1420
 *
1421
 * @param int $errorcode found in $_FILES['filename.ext']['error']
1422
 * @return string error description string, '' if ok
1423
 */
1424
function file_get_upload_error($errorcode) {
1425
 
1426
    switch ($errorcode) {
1427
    case 0: // UPLOAD_ERR_OK - no error
1428
        $errmessage = '';
1429
        break;
1430
 
1431
    case 1: // UPLOAD_ERR_INI_SIZE
1432
        $errmessage = get_string('uploadserverlimit');
1433
        break;
1434
 
1435
    case 2: // UPLOAD_ERR_FORM_SIZE
1436
        $errmessage = get_string('uploadformlimit');
1437
        break;
1438
 
1439
    case 3: // UPLOAD_ERR_PARTIAL
1440
        $errmessage = get_string('uploadpartialfile');
1441
        break;
1442
 
1443
    case 4: // UPLOAD_ERR_NO_FILE
1444
        $errmessage = get_string('uploadnofilefound');
1445
        break;
1446
 
1447
    // Note: there is no error with a value of 5
1448
 
1449
    case 6: // UPLOAD_ERR_NO_TMP_DIR
1450
        $errmessage = get_string('uploadnotempdir');
1451
        break;
1452
 
1453
    case 7: // UPLOAD_ERR_CANT_WRITE
1454
        $errmessage = get_string('uploadcantwrite');
1455
        break;
1456
 
1457
    case 8: // UPLOAD_ERR_EXTENSION
1458
        $errmessage = get_string('uploadextension');
1459
        break;
1460
 
1461
    default:
1462
        $errmessage = get_string('uploadproblem');
1463
    }
1464
 
1465
    return $errmessage;
1466
}
1467
 
1468
/**
1469
 * Recursive function formating an array in POST parameter
1470
 * @param array $arraydata - the array that we are going to format and add into &$data array
1471
 * @param string $currentdata - a row of the final postdata array at instant T
1472
 *                when finish, it's assign to $data under this format: name[keyname][][]...[]='value'
1473
 * @param array $data - the final data array containing all POST parameters : 1 row = 1 parameter
1474
 */
1475
function format_array_postdata_for_curlcall($arraydata, $currentdata, &$data) {
1476
        foreach ($arraydata as $k=>$v) {
1477
            $newcurrentdata = $currentdata;
1478
            if (is_array($v)) { //the value is an array, call the function recursively
1479
                $newcurrentdata = $newcurrentdata.'['.urlencode($k).']';
1480
                format_array_postdata_for_curlcall($v, $newcurrentdata, $data);
1481
            }  else { //add the POST parameter to the $data array
1482
                $data[] = $newcurrentdata.'['.urlencode($k).']='.urlencode($v);
1483
            }
1484
        }
1485
}
1486
 
1487
/**
1488
 * Transform a PHP array into POST parameter
1489
 * (see the recursive function format_array_postdata_for_curlcall)
1490
 * @param array $postdata
1491
 * @return string containing all POST parameters  (1 row = 1 POST parameter)
1492
 */
1493
function format_postdata_for_curlcall($postdata) {
1494
        $data = array();
1495
        foreach ($postdata as $k=>$v) {
1496
            if (is_array($v)) {
1497
                $currentdata = urlencode($k);
1498
                format_array_postdata_for_curlcall($v, $currentdata, $data);
1499
            }  else {
1500
                $data[] = urlencode($k).'='.urlencode($v ?? '');
1501
            }
1502
        }
1503
        $convertedpostdata = implode('&', $data);
1504
        return $convertedpostdata;
1505
}
1506
 
1507
/**
1508
 * Fetches content of file from Internet (using proxy if defined). Uses cURL extension if present.
1509
 * Due to security concerns only downloads from http(s) sources are supported.
1510
 *
1511
 * @category files
1512
 * @param string $url file url starting with http(s)://
1513
 * @param array $headers http headers, null if none. If set, should be an
1514
 *   associative array of header name => value pairs.
1515
 * @param array $postdata array means use POST request with given parameters
1516
 * @param bool $fullresponse return headers, responses, etc in a similar way snoopy does
1517
 *   (if false, just returns content)
1518
 * @param int $timeout timeout for complete download process including all file transfer
1519
 *   (default 5 minutes)
1520
 * @param int $connecttimeout timeout for connection to server; this is the timeout that
1521
 *   usually happens if the remote server is completely down (default 20 seconds);
1522
 *   may not work when using proxy
1523
 * @param bool $skipcertverify If true, the peer's SSL certificate will not be checked.
1524
 *   Only use this when already in a trusted location.
1525
 * @param string $tofile store the downloaded content to file instead of returning it.
1526
 * @param bool $calctimeout false by default, true enables an extra head request to try and determine
1527
 *   filesize and appropriately larger timeout based on $CFG->curltimeoutkbitrate
1528
 * @return stdClass|string|bool stdClass object if $fullresponse is true, false if request failed, true
1529
 *   if file downloaded into $tofile successfully or the file content as a string.
1530
 */
1531
function download_file_content($url, $headers=null, $postdata=null, $fullresponse=false, $timeout=300, $connecttimeout=20, $skipcertverify=false, $tofile=NULL, $calctimeout=false) {
1532
    global $CFG;
1533
 
1534
    // Only http and https links supported.
1535
    if (!preg_match('|^https?://|i', $url)) {
1536
        if ($fullresponse) {
1537
            $response = new stdClass();
1538
            $response->status        = 0;
1539
            $response->headers       = array();
1540
            $response->response_code = 'Invalid protocol specified in url';
1541
            $response->results       = '';
1542
            $response->error         = 'Invalid protocol specified in url';
1543
            return $response;
1544
        } else {
1545
            return false;
1546
        }
1547
    }
1548
 
1549
    $options = array();
1550
 
1551
    $headers2 = array();
1552
    if (is_array($headers)) {
1553
        foreach ($headers as $key => $value) {
1554
            if (is_numeric($key)) {
1555
                $headers2[] = $value;
1556
            } else {
1557
                $headers2[] = "$key: $value";
1558
            }
1559
        }
1560
    }
1561
 
1562
    if ($skipcertverify) {
1563
        $options['CURLOPT_SSL_VERIFYPEER'] = false;
1564
    } else {
1565
        $options['CURLOPT_SSL_VERIFYPEER'] = true;
1566
    }
1567
 
1568
    $options['CURLOPT_CONNECTTIMEOUT'] = $connecttimeout;
1569
 
1570
    $options['CURLOPT_FOLLOWLOCATION'] = 1;
1571
    $options['CURLOPT_MAXREDIRS'] = 5;
1572
 
1573
    // Use POST if requested.
1574
    if (is_array($postdata)) {
1575
        $postdata = format_postdata_for_curlcall($postdata);
1576
    } else if (empty($postdata)) {
1577
        $postdata = null;
1578
    }
1579
 
1580
    // Optionally attempt to get more correct timeout by fetching the file size.
1581
    if (!isset($CFG->curltimeoutkbitrate)) {
1582
        // Use very slow rate of 56kbps as a timeout speed when not set.
1583
        $bitrate = 56;
1584
    } else {
1585
        $bitrate = $CFG->curltimeoutkbitrate;
1586
    }
1587
    if ($calctimeout and !isset($postdata)) {
1588
        $curl = new curl();
1589
        $curl->setHeader($headers2);
1590
 
1591
        $curl->head($url, $postdata, $options);
1592
 
1593
        $info = $curl->get_info();
1594
        $error_no = $curl->get_errno();
1595
        if (!$error_no && $info['download_content_length'] > 0) {
1596
            // No curl errors - adjust for large files only - take max timeout.
1597
            $timeout = max($timeout, ceil($info['download_content_length'] * 8 / ($bitrate * 1024)));
1598
        }
1599
    }
1600
 
1601
    $curl = new curl();
1602
    $curl->setHeader($headers2);
1603
 
1604
    $options['CURLOPT_RETURNTRANSFER'] = true;
1605
    $options['CURLOPT_NOBODY'] = false;
1606
    $options['CURLOPT_TIMEOUT'] = $timeout;
1607
 
1608
    if ($tofile) {
1609
        $fh = fopen($tofile, 'w');
1610
        if (!$fh) {
1611
            if ($fullresponse) {
1612
                $response = new stdClass();
1613
                $response->status        = 0;
1614
                $response->headers       = array();
1615
                $response->response_code = 'Can not write to file';
1616
                $response->results       = false;
1617
                $response->error         = 'Can not write to file';
1618
                return $response;
1619
            } else {
1620
                return false;
1621
            }
1622
        }
1623
        $options['CURLOPT_FILE'] = $fh;
1624
    }
1625
 
1626
    if (isset($postdata)) {
1627
        $content = $curl->post($url, $postdata, $options);
1628
    } else {
1629
        $content = $curl->get($url, null, $options);
1630
    }
1631
 
1632
    if ($tofile) {
1633
        fclose($fh);
1634
        @chmod($tofile, $CFG->filepermissions);
1635
    }
1636
 
1637
/*
1638
    // Try to detect encoding problems.
1639
    if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {
1640
        curl_setopt($ch, CURLOPT_ENCODING, 'none');
1641
        $result = curl_exec($ch);
1642
    }
1643
*/
1644
 
1645
    $info       = $curl->get_info();
1646
    $error_no   = $curl->get_errno();
1647
    $rawheaders = $curl->get_raw_response();
1648
 
1649
    if ($error_no) {
1650
        $error = $content;
1651
        if (!$fullresponse) {
1652
            debugging("cURL request for \"$url\" failed with: $error ($error_no)", DEBUG_ALL);
1653
            return false;
1654
        }
1655
 
1656
        $response = new stdClass();
1657
        if ($error_no == 28) {
1658
            $response->status    = '-100'; // Mimic snoopy.
1659
        } else {
1660
            $response->status    = '0';
1661
        }
1662
        $response->headers       = array();
1663
        $response->response_code = $error;
1664
        $response->results       = false;
1665
        $response->error         = $error;
1666
        return $response;
1667
    }
1668
 
1669
    if ($tofile) {
1670
        $content = true;
1671
    }
1672
 
1673
    if (empty($info['http_code'])) {
1674
        // For security reasons we support only true http connections (Location: file:// exploit prevention).
1675
        $response = new stdClass();
1676
        $response->status        = '0';
1677
        $response->headers       = array();
1678
        $response->response_code = 'Unknown cURL error';
1679
        $response->results       = false; // do NOT change this, we really want to ignore the result!
1680
        $response->error         = 'Unknown cURL error';
1681
 
1682
    } else {
1683
        $response = new stdClass();
1684
        $response->status        = (string)$info['http_code'];
1685
        $response->headers       = $rawheaders;
1686
        $response->results       = $content;
1687
        $response->error         = '';
1688
 
1689
        // There might be multiple headers on redirect, find the status of the last one.
1690
        $firstline = true;
1691
        foreach ($rawheaders as $line) {
1692
            if ($firstline) {
1693
                $response->response_code = $line;
1694
                $firstline = false;
1695
            }
1696
            if (trim($line, "\r\n") === '') {
1697
                $firstline = true;
1698
            }
1699
        }
1700
    }
1701
 
1702
    if ($fullresponse) {
1703
        return $response;
1704
    }
1705
 
1706
    if ($info['http_code'] != 200) {
1707
        debugging("cURL request for \"$url\" failed, HTTP response code: ".$response->response_code, DEBUG_ALL);
1708
        return false;
1709
    }
1710
    return $response->results;
1711
}
1712
 
1713
/**
1714
 * Returns a list of information about file types based on extensions.
1715
 *
1716
 * The following elements expected in value array for each extension:
1717
 * 'type' - mimetype
1718
 * 'icon' - location of the icon file. If value is FILENAME, then either pix/f/FILENAME.gif
1719
 *     or pix/f/FILENAME.png must be present in moodle and contain 16x16 filetype icon;
1720
 *     also files with bigger sizes under names
1721
 *     FILENAME-24, FILENAME-32, FILENAME-64, FILENAME-128, FILENAME-256 are recommended.
1722
 * 'groups' (optional) - array of filetype groups this filetype extension is part of;
1723
 *     commonly used in moodle the following groups:
1724
 *       - web_image - image that can be included as <img> in HTML
1725
 *       - image - image that we can parse using GD to find it's dimensions, also used for portfolio format
1726
 *       - optimised_image - image that will be processed and optimised
1727
 *       - video - file that can be imported as video in text editor
1728
 *       - audio - file that can be imported as audio in text editor
1729
 *       - archive - we can extract files from this archive
1730
 *       - spreadsheet - used for portfolio format
1731
 *       - document - used for portfolio format
1732
 *       - presentation - used for portfolio format
1733
 * 'string' (optional) - the name of the string from lang/en/mimetypes.php that displays
1734
 *     human-readable description for this filetype;
1735
 *     Function {@link get_mimetype_description()} first looks at the presence of string for
1736
 *     particular mimetype (value of 'type'), if not found looks for string specified in 'string'
1737
 *     attribute, if not found returns the value of 'type';
1738
 * 'defaulticon' (boolean, optional) - used by function {@link file_mimetype_icon()} to find
1739
 *     an icon for mimetype. If an entry with 'defaulticon' is not found for a particular mimetype,
1740
 *     this function will return first found icon; Especially usefull for types such as 'text/plain'
1741
 *
1742
 * @category files
1743
 * @return array List of information about file types based on extensions.
1744
 *   Associative array of extension (lower-case) to associative array
1745
 *   from 'element name' to data. Current element names are 'type' and 'icon'.
1746
 *   Unknown types should use the 'xxx' entry which includes defaults.
1747
 */
1748
function &get_mimetypes_array() {
1749
    // Get types from the core_filetypes function, which includes caching.
1750
    return core_filetypes::get_types();
1751
}
1752
 
1753
/**
1754
 * Determine a file's MIME type based on the given filename using the function mimeinfo.
1755
 *
1756
 * This function retrieves a file's MIME type for a file that will be sent to the user.
1757
 * This should only be used for file-sending purposes just like in send_stored_file, send_file, and send_temp_file.
1758
 * Should the file's MIME type cannot be determined by mimeinfo, it will return 'application/octet-stream' as a default
1759
 * MIME type which should tell the browser "I don't know what type of file this is, so just download it.".
1760
 *
1761
 * @param string $filename The file's filename.
1762
 * @return string The file's MIME type or 'application/octet-stream' if it cannot be determined.
1763
 */
1764
function get_mimetype_for_sending($filename = '') {
1765
    // Guess the file's MIME type using mimeinfo.
1766
    $mimetype = mimeinfo('type', $filename);
1767
 
1768
    // Use octet-stream as fallback if MIME type cannot be determined by mimeinfo.
1769
    if (!$mimetype || $mimetype === 'document/unknown') {
1770
        $mimetype = 'application/octet-stream';
1771
    }
1772
 
1773
    return $mimetype;
1774
}
1775
 
1776
/**
1777
 * Obtains information about a filetype based on its extension. Will
1778
 * use a default if no information is present about that particular
1779
 * extension.
1780
 *
1781
 * @category files
1782
 * @param string $element Desired information (usually 'icon'
1783
 *   for icon filename or 'type' for MIME type. Can also be
1784
 *   'icon24', ...32, 48, 64, 72, 80, 96, 128, 256)
1785
 * @param string $filename Filename we're looking up
1786
 * @return string Requested piece of information from array
1787
 */
1788
function mimeinfo($element, $filename) {
1789
    global $CFG;
1790
    $mimeinfo = & get_mimetypes_array();
1791
    static $iconpostfixes = array(256=>'-256', 128=>'-128', 96=>'-96', 80=>'-80', 72=>'-72', 64=>'-64', 48=>'-48', 32=>'-32', 24=>'-24', 16=>'');
1792
 
1793
    $filetype = strtolower(pathinfo($filename ?? '', PATHINFO_EXTENSION));
1794
    if (empty($filetype)) {
1795
        $filetype = 'xxx'; // file without extension
1796
    }
1797
    if (preg_match('/^icon(\d*)$/', $element, $iconsizematch)) {
1798
        $iconsize = max(array(16, (int)$iconsizematch[1]));
1799
        $filenames = array($mimeinfo['xxx']['icon']);
1800
        if ($filetype != 'xxx' && isset($mimeinfo[$filetype]['icon'])) {
1801
            array_unshift($filenames, $mimeinfo[$filetype]['icon']);
1802
        }
1803
        // find the file with the closest size, first search for specific icon then for default icon
1804
        foreach ($filenames as $filename) {
1805
            foreach ($iconpostfixes as $size => $postfix) {
1806
                $fullname = $CFG->dirroot.'/pix/f/'.$filename.$postfix;
1807
                if ($iconsize >= $size &&
1808
                        (file_exists($fullname.'.svg') || file_exists($fullname.'.png') || file_exists($fullname.'.gif'))) {
1809
                    return $filename.$postfix;
1810
                }
1811
            }
1812
        }
1813
    } else if (isset($mimeinfo[$filetype][$element])) {
1814
        return $mimeinfo[$filetype][$element];
1815
    } else if (isset($mimeinfo['xxx'][$element])) {
1816
        return $mimeinfo['xxx'][$element];   // By default
1817
    } else {
1818
        return null;
1819
    }
1820
}
1821
 
1822
/**
1823
 * Obtains information about a filetype based on the MIME type rather than
1824
 * the other way around.
1825
 *
1826
 * @category files
1827
 * @param string $element Desired information ('extension', 'icon', etc.)
1828
 * @param string $mimetype MIME type we're looking up
1829
 * @return string Requested piece of information from array
1830
 */
1831
function mimeinfo_from_type($element, $mimetype) {
1832
    /* array of cached mimetype->extension associations */
1833
    static $cached = array();
1834
    $mimeinfo = & get_mimetypes_array();
1835
 
1836
    if (!array_key_exists($mimetype, $cached)) {
1837
        $cached[$mimetype] = null;
1838
        foreach($mimeinfo as $filetype => $values) {
1839
            if ($values['type'] == $mimetype) {
1840
                if ($cached[$mimetype] === null) {
1841
                    $cached[$mimetype] = '.'.$filetype;
1842
                }
1843
                if (!empty($values['defaulticon'])) {
1844
                    $cached[$mimetype] = '.'.$filetype;
1845
                    break;
1846
                }
1847
            }
1848
        }
1849
        if (empty($cached[$mimetype])) {
1850
            $cached[$mimetype] = '.xxx';
1851
        }
1852
    }
1853
    if ($element === 'extension') {
1854
        return $cached[$mimetype];
1855
    } else {
1856
        return mimeinfo($element, $cached[$mimetype]);
1857
    }
1858
}
1859
 
1860
/**
1861
 * Return the relative icon path for a given file
1862
 *
1863
 * Usage:
1864
 * <code>
1865
 * // $file - instance of stored_file or file_info
1866
 * $icon = $OUTPUT->image_url(file_file_icon($file))->out();
1867
 * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($file)));
1868
 * </code>
1869
 * or
1870
 * <code>
1871
 * echo $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file));
1872
 * </code>
1873
 *
1874
 * @param stored_file|file_info|stdClass|array $file (in case of object attributes $file->filename
1875
 *     and $file->mimetype are expected)
1876
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1877
 * @return string
1878
 */
1879
function file_file_icon($file, $unused = null) {
1880
    if ($unused !== null) {
1881
        debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
1882
    }
1883
 
1884
    if (!is_object($file)) {
1885
        $file = (object)$file;
1886
    }
1887
    if (isset($file->filename)) {
1888
        $filename = $file->filename;
1889
    } else if (method_exists($file, 'get_filename')) {
1890
        $filename = $file->get_filename();
1891
    } else if (method_exists($file, 'get_visible_name')) {
1892
        $filename = $file->get_visible_name();
1893
    } else {
1894
        $filename = '';
1895
    }
1896
    if (isset($file->mimetype)) {
1897
        $mimetype = $file->mimetype;
1898
    } else if (method_exists($file, 'get_mimetype')) {
1899
        $mimetype = $file->get_mimetype();
1900
    } else {
1901
        $mimetype = '';
1902
    }
1903
    $mimetypes = &get_mimetypes_array();
1904
    if ($filename) {
1905
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
1906
        if ($extension && !empty($mimetypes[$extension])) {
1907
            // if file name has known extension, return icon for this extension
1908
            return file_extension_icon($filename);
1909
        }
1910
    }
1911
    return file_mimetype_icon($mimetype);
1912
}
1913
 
1914
/**
1915
 * Return the relative icon path for a folder image.
1916
 *
1917
 * Usage:
1918
 * <code>
1919
 * $icon = $OUTPUT->image_url(file_folder_icon())->out();
1920
 * echo html_writer::empty_tag('img', array('src' => $icon));
1921
 * </code>
1922
 * or
1923
 * <code>
1924
 * echo $OUTPUT->pix_icon(file_folder_icon(), '');
1925
 * </code>
1926
 *
1927
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1928
 * @return string
1929
 */
1930
function file_folder_icon($unused = null) {
1931
    global $CFG;
1932
 
1933
    if ($unused !== null) {
1934
        debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
1935
    }
1936
 
1937
    return 'f/folder';
1938
}
1939
 
1940
/**
1941
 * Returns the relative icon path for a given mime type
1942
 *
1943
 * This function should be used in conjunction with $OUTPUT->image_url to produce
1944
 * a return the full path to an icon.
1945
 *
1946
 * <code>
1947
 * $mimetype = 'image/jpg';
1948
 * $icon = $OUTPUT->image_url(file_mimetype_icon($mimetype))->out();
1949
 * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($mimetype)));
1950
 * </code>
1951
 *
1952
 * @category files
1953
 * @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered
1954
 * to conform with that.
1955
 * @param string $mimetype The mimetype to fetch an icon for
1956
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1957
 * @return string The relative path to the icon
1958
 */
1959
function file_mimetype_icon($mimetype, $unused = null) {
1960
    return 'f/'.mimeinfo_from_type('icon', $mimetype);
1961
}
1962
 
1963
/**
1964
 * Returns the relative icon path for a given file name
1965
 *
1966
 * This function should be used in conjunction with $OUTPUT->image_url to produce
1967
 * a return the full path to an icon.
1968
 *
1969
 * <code>
1970
 * $filename = '.jpg';
1971
 * $icon = $OUTPUT->image_url(file_extension_icon($filename))->out();
1972
 * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => '...'));
1973
 * </code>
1974
 *
1975
 * @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered
1976
 * to conform with that.
1977
 * @todo MDL-31074 Implement $size
1978
 * @category files
1979
 * @param string $filename The filename to get the icon for
1980
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1981
 * @return string
1982
 */
1983
function file_extension_icon($filename, $unused = null) {
1984
    if ($unused !== null) {
1985
        debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
1986
    }
1987
    return 'f/'.mimeinfo('icon', $filename);
1988
}
1989
 
1990
/**
1991
 * Obtains descriptions for file types (e.g. 'Microsoft Word document') from the
1992
 * mimetypes.php language file.
1993
 *
1994
 * @param mixed $obj - instance of stored_file or file_info or array/stdClass with field
1995
 *   'filename' and 'mimetype', or just a string with mimetype (though it is recommended to
1996
 *   have filename); In case of array/stdClass the field 'mimetype' is optional.
1997
 * @param bool $capitalise If true, capitalises first character of result
1998
 * @return string Text description
1999
 */
2000
function get_mimetype_description($obj, $capitalise=false) {
2001
    $filename = $mimetype = '';
2002
    if (is_object($obj) && method_exists($obj, 'get_filename') && method_exists($obj, 'get_mimetype')) {
2003
        // this is an instance of stored_file
2004
        $mimetype = $obj->get_mimetype();
2005
        $filename = $obj->get_filename();
2006
    } else if (is_object($obj) && method_exists($obj, 'get_visible_name') && method_exists($obj, 'get_mimetype')) {
2007
        // this is an instance of file_info
2008
        $mimetype = $obj->get_mimetype();
2009
        $filename = $obj->get_visible_name();
2010
    } else if (is_array($obj) || is_object ($obj)) {
2011
        $obj = (array)$obj;
2012
        if (!empty($obj['filename'])) {
2013
            $filename = $obj['filename'];
2014
        }
2015
        if (!empty($obj['mimetype'])) {
2016
            $mimetype = $obj['mimetype'];
2017
        }
2018
    } else {
2019
        $mimetype = $obj;
2020
    }
2021
    $mimetypefromext = mimeinfo('type', $filename);
2022
    if (empty($mimetype) || $mimetypefromext !== 'document/unknown') {
2023
        // if file has a known extension, overwrite the specified mimetype
2024
        $mimetype = $mimetypefromext;
2025
    }
2026
    $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
2027
    if (empty($extension)) {
2028
        $mimetypestr = mimeinfo_from_type('string', $mimetype);
2029
        $extension = str_replace('.', '', mimeinfo_from_type('extension', $mimetype));
2030
    } else {
2031
        $mimetypestr = mimeinfo('string', $filename);
2032
    }
2033
    $chunks = explode('/', $mimetype, 2);
2034
    $chunks[] = '';
2035
    $attr = array(
2036
        'mimetype' => $mimetype,
2037
        'ext' => $extension,
2038
        'mimetype1' => $chunks[0],
2039
        'mimetype2' => $chunks[1],
2040
    );
2041
    $a = array();
2042
    foreach ($attr as $key => $value) {
2043
        $a[$key] = $value;
2044
        $a[strtoupper($key)] = strtoupper($value);
2045
        $a[ucfirst($key)] = ucfirst($value);
2046
    }
2047
 
2048
    // MIME types may include + symbol but this is not permitted in string ids.
2049
    $safemimetype = str_replace('+', '_', $mimetype ?? '');
2050
    $safemimetypestr = str_replace('+', '_', $mimetypestr ?? '');
2051
    $customdescription = mimeinfo('customdescription', $filename);
2052
    if ($customdescription) {
2053
        // Call format_string on the custom description so that multilang
2054
        // filter can be used (if enabled on system context). We use system
2055
        // context because it is possible that the page context might not have
2056
        // been defined yet.
2057
        $result = format_string($customdescription, true,
2058
                array('context' => context_system::instance()));
2059
    } else if (get_string_manager()->string_exists($safemimetype, 'mimetypes')) {
2060
        $result = get_string($safemimetype, 'mimetypes', (object)$a);
2061
    } else if (get_string_manager()->string_exists($safemimetypestr, 'mimetypes')) {
2062
        $result = get_string($safemimetypestr, 'mimetypes', (object)$a);
2063
    } else if (get_string_manager()->string_exists('default', 'mimetypes')) {
2064
        $result = get_string('default', 'mimetypes', (object)$a);
2065
    } else {
2066
        $result = $mimetype;
2067
    }
2068
    if ($capitalise) {
2069
        $result=ucfirst($result);
2070
    }
2071
    return $result;
2072
}
2073
 
2074
/**
2075
 * Returns array of elements of type $element in type group(s)
2076
 *
2077
 * @param string $element name of the element we are interested in, usually 'type' or 'extension'
2078
 * @param string|array $groups one group or array of groups/extensions/mimetypes
2079
 * @return array
2080
 */
2081
function file_get_typegroup($element, $groups) {
2082
    static $cached = array();
2083
 
2084
    // Turn groups into a list.
2085
    if (!is_array($groups)) {
2086
        $groups = preg_split('/[\s,;:"\']+/', $groups, -1, PREG_SPLIT_NO_EMPTY);
2087
    }
2088
 
2089
    if (!array_key_exists($element, $cached)) {
2090
        $cached[$element] = array();
2091
    }
2092
    $result = array();
2093
    foreach ($groups as $group) {
2094
        if (!array_key_exists($group, $cached[$element])) {
2095
            // retrieive and cache all elements of type $element for group $group
2096
            $mimeinfo = & get_mimetypes_array();
2097
            $cached[$element][$group] = array();
2098
            foreach ($mimeinfo as $extension => $value) {
2099
                $value['extension'] = '.'.$extension;
2100
                if (empty($value[$element])) {
2101
                    continue;
2102
                }
2103
                if (($group === '.'.$extension || $group === $value['type'] ||
2104
                        (!empty($value['groups']) && in_array($group, $value['groups']))) &&
2105
                        !in_array($value[$element], $cached[$element][$group])) {
2106
                    $cached[$element][$group][] = $value[$element];
2107
                }
2108
            }
2109
        }
2110
        $result = array_merge($result, $cached[$element][$group]);
2111
    }
2112
    return array_values(array_unique($result));
2113
}
2114
 
2115
/**
2116
 * Checks if file with name $filename has one of the extensions in groups $groups
2117
 *
2118
 * @see get_mimetypes_array()
2119
 * @param string $filename name of the file to check
2120
 * @param string|array $groups one group or array of groups to check
2121
 * @param bool $checktype if true and extension check fails, find the mimetype and check if
2122
 * file mimetype is in mimetypes in groups $groups
2123
 * @return bool
2124
 */
2125
function file_extension_in_typegroup($filename, $groups, $checktype = false) {
2126
    $extension = pathinfo($filename, PATHINFO_EXTENSION);
2127
    if (!empty($extension) && in_array('.'.strtolower($extension), file_get_typegroup('extension', $groups))) {
2128
        return true;
2129
    }
2130
    return $checktype && file_mimetype_in_typegroup(mimeinfo('type', $filename), $groups);
2131
}
2132
 
2133
/**
2134
 * Checks if mimetype $mimetype belongs to one of the groups $groups
2135
 *
2136
 * @see get_mimetypes_array()
2137
 * @param string $mimetype
2138
 * @param string|array $groups one group or array of groups to check
2139
 * @return bool
2140
 */
2141
function file_mimetype_in_typegroup($mimetype, $groups) {
2142
    return !empty($mimetype) && in_array($mimetype, file_get_typegroup('type', $groups));
2143
}
2144
 
2145
/**
2146
 * Requested file is not found or not accessible, does not return, terminates script
2147
 *
2148
 * @global stdClass $CFG
2149
 * @global stdClass $COURSE
2150
 */
2151
function send_file_not_found() {
2152
    global $CFG, $COURSE;
2153
 
2154
    // Allow cross-origin requests only for Web Services.
2155
    // This allow to receive requests done by Web Workers or webapps in different domains.
2156
    if (WS_SERVER) {
2157
        header('Access-Control-Allow-Origin: *');
2158
    }
2159
 
2160
    send_header_404();
2161
    throw new \moodle_exception('filenotfound', 'error',
2162
        $CFG->wwwroot.'/course/view.php?id='.$COURSE->id); // This is not displayed on IIS?
2163
}
2164
/**
2165
 * Helper function to send correct 404 for server.
2166
 */
2167
function send_header_404() {
2168
    if (substr(php_sapi_name(), 0, 3) == 'cgi') {
2169
        header("Status: 404 Not Found");
2170
    } else {
2171
        header('HTTP/1.0 404 not found');
2172
    }
2173
}
2174
 
2175
/**
2176
 * The readfile function can fail when files are larger than 2GB (even on 64-bit
2177
 * platforms). This wrapper uses readfile for small files and custom code for
2178
 * large ones.
2179
 *
2180
 * @param string $path Path to file
2181
 * @param int $filesize Size of file (if left out, will get it automatically)
2182
 * @return int|bool Size read (will always be $filesize) or false if failed
2183
 */
2184
function readfile_allow_large($path, $filesize = -1) {
2185
    // Automatically get size if not specified.
2186
    if ($filesize === -1) {
2187
        $filesize = filesize($path);
2188
    }
2189
    if ($filesize <= 2147483647) {
2190
        // If the file is up to 2^31 - 1, send it normally using readfile.
2191
        return readfile($path);
2192
    } else {
2193
        // For large files, read and output in 64KB chunks.
2194
        $handle = fopen($path, 'r');
2195
        if ($handle === false) {
2196
            return false;
2197
        }
2198
        $left = $filesize;
2199
        while ($left > 0) {
2200
            $size = min($left, 65536);
2201
            $buffer = fread($handle, $size);
2202
            if ($buffer === false) {
2203
                return false;
2204
            }
2205
            echo $buffer;
2206
            $left -= $size;
2207
        }
2208
        return $filesize;
2209
    }
2210
}
2211
 
2212
/**
2213
 * Enhanced readfile() with optional acceleration.
2214
 * @param string|stored_file $file
2215
 * @param string $mimetype
2216
 * @param bool $accelerate
2217
 * @return void
2218
 */
2219
function readfile_accel($file, $mimetype, $accelerate) {
2220
    global $CFG;
2221
 
2222
    if ($mimetype === 'text/plain') {
2223
        // there is no encoding specified in text files, we need something consistent
2224
        header('Content-Type: text/plain; charset=utf-8');
2225
    } else {
2226
        header('Content-Type: '.$mimetype);
2227
    }
2228
 
1441 ariadna 2229
    $isfileobj = is_object($file);
2230
    $lastmodified = $isfileobj ? $file->get_timemodified() : filemtime($file);
1 efrain 2231
    header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
2232
 
1441 ariadna 2233
    if ($isfileobj) {
1 efrain 2234
        header('Etag: "' . $file->get_contenthash() . '"');
2235
        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) and trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') === $file->get_contenthash()) {
2236
            header('HTTP/1.1 304 Not Modified');
2237
            return;
2238
        }
2239
    }
2240
 
2241
    // if etag present for stored file rely on it exclusively
1441 ariadna 2242
    if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) && (empty($_SERVER['HTTP_IF_NONE_MATCH']) || !$isfileobj)) {
1 efrain 2243
        // get unixtime of request header; clip extra junk off first
2244
        $since = strtotime(preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]));
2245
        if ($since && $since >= $lastmodified) {
2246
            header('HTTP/1.1 304 Not Modified');
2247
            return;
2248
        }
2249
    }
2250
 
2251
    if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
2252
        header('Accept-Ranges: bytes');
2253
    } else {
2254
        header('Accept-Ranges: none');
2255
    }
2256
 
2257
    if ($accelerate) {
1441 ariadna 2258
        if ($isfileobj) {
1 efrain 2259
            $fs = get_file_storage();
2260
            if ($fs->supports_xsendfile()) {
2261
                if ($fs->xsendfile_file($file)) {
2262
                    return;
2263
                }
2264
            }
2265
        } else {
2266
            if (!empty($CFG->xsendfile)) {
2267
                require_once("$CFG->libdir/xsendfilelib.php");
2268
                if (xsendfile($file)) {
2269
                    return;
2270
                }
2271
            }
2272
        }
2273
    }
2274
 
1441 ariadna 2275
    $filesize = $isfileobj ? $file->get_filesize() : filesize($file);
2276
    $filename = $isfileobj ? $file->get_filename() : $file;
1 efrain 2277
 
2278
    header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
2279
 
2280
    if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
2281
 
2282
        if (!empty($_SERVER['HTTP_RANGE']) and strpos($_SERVER['HTTP_RANGE'],'bytes=') !== FALSE) {
2283
            // byteserving stuff - for acrobat reader and download accelerators
2284
            // see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
2285
            // inspired by: http://www.coneural.org/florian/papers/04_byteserving.php
2286
            $ranges = false;
2287
            if (preg_match_all('/(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $ranges, PREG_SET_ORDER)) {
2288
                foreach ($ranges as $key=>$value) {
2289
                    if ($ranges[$key][1] == '') {
2290
                        //suffix case
2291
                        $ranges[$key][1] = $filesize - $ranges[$key][2];
2292
                        $ranges[$key][2] = $filesize - 1;
2293
                    } else if ($ranges[$key][2] == '' || $ranges[$key][2] > $filesize - 1) {
2294
                        //fix range length
2295
                        $ranges[$key][2] = $filesize - 1;
2296
                    }
2297
                    if ($ranges[$key][2] != '' && $ranges[$key][2] < $ranges[$key][1]) {
2298
                        //invalid byte-range ==> ignore header
2299
                        $ranges = false;
2300
                        break;
2301
                    }
2302
                    //prepare multipart header
2303
                    $ranges[$key][0] =  "\r\n--".BYTESERVING_BOUNDARY."\r\nContent-Type: $mimetype\r\n";
2304
                    $ranges[$key][0] .= "Content-Range: bytes {$ranges[$key][1]}-{$ranges[$key][2]}/$filesize\r\n\r\n";
2305
                }
2306
            } else {
2307
                $ranges = false;
2308
            }
2309
            if ($ranges) {
1441 ariadna 2310
                if ($isfileobj) {
1 efrain 2311
                    $handle = $file->get_content_file_handle();
2312
                    if ($handle === false) {
1441 ariadna 2313
                        throw new file_exception('storedfilecannotreadfile', $filename);
1 efrain 2314
                    }
2315
                } else {
2316
                    $handle = fopen($file, 'rb');
2317
                    if ($handle === false) {
2318
                        throw new file_exception('cannotopenfile', $file);
2319
                    }
2320
                }
2321
                byteserving_send_file($handle, $mimetype, $ranges, $filesize);
2322
            }
2323
        }
2324
    }
2325
 
2326
    header('Content-Length: ' . $filesize);
2327
 
2328
    if (!empty($_SERVER['REQUEST_METHOD']) and $_SERVER['REQUEST_METHOD'] === 'HEAD') {
2329
        exit;
2330
    }
2331
 
2332
    while (ob_get_level()) {
2333
        $handlerstack = ob_list_handlers();
2334
        $activehandler = array_pop($handlerstack);
2335
        if ($activehandler === 'default output handler') {
2336
            // We do not expect any content in the buffer when we are serving files.
2337
            $buffercontents = ob_get_clean();
2338
            if ($buffercontents !== '') {
1441 ariadna 2339
                // Include a preview of the first 20 characters of the output buffer to help identify
2340
                // what's causing it to be non-empty. This is useful for diagnosing unexpected output
2341
                // without exposing full content.
2342
                $buffercontentspreview = substr($buffercontents, 0, 20);
2343
                debugging("Non-empty default output handler buffer detected while serving the file {$filename}. " .
2344
                    "Buffer contents (first 20 characters): {$buffercontentspreview}", DEBUG_DEVELOPER);
1 efrain 2345
            }
2346
        } else {
2347
            // Some handlers such as zlib output compression may have file signature buffered - flush it.
2348
            ob_end_flush();
2349
        }
2350
    }
2351
 
2352
    // send the whole file content
1441 ariadna 2353
    if ($isfileobj) {
1 efrain 2354
        $file->readfile();
2355
    } else {
2356
        if (readfile_allow_large($file, $filesize) === false) {
2357
            throw new file_exception('cannotopenfile', $file);
2358
        }
2359
    }
2360
}
2361
 
2362
/**
2363
 * Similar to readfile_accel() but designed for strings.
2364
 * @param string $string
2365
 * @param string $mimetype
2366
 * @param bool $accelerate Ignored
2367
 * @return void
2368
 */
2369
function readstring_accel($string, $mimetype, $accelerate = false) {
2370
    global $CFG;
2371
 
2372
    if ($mimetype === 'text/plain') {
2373
        // there is no encoding specified in text files, we need something consistent
2374
        header('Content-Type: text/plain; charset=utf-8');
2375
    } else {
2376
        header('Content-Type: '.$mimetype);
2377
    }
2378
    header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
2379
    header('Accept-Ranges: none');
2380
    header('Content-Length: '.strlen($string));
2381
    echo $string;
2382
}
2383
 
2384
/**
2385
 * Handles the sending of temporary file to user, download is forced.
2386
 * File is deleted after abort or successful sending, does not return, script terminated
2387
 *
2388
 * @param string $path path to file, preferably from moodledata/temp/something; or content of file itself
2389
 * @param string $filename proposed file name when saving file
2390
 * @param bool $pathisstring If the path is string
2391
 */
2392
function send_temp_file($path, $filename, $pathisstring=false) {
2393
    global $CFG;
2394
 
2395
    // Guess the file's MIME type.
2396
    $mimetype = get_mimetype_for_sending($filename);
2397
 
2398
    // close session - not needed anymore
2399
    \core\session\manager::write_close();
2400
 
2401
    if (!$pathisstring) {
2402
        if (!file_exists($path)) {
2403
            send_header_404();
2404
            throw new \moodle_exception('filenotfound', 'error', $CFG->wwwroot.'/');
2405
        }
2406
        // executed after normal finish or abort
2407
        core_shutdown_manager::register_function('send_temp_file_finished', array($path));
2408
    }
2409
 
2410
    // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
2411
    if (core_useragent::is_ie() || core_useragent::is_edge()) {
2412
        $filename = urlencode($filename);
2413
    }
2414
 
2415
    // If this file was requested from a form, then mark download as complete.
2416
    \core_form\util::form_download_complete();
2417
 
2418
    header('Content-Disposition: attachment; filename="'.$filename.'"');
2419
    if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.
2420
        header('Cache-Control: private, max-age=10, no-transform');
2421
        header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2422
        header('Pragma: ');
2423
    } else { //normal http - prevent caching at all cost
2424
        header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0, no-transform');
2425
        header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2426
        header('Pragma: no-cache');
2427
    }
2428
 
2429
    // send the contents - we can not accelerate this because the file will be deleted asap
2430
    if ($pathisstring) {
2431
        readstring_accel($path, $mimetype);
2432
    } else {
2433
        readfile_accel($path, $mimetype, false);
2434
        @unlink($path);
2435
    }
2436
 
2437
    die; //no more chars to output
2438
}
2439
 
2440
/**
2441
 * Internal callback function used by send_temp_file()
2442
 *
2443
 * @param string $path
2444
 */
2445
function send_temp_file_finished($path) {
2446
    if (file_exists($path)) {
2447
        @unlink($path);
2448
    }
2449
}
2450
 
2451
/**
2452
 * Serve content which is not meant to be cached.
2453
 *
2454
 * This is only intended to be used for volatile public files, for instance
2455
 * when development is enabled, or when caching is not required on a public resource.
2456
 *
2457
 * @param string $content Raw content.
2458
 * @param string $filename The file name.
2459
 * @return void
2460
 */
2461
function send_content_uncached($content, $filename) {
2462
    $mimetype = mimeinfo('type', $filename);
2463
    $charset = strpos($mimetype, 'text/') === 0 ? '; charset=utf-8' : '';
2464
 
2465
    header('Content-Disposition: inline; filename="' . $filename . '"');
2466
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
2467
    header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2) . ' GMT');
2468
    header('Pragma: ');
2469
    header('Accept-Ranges: none');
2470
    header('Content-Type: ' . $mimetype . $charset);
2471
    header('Content-Length: ' . strlen($content));
2472
 
2473
    echo $content;
2474
    die();
2475
}
2476
 
2477
/**
2478
 * Safely save content to a certain path.
2479
 *
2480
 * This function tries hard to be atomic by first copying the content
2481
 * to a separate file, and then moving the file across. It also prevents
2482
 * the user to abort a request to prevent half-safed files.
2483
 *
2484
 * This function is intended to be used when saving some content to cache like
2485
 * $CFG->localcachedir. If you're not caching a file you should use the File API.
2486
 *
2487
 * @param string $content The file content.
2488
 * @param string $destination The absolute path of the final file.
2489
 * @return void
2490
 */
2491
function file_safe_save_content($content, $destination) {
2492
    global $CFG;
2493
 
2494
    clearstatcache();
2495
    if (!file_exists(dirname($destination))) {
2496
        @mkdir(dirname($destination), $CFG->directorypermissions, true);
2497
    }
2498
 
2499
    // Prevent serving of incomplete file from concurrent request,
2500
    // the rename() should be more atomic than fwrite().
2501
    ignore_user_abort(true);
2502
    if ($fp = fopen($destination . '.tmp', 'xb')) {
2503
        fwrite($fp, $content);
2504
        fclose($fp);
2505
        rename($destination . '.tmp', $destination);
2506
        @chmod($destination, $CFG->filepermissions);
2507
        @unlink($destination . '.tmp'); // Just in case anything fails.
2508
    }
2509
    ignore_user_abort(false);
2510
    if (connection_aborted()) {
2511
        die();
2512
    }
2513
}
2514
 
2515
/**
2516
 * Handles the sending of file data to the user's browser, including support for
2517
 * byteranges etc.
2518
 *
2519
 * @category files
2520
 * @param string|stored_file $path Path of file on disk (including real filename),
2521
 *                                 or actual content of file as string,
2522
 *                                 or stored_file object
2523
 * @param string $filename Filename to send
2524
 * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
2525
 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
2526
 * @param bool $pathisstring If true (default false), $path is the content to send and not the pathname.
2527
 *                           Forced to false when $path is a stored_file object.
2528
 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
2529
 * @param string $mimetype Include to specify the MIME type; leave blank to have it guess the type from $filename
2530
 * @param bool $dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.
2531
 *                        if this is passed as true, ignore_user_abort is called.  if you don't want your processing to continue on cancel,
2532
 *                        you must detect this case when control is returned using connection_aborted. Please not that session is closed
2533
 *                        and should not be reopened.
2534
 * @param array $options An array of options, currently accepts:
2535
 *                       - (string) cacheability: public, or private.
2536
 *                       - (string|null) immutable
2537
 *                       - (bool) dontforcesvgdownload: true if force download should be disabled on SVGs.
2538
 *                                Note: This overrides a security feature, so should only be applied to "trusted" content
2539
 *                                (eg module content that is created using an XSS risk flagged capability, such as SCORM).
2540
 * @return null script execution stopped unless $dontdie is true
2541
 */
2542
function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring=false, $forcedownload=false, $mimetype='',
2543
                   $dontdie=false, array $options = array()) {
2544
    global $CFG, $COURSE;
2545
 
2546
    if ($dontdie) {
2547
        ignore_user_abort(true);
2548
    }
2549
 
2550
    if ($lifetime === 'default' or is_null($lifetime)) {
2551
        $lifetime = $CFG->filelifetime;
2552
    }
2553
 
2554
    if (is_object($path)) {
2555
        $pathisstring = false;
2556
    }
2557
 
2558
    \core\session\manager::write_close(); // Unlock session during file serving.
2559
 
2560
    // Use given MIME type if specified, otherwise guess it.
2561
    if (!$mimetype || $mimetype === 'document/unknown') {
2562
        $mimetype = get_mimetype_for_sending($filename);
2563
    }
2564
 
2565
    // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
2566
    if (core_useragent::is_ie() || core_useragent::is_edge()) {
2567
        $filename = rawurlencode($filename);
2568
    }
2569
 
2570
    // Make sure we force download of SVG files, unless the module explicitly allows them (eg within SCORM content).
2571
    // This is for security reasons (https://digi.ninja/blog/svg_xss.php).
2572
    if (file_is_svg_image_from_mimetype($mimetype) && empty($options['dontforcesvgdownload'])) {
2573
        $forcedownload = true;
2574
    }
2575
 
2576
    if ($forcedownload) {
2577
        header('Content-Disposition: attachment; filename="'.$filename.'"');
2578
 
2579
        // If this file was requested from a form, then mark download as complete.
2580
        \core_form\util::form_download_complete();
2581
    } else if ($mimetype !== 'application/x-shockwave-flash') {
2582
        // If this is an swf don't pass content-disposition with filename as this makes the flash player treat the file
2583
        // as an upload and enforces security that may prevent the file from being loaded.
2584
 
2585
        header('Content-Disposition: inline; filename="'.$filename.'"');
2586
    }
2587
 
2588
    if ($lifetime > 0) {
2589
        $immutable = '';
2590
        if (!empty($options['immutable'])) {
2591
            $immutable = ', immutable';
2592
            // Overwrite lifetime accordingly:
2593
            // 90 days only - based on Moodle point release cadence being every 3 months.
2594
            $lifetimemin = 60 * 60 * 24 * 90;
2595
            $lifetime = max($lifetime, $lifetimemin);
2596
        }
2597
        $cacheability = ' public,';
2598
        if (!empty($options['cacheability']) && ($options['cacheability'] === 'public')) {
2599
            // This file must be cache-able by both browsers and proxies.
2600
            $cacheability = ' public,';
2601
        } else if (!empty($options['cacheability']) && ($options['cacheability'] === 'private')) {
2602
            // This file must be cache-able only by browsers.
2603
            $cacheability = ' private,';
2604
        } else if (isloggedin() and !isguestuser()) {
2605
            // By default, under the conditions above, this file must be cache-able only by browsers.
2606
            $cacheability = ' private,';
2607
        }
2608
        $nobyteserving = false;
2609
        header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform'.$immutable);
2610
        header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
2611
        header('Pragma: ');
2612
 
2613
    } else { // Do not cache files in proxies and browsers
2614
        $nobyteserving = true;
2615
        if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.
2616
            header('Cache-Control: private, max-age=10, no-transform');
2617
            header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2618
            header('Pragma: ');
1441 ariadna 2619
        } else { // Normal http - prevent caching at all cost.
2620
            header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0, no-transform', 'no-store');
1 efrain 2621
            header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2622
            header('Pragma: no-cache');
2623
        }
2624
    }
2625
 
2626
    if (empty($filter)) {
2627
        // send the contents
2628
        if ($pathisstring) {
2629
            readstring_accel($path, $mimetype);
2630
        } else {
2631
            readfile_accel($path, $mimetype, !$dontdie);
2632
        }
2633
 
2634
    } else {
2635
        // Try to put the file through filters
2636
        if ($mimetype == 'text/html' || $mimetype == 'application/xhtml+xml' || file_is_svg_image_from_mimetype($mimetype)) {
2637
            $options = new stdClass();
2638
            $options->noclean = true;
1441 ariadna 2639
            $options->context = context_course::instance($COURSE->id);
2640
 
1 efrain 2641
            if (is_object($path)) {
2642
                $text = $path->get_content();
2643
            } else if ($pathisstring) {
2644
                $text = $path;
2645
            } else {
2646
                $text = implode('', file($path));
2647
            }
2648
 
1441 ariadna 2649
            $output = format_text($text, FORMAT_HTML, $options);
1 efrain 2650
            readstring_accel($output, $mimetype);
2651
        } else if (($mimetype == 'text/plain') and ($filter == 1)) {
2652
            // only filter text if filter all files is selected
2653
            $options = new stdClass();
2654
            $options->newlines = false;
2655
            $options->noclean = true;
1441 ariadna 2656
            $options->context = context_course::instance($COURSE->id);
2657
 
1 efrain 2658
            if (is_object($path)) {
2659
                $text = htmlentities($path->get_content(), ENT_QUOTES, 'UTF-8');
2660
            } else if ($pathisstring) {
2661
                $text = htmlentities($path, ENT_QUOTES, 'UTF-8');
2662
            } else {
2663
                $text = htmlentities(implode('', file($path)), ENT_QUOTES, 'UTF-8');
2664
            }
2665
 
1441 ariadna 2666
            $output = '<pre>'. format_text($text, FORMAT_MOODLE, $options) .'</pre>';
1 efrain 2667
            readstring_accel($output, $mimetype);
2668
        } else {
2669
            // send the contents
2670
            if ($pathisstring) {
2671
                readstring_accel($path, $mimetype);
2672
            } else {
2673
                readfile_accel($path, $mimetype, !$dontdie);
2674
            }
2675
        }
2676
    }
2677
    if ($dontdie) {
2678
        return;
2679
    }
2680
    die; //no more chars to output!!!
2681
}
2682
 
2683
/**
2684
 * Handles the sending of file data to the user's browser, including support for
2685
 * byteranges etc.
2686
 *
2687
 * The $options parameter supports the following keys:
2688
 *  (string|null) preview - send the preview of the file (e.g. "thumb" for a thumbnail)
2689
 *  (string|null) filename - overrides the implicit filename
2690
 *  (bool) dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.
2691
 *      if this is passed as true, ignore_user_abort is called.  if you don't want your processing to continue on cancel,
2692
 *      you must detect this case when control is returned using connection_aborted. Please not that session is closed
2693
 *      and should not be reopened
2694
 *  (string|null) cacheability - force the cacheability setting of the HTTP response, "private" or "public",
2695
 *      when $lifetime is greater than 0. Cacheability defaults to "private" when logged in as other than guest; otherwise,
2696
 *      defaults to "public".
2697
 *  (string|null) immutable - set the immutable cache setting in the HTTP response, when served under HTTPS.
2698
 *      Note: it's up to the consumer to set it properly i.e. when serving a "versioned" URL.
2699
 *
2700
 * @category files
2701
 * @param stored_file $storedfile local file object
2702
 * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
2703
 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
2704
 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
2705
 * @param array $options additional options affecting the file serving
2706
 * @return null script execution stopped unless $options['dontdie'] is true
2707
 */
2708
function send_stored_file($storedfile, $lifetime=null, $filter=0, $forcedownload=false, array $options=array()) {
2709
    global $CFG, $COURSE;
2710
 
2711
    static $recursion = 0;
2712
 
2713
    if (empty($options['filename'])) {
2714
        $filename = null;
2715
    } else {
2716
        $filename = $options['filename'];
2717
    }
2718
 
2719
    if (empty($options['dontdie'])) {
2720
        $dontdie = false;
2721
    } else {
2722
        $dontdie = true;
2723
    }
2724
 
2725
    if ($lifetime === 'default' or is_null($lifetime)) {
2726
        $lifetime = $CFG->filelifetime;
2727
    }
2728
 
2729
    if (!empty($options['preview'])) {
2730
        // replace the file with its preview
2731
        $fs = get_file_storage();
2732
        $previewfile = $fs->get_file_preview($storedfile, $options['preview']);
2733
        if (!$previewfile) {
2734
            // Unable to create a preview of the file, send its default mime icon instead.
2735
            $fileicon = file_file_icon($storedfile);
2736
            send_file($CFG->dirroot.'/pix/'.$fileicon.'.svg', basename($fileicon).'.svg');
2737
        } else {
2738
            // preview images have fixed cache lifetime and they ignore forced download
2739
            // (they are generated by GD and therefore they are considered reasonably safe).
2740
            $storedfile = $previewfile;
2741
            $lifetime = DAYSECS;
2742
            $filter = 0;
2743
            $forcedownload = false;
2744
        }
2745
    }
2746
 
2747
    // handle external resource
2748
    if ($storedfile && $storedfile->is_external_file() && !isset($options['sendcachedexternalfile'])) {
2749
 
2750
        // Have we been here before?
2751
        $recursion++;
2752
        if ($recursion > 10) {
2753
            throw new coding_exception('Recursive file serving detected');
2754
        }
2755
 
2756
        $storedfile->send_file($lifetime, $filter, $forcedownload, $options);
2757
        die;
2758
    }
2759
 
2760
    if (!$storedfile || $storedfile->is_directory()) {
2761
        // Nothing to serve.
2762
        if ($dontdie) {
2763
            return;
2764
        }
2765
        die;
2766
    }
2767
 
2768
    $filename = is_null($filename) ? $storedfile->get_filename() : $filename;
2769
 
2770
    // Use given MIME type if specified.
2771
    $mimetype = $storedfile->get_mimetype();
2772
 
2773
    // Allow cross-origin requests only for Web Services.
2774
    // This allow to receive requests done by Web Workers or webapps in different domains.
2775
    if (WS_SERVER) {
2776
        header('Access-Control-Allow-Origin: *');
2777
    }
2778
 
2779
    send_file($storedfile, $filename, $lifetime, $filter, false, $forcedownload, $mimetype, $dontdie, $options);
2780
}
2781
 
2782
/**
2783
 * Recursively delete the file or folder with path $location. That is,
2784
 * if it is a file delete it. If it is a folder, delete all its content
2785
 * then delete it. If $location does not exist to start, that is not
2786
 * considered an error.
2787
 *
2788
 * @param string $location the path to remove.
2789
 * @return bool
2790
 */
2791
function fulldelete($location) {
2792
    if (empty($location)) {
2793
        // extra safety against wrong param
2794
        return false;
2795
    }
2796
    if (is_dir($location)) {
2797
        if (!$currdir = opendir($location)) {
2798
            return false;
2799
        }
2800
        while (false !== ($file = readdir($currdir))) {
2801
            if ($file <> ".." && $file <> ".") {
2802
                $fullfile = $location."/".$file;
2803
                if (is_dir($fullfile)) {
2804
                    if (!fulldelete($fullfile)) {
2805
                        return false;
2806
                    }
2807
                } else {
2808
                    if (!unlink($fullfile)) {
2809
                        return false;
2810
                    }
2811
                }
2812
            }
2813
        }
2814
        closedir($currdir);
2815
        if (! rmdir($location)) {
2816
            return false;
2817
        }
2818
 
2819
    } else if (file_exists($location)) {
2820
        if (!unlink($location)) {
2821
            return false;
2822
        }
2823
    }
2824
    return true;
2825
}
2826
 
2827
/**
2828
 * Send requested byterange of file.
2829
 *
2830
 * @param resource $handle A file handle
2831
 * @param string $mimetype The mimetype for the output
2832
 * @param array $ranges An array of ranges to send
2833
 * @param string $filesize The size of the content if only one range is used
2834
 */
2835
function byteserving_send_file($handle, $mimetype, $ranges, $filesize) {
2836
    // better turn off any kind of compression and buffering
2837
    ini_set('zlib.output_compression', 'Off');
2838
 
2839
    $chunksize = 1*(1024*1024); // 1MB chunks - must be less than 2MB!
2840
    if ($handle === false) {
2841
        die;
2842
    }
2843
    if (count($ranges) == 1) { //only one range requested
2844
        $length = $ranges[0][2] - $ranges[0][1] + 1;
2845
        header('HTTP/1.1 206 Partial content');
2846
        header('Content-Length: '.$length);
2847
        header('Content-Range: bytes '.$ranges[0][1].'-'.$ranges[0][2].'/'.$filesize);
2848
        header('Content-Type: '.$mimetype);
2849
 
2850
        while(@ob_get_level()) {
2851
            if (!@ob_end_flush()) {
2852
                break;
2853
            }
2854
        }
2855
 
2856
        fseek($handle, $ranges[0][1]);
2857
        while (!feof($handle) && $length > 0) {
2858
            core_php_time_limit::raise(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk
2859
            $buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));
2860
            echo $buffer;
2861
            flush();
2862
            $length -= strlen($buffer);
2863
        }
2864
        fclose($handle);
2865
        die;
2866
    } else { // multiple ranges requested - not tested much
2867
        $totallength = 0;
2868
        foreach($ranges as $range) {
2869
            $totallength += strlen($range[0]) + $range[2] - $range[1] + 1;
2870
        }
2871
        $totallength += strlen("\r\n--".BYTESERVING_BOUNDARY."--\r\n");
2872
        header('HTTP/1.1 206 Partial content');
2873
        header('Content-Length: '.$totallength);
2874
        header('Content-Type: multipart/byteranges; boundary='.BYTESERVING_BOUNDARY);
2875
 
2876
        while(@ob_get_level()) {
2877
            if (!@ob_end_flush()) {
2878
                break;
2879
            }
2880
        }
2881
 
2882
        foreach($ranges as $range) {
2883
            $length = $range[2] - $range[1] + 1;
2884
            echo $range[0];
2885
            fseek($handle, $range[1]);
2886
            while (!feof($handle) && $length > 0) {
2887
                core_php_time_limit::raise(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk
2888
                $buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));
2889
                echo $buffer;
2890
                flush();
2891
                $length -= strlen($buffer);
2892
            }
2893
        }
2894
        echo "\r\n--".BYTESERVING_BOUNDARY."--\r\n";
2895
        fclose($handle);
2896
        die;
2897
    }
2898
}
2899
 
2900
/**
2901
 * Tells whether the filename is executable.
2902
 *
2903
 * @link http://php.net/manual/en/function.is-executable.php
2904
 * @link https://bugs.php.net/bug.php?id=41062
2905
 * @param string $filename Path to the file.
2906
 * @return bool True if the filename exists and is executable; otherwise, false.
2907
 */
2908
function file_is_executable($filename) {
2909
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
2910
        if (is_executable($filename)) {
2911
            return true;
2912
        } else {
2913
            $fileext = strrchr($filename, '.');
2914
            // If we have an extension we can check if it is listed as executable.
2915
            if ($fileext && file_exists($filename) && !is_dir($filename)) {
2916
                $winpathext = strtolower(getenv('PATHEXT'));
2917
                $winpathexts = explode(';', $winpathext);
2918
 
2919
                return in_array(strtolower($fileext), $winpathexts);
2920
            }
2921
 
2922
            return false;
2923
        }
2924
    } else {
2925
        return is_executable($filename);
2926
    }
2927
}
2928
 
2929
/**
2930
 * Overwrite an existing file in a draft area.
2931
 *
2932
 * @param  stored_file $newfile      the new file with the new content and meta-data
2933
 * @param  stored_file $existingfile the file that will be overwritten
2934
 * @throws moodle_exception
2935
 * @since Moodle 3.2
2936
 */
2937
function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $existingfile) {
2938
    if ($existingfile->get_component() != 'user' or $existingfile->get_filearea() != 'draft') {
2939
        throw new coding_exception('The file to overwrite is not in a draft area.');
2940
    }
2941
 
2942
    $fs = get_file_storage();
2943
    // Remember original file source field.
2944
    $source = @unserialize($existingfile->get_source() ?? '');
2945
    // Remember the original sortorder.
2946
    $sortorder = $existingfile->get_sortorder();
2947
    if ($newfile->is_external_file()) {
2948
        // New file is a reference. Check that existing file does not have any other files referencing to it
2949
        if (isset($source->original) && $fs->search_references_count($source->original)) {
2950
            throw new moodle_exception('errordoublereference', 'repository');
2951
        }
2952
    }
2953
 
2954
    // Delete existing file to release filename.
2955
    $newfilerecord = array(
2956
        'contextid' => $existingfile->get_contextid(),
2957
        'component' => 'user',
2958
        'filearea' => 'draft',
2959
        'itemid' => $existingfile->get_itemid(),
2960
        'timemodified' => time()
2961
    );
2962
    $existingfile->delete();
2963
 
2964
    // Create new file.
2965
    $newfile = $fs->create_file_from_storedfile($newfilerecord, $newfile);
2966
    // Preserve original file location (stored in source field) for handling references.
2967
    if (isset($source->original)) {
2968
        if (!($newfilesource = @unserialize($newfile->get_source() ?? ''))) {
2969
            $newfilesource = new stdClass();
2970
        }
2971
        $newfilesource->original = $source->original;
2972
        $newfile->set_source(serialize($newfilesource));
2973
    }
2974
    $newfile->set_sortorder($sortorder);
2975
}
2976
 
2977
/**
2978
 * Add files from a draft area into a final area.
2979
 *
2980
 * Most of the time you do not want to use this. It is intended to be used
2981
 * by asynchronous services which cannot direcly manipulate a final
2982
 * area through a draft area. Instead they add files to a new draft
2983
 * area and merge that new draft into the final area when ready.
2984
 *
2985
 * @param int $draftitemid the id of the draft area to use.
2986
 * @param int $contextid this parameter and the next two identify the file area to save to.
2987
 * @param string $component component name
2988
 * @param string $filearea indentifies the file area
2989
 * @param int $itemid identifies the item id or false for all items in the file area
2990
 * @param array $options area options (subdirs=false, maxfiles=-1, maxbytes=0, areamaxbytes=FILE_AREA_MAX_BYTES_UNLIMITED)
2991
 * @see file_save_draft_area_files
2992
 * @since Moodle 3.2
2993
 */
2994
function file_merge_files_from_draft_area_into_filearea($draftitemid, $contextid, $component, $filearea, $itemid,
1441 ariadna 2995
                                                        ?array $options = null) {
1 efrain 2996
    // We use 0 here so file_prepare_draft_area creates a new one, finaldraftid will be updated with the new draft id.
2997
    $finaldraftid = 0;
2998
    file_prepare_draft_area($finaldraftid, $contextid, $component, $filearea, $itemid, $options);
2999
    file_merge_draft_area_into_draft_area($draftitemid, $finaldraftid);
3000
    file_save_draft_area_files($finaldraftid, $contextid, $component, $filearea, $itemid, $options);
3001
}
3002
 
3003
/**
3004
 * Merge files from two draftarea areas.
3005
 *
3006
 * This does not handle conflict resolution, files in the destination area which appear
3007
 * to be more recent will be kept disregarding the intended ones.
3008
 *
3009
 * @param int $getfromdraftid the id of the draft area where are the files to merge.
3010
 * @param int $mergeintodraftid the id of the draft area where new files will be merged.
3011
 * @throws coding_exception
3012
 * @since Moodle 3.2
3013
 */
3014
function file_merge_draft_area_into_draft_area($getfromdraftid, $mergeintodraftid) {
3015
    global $USER;
3016
 
3017
    $fs = get_file_storage();
3018
    $contextid = context_user::instance($USER->id)->id;
3019
 
3020
    if (!$filestomerge = $fs->get_area_files($contextid, 'user', 'draft', $getfromdraftid)) {
3021
        throw new coding_exception('Nothing to merge or area does not belong to current user');
3022
    }
3023
 
3024
    $currentfiles = $fs->get_area_files($contextid, 'user', 'draft', $mergeintodraftid);
3025
 
3026
    // Get hashes of the files to merge.
3027
    $newhashes = array();
3028
    foreach ($filestomerge as $filetomerge) {
3029
        $filepath = $filetomerge->get_filepath();
3030
        $filename = $filetomerge->get_filename();
3031
 
3032
        $newhash = $fs->get_pathname_hash($contextid, 'user', 'draft', $mergeintodraftid, $filepath, $filename);
3033
        $newhashes[$newhash] = $filetomerge;
3034
    }
3035
 
3036
    // Calculate wich files must be added.
3037
    foreach ($currentfiles as $file) {
3038
        $filehash = $file->get_pathnamehash();
3039
        // One file to be merged already exists.
3040
        if (isset($newhashes[$filehash])) {
3041
            $updatedfile = $newhashes[$filehash];
3042
 
3043
            // Avoid race conditions.
3044
            if ($file->get_timemodified() > $updatedfile->get_timemodified()) {
3045
                // The existing file is more recent, do not copy the suposedly "new" one.
3046
                unset($newhashes[$filehash]);
3047
                continue;
3048
            }
3049
            // Update existing file (not only content, meta-data too).
3050
            file_overwrite_existing_draftfile($updatedfile, $file);
3051
            unset($newhashes[$filehash]);
3052
        }
3053
    }
3054
 
3055
    foreach ($newhashes as $newfile) {
3056
        $newfilerecord = array(
3057
            'contextid' => $contextid,
3058
            'component' => 'user',
3059
            'filearea' => 'draft',
3060
            'itemid' => $mergeintodraftid,
3061
            'timemodified' => time()
3062
        );
3063
 
3064
        $fs->create_file_from_storedfile($newfilerecord, $newfile);
3065
    }
3066
}
3067
 
3068
/**
3069
 * Attempt to determine whether the specified mime-type is an SVG image or not.
3070
 *
3071
 * @param string $mimetype Mime-type
3072
 * @return bool True if it is an SVG file
3073
 */
3074
function file_is_svg_image_from_mimetype(string $mimetype): bool {
3075
    return preg_match('|^image/svg|', $mimetype);
3076
}
3077
 
3078
/**
3079
 * Returns the moodle proxy configuration as a formatted url
3080
 *
3081
 * @return string the string to use for proxy settings.
3082
 */
3083
function get_moodle_proxy_url() {
3084
    global $CFG;
3085
    $proxy = '';
3086
    if (empty($CFG->proxytype)) {
3087
        return $proxy;
3088
    }
3089
    if (empty($CFG->proxyhost)) {
3090
        return $proxy;
3091
    }
3092
    if ($CFG->proxytype === 'SOCKS5') {
3093
        // If it is a SOCKS proxy, append the protocol info.
3094
        $protocol = 'socks5://';
3095
    } else {
3096
        $protocol = '';
3097
    }
3098
    $proxy = $CFG->proxyhost;
3099
    if (!empty($CFG->proxyport)) {
3100
        $proxy .= ':'. $CFG->proxyport;
3101
    }
3102
    if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {
3103
        $proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy;
3104
    }
3105
    return $proxy;
3106
}
3107
 
3108
 
3109
 
3110
/**
3111
 * RESTful cURL class
3112
 *
3113
 * This is a wrapper class for curl, it is quite easy to use:
3114
 * <code>
3115
 * $c = new curl;
3116
 * // enable cache
3117
 * $c = new curl(array('cache'=>true));
3118
 * // enable cookie
3119
 * $c = new curl(array('cookie'=>true));
3120
 * // enable proxy
3121
 * $c = new curl(array('proxy'=>true));
3122
 *
3123
 * // HTTP GET Method
3124
 * $html = $c->get('http://example.com');
3125
 * // HTTP POST Method
3126
 * $html = $c->post('http://example.com/', array('q'=>'words', 'name'=>'moodle'));
3127
 * // HTTP PUT Method
3128
 * $html = $c->put('http://example.com/', array('file'=>'/var/www/test.txt');
3129
 * </code>
3130
 *
3131
 * @package   core_files
3132
 * @category files
3133
 * @copyright Dongsheng Cai <dongsheng@moodle.com>
3134
 * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
3135
 */
3136
class curl {
3137
    /** @var curl_cache|false Caches http request contents */
3138
    public $cache    = false;
3139
    /** @var bool Uses proxy, null means automatic based on URL */
3140
    public $proxy    = null;
3141
    /** @var string library version */
3142
    public $version  = '0.4 dev';
3143
    /** @var array http's response */
3144
    public $response = array();
3145
    /** @var array Raw response headers, needed for BC in download_file_content(). */
3146
    public $rawresponse = array();
3147
    /** @var array http header */
3148
    public $header   = array();
3149
    /** @var array cURL information */
3150
    public $info;
3151
    /** @var string error */
3152
    public $error;
3153
    /** @var int error code */
3154
    public $errno;
3155
    /** @var bool Perform redirects at PHP level instead of relying on native cURL functionality. Always true now. */
3156
    public $emulateredirects = null;
3157
 
3158
    /** @var array cURL options */
3159
    private $options;
3160
 
3161
    /** @var string Proxy host */
3162
    private $proxy_host = '';
3163
    /** @var string Proxy auth */
3164
    private $proxy_auth = '';
3165
    /** @var string Proxy type */
3166
    private $proxy_type = '';
3167
    /** @var bool Debug mode on */
3168
    private $debug    = false;
3169
    /** @var bool|string Path to cookie file */
3170
    private $cookie   = false;
3171
    /** @var bool tracks multiple headers in response - redirect detection */
3172
    private $responsefinished = false;
3173
    /** @var ?\core\files\curl_security_helper security helper class, responsible for checking host/ports against allowed/blocked entries.*/
3174
    private $securityhelper;
3175
    /** @var bool ignoresecurity a flag which can be supplied to the constructor, allowing security to be bypassed. */
3176
    private $ignoresecurity;
3177
    /** @var array $mockresponses For unit testing only - return the head of this list instead of making the next request. */
3178
    private static $mockresponses = [];
1441 ariadna 3179
    /** @var array $curlresolveinfo Resolve addresses for the URL that have passed cuRL security checks, in a CURLOPT_RESOLVE compatible format. */
3180
    private $curlresolveinfo = [];
1 efrain 3181
    /** @var array temporary params value if the value is not belongs to class stored_file. */
3182
    public $_tmp_file_post_params = [];
3183
 
3184
    /**
3185
     * Curl constructor.
3186
     *
3187
     * Allowed settings are:
3188
     *  proxy: (bool) use proxy server, null means autodetect non-local from url
3189
     *  debug: (bool) use debug output
3190
     *  cookie: (string) path to cookie file, false if none
3191
     *  cache: (bool) use cache
3192
     *  module_cache: (string) type of cache
3193
     *  securityhelper: (\core\files\curl_security_helper_base) helper object providing URL checking for requests.
3194
     *  ignoresecurity: (bool) set true to override and ignore the security helper when making requests.
3195
     *
3196
     * @param array $settings
3197
     */
3198
    public function __construct($settings = array()) {
3199
        global $CFG;
3200
        if (!function_exists('curl_init')) {
3201
            $this->error = 'cURL module must be enabled!';
3202
            trigger_error($this->error, E_USER_ERROR);
3203
            return;
3204
        }
3205
 
3206
        // All settings of this class should be init here.
3207
        $this->resetopt();
3208
        if (!empty($settings['debug'])) {
3209
            $this->debug = true;
3210
        }
3211
        if (!empty($settings['cookie'])) {
3212
            if($settings['cookie'] === true) {
3213
                $this->cookie = $CFG->dataroot.'/curl_cookie.txt';
3214
            } else {
3215
                $this->cookie = $settings['cookie'];
3216
            }
3217
        }
3218
        if (!empty($settings['cache'])) {
3219
            if (class_exists('curl_cache')) {
3220
                if (!empty($settings['module_cache'])) {
3221
                    $this->cache = new curl_cache($settings['module_cache']);
3222
                } else {
3223
                    $this->cache = new curl_cache('misc');
3224
                }
3225
            }
3226
        }
3227
        if (!empty($CFG->proxyhost)) {
3228
            if (empty($CFG->proxyport)) {
3229
                $this->proxy_host = $CFG->proxyhost;
3230
            } else {
3231
                $this->proxy_host = $CFG->proxyhost.':'.$CFG->proxyport;
3232
            }
3233
            if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
3234
                $this->proxy_auth = $CFG->proxyuser.':'.$CFG->proxypassword;
3235
                $this->setopt(array(
3236
                            'proxyauth'=> CURLAUTH_BASIC | CURLAUTH_NTLM,
3237
                            'proxyuserpwd'=>$this->proxy_auth));
3238
            }
3239
            if (!empty($CFG->proxytype)) {
3240
                if ($CFG->proxytype == 'SOCKS5') {
3241
                    $this->proxy_type = CURLPROXY_SOCKS5;
3242
                } else {
3243
                    $this->proxy_type = CURLPROXY_HTTP;
3244
                    $this->setopt([
3245
                        'httpproxytunnel' => false,
3246
                    ]);
3247
                    if (defined('CURLOPT_SUPPRESS_CONNECT_HEADERS')) {
3248
                        $this->setopt([
3249
                            'suppress_connect_headers' => true,
3250
                        ]);
3251
                    }
3252
                }
3253
                $this->setopt(array('proxytype'=>$this->proxy_type));
3254
            }
3255
 
3256
            if (isset($settings['proxy'])) {
3257
                $this->proxy = $settings['proxy'];
3258
            }
3259
        } else {
3260
            $this->proxy = false;
3261
        }
3262
 
3263
        // All redirects are performed at PHP level now and each one is checked against blocked URLs rules. We do not
3264
        // want to let cURL naively follow the redirect chain and visit every URL for security reasons. Even when the
3265
        // caller explicitly wants to ignore the security checks, we would need to fall back to the original
3266
        // implementation and use emulated redirects if open_basedir is in effect to avoid the PHP warning
3267
        // "CURLOPT_FOLLOWLOCATION cannot be activated when in safe_mode or an open_basedir". So it is better to simply
3268
        // ignore this property and always handle redirects at this PHP wrapper level and not inside the native cURL.
3269
        $this->emulateredirects = true;
3270
 
3271
        // Curl security setup. Allow injection of a security helper, but if not found, default to the core helper.
3272
        if (isset($settings['securityhelper']) && $settings['securityhelper'] instanceof \core\files\curl_security_helper_base) {
3273
            $this->set_security($settings['securityhelper']);
3274
        } else {
3275
            $this->set_security(new \core\files\curl_security_helper());
3276
        }
3277
        $this->ignoresecurity = isset($settings['ignoresecurity']) ? $settings['ignoresecurity'] : false;
3278
    }
3279
 
3280
    /**
3281
     * Resets the CURL options that have already been set
3282
     */
3283
    public function resetopt() {
3284
        $this->options = array();
3285
        $this->options['CURLOPT_USERAGENT']         = \core_useragent::get_moodlebot_useragent();
3286
        // True to include the header in the output
3287
        $this->options['CURLOPT_HEADER']            = 0;
3288
        // True to Exclude the body from the output
3289
        $this->options['CURLOPT_NOBODY']            = 0;
3290
        // Redirect ny default.
3291
        $this->options['CURLOPT_FOLLOWLOCATION']    = 1;
3292
        $this->options['CURLOPT_MAXREDIRS']         = 10;
3293
        $this->options['CURLOPT_ENCODING']          = '';
3294
        // TRUE to return the transfer as a string of the return
3295
        // value of curl_exec() instead of outputting it out directly.
3296
        $this->options['CURLOPT_RETURNTRANSFER']    = 1;
3297
        $this->options['CURLOPT_SSL_VERIFYPEER']    = 0;
3298
        $this->options['CURLOPT_SSL_VERIFYHOST']    = 2;
3299
        $this->options['CURLOPT_CONNECTTIMEOUT']    = 30;
3300
 
3301
        if ($cacert = self::get_cacert()) {
3302
            $this->options['CURLOPT_CAINFO'] = $cacert;
3303
        }
3304
    }
3305
 
3306
    /**
3307
     * Get the location of ca certificates.
3308
     * @return string absolute file path or empty if default used
3309
     */
3310
    public static function get_cacert() {
3311
        global $CFG;
3312
 
3313
        // Bundle in dataroot always wins.
3314
        if (is_readable("$CFG->dataroot/moodleorgca.crt")) {
3315
            return realpath("$CFG->dataroot/moodleorgca.crt");
3316
        }
3317
 
3318
        // Next comes the default from php.ini
3319
        $cacert = ini_get('curl.cainfo');
3320
        if (!empty($cacert) and is_readable($cacert)) {
3321
            return realpath($cacert);
3322
        }
3323
 
3324
        // Windows PHP does not have any certs, we need to use something.
3325
        if ($CFG->ostype === 'WINDOWS') {
3326
            if (is_readable("$CFG->libdir/cacert.pem")) {
3327
                return realpath("$CFG->libdir/cacert.pem");
3328
            }
3329
        }
3330
 
3331
        // Use default, this should work fine on all properly configured *nix systems.
3332
        return null;
3333
    }
3334
 
3335
    /**
3336
     * Reset Cookie
3337
     */
3338
    public function resetcookie() {
3339
        if (!empty($this->cookie)) {
3340
            if (is_file($this->cookie)) {
3341
                $fp = fopen($this->cookie, 'w');
3342
                if (!empty($fp)) {
3343
                    fwrite($fp, '');
3344
                    fclose($fp);
3345
                }
3346
            }
3347
        }
3348
    }
3349
 
3350
    /**
3351
     * Set curl options.
3352
     *
3353
     * Do not use the curl constants to define the options, pass a string
3354
     * corresponding to that constant. Ie. to set CURLOPT_MAXREDIRS, pass
3355
     * array('CURLOPT_MAXREDIRS' => 10) or array('maxredirs' => 10) to this method.
3356
     *
3357
     * @param array $options If array is null, this function will reset the options to default value.
3358
     * @return void
3359
     * @throws coding_exception If an option uses constant value instead of option name.
3360
     */
3361
    public function setopt($options = array()) {
3362
        if (is_array($options)) {
3363
            foreach ($options as $name => $val) {
3364
                if (!is_string($name)) {
3365
                    throw new coding_exception('Curl options should be defined using strings, not constant values.');
3366
                }
3367
                if (stripos($name, 'CURLOPT_') === false) {
3368
                    // Only prefix with CURLOPT_ if the option doesn't contain CURLINFO_,
3369
                    // which is a valid prefix for at least one option CURLINFO_HEADER_OUT.
3370
                    if (stripos($name, 'CURLINFO_') === false) {
3371
                        $name = strtoupper('CURLOPT_'.$name);
3372
                    }
3373
                } else {
3374
                    $name = strtoupper($name);
3375
                }
3376
                $this->options[$name] = $val;
3377
            }
3378
        }
3379
    }
3380
 
3381
    /**
3382
     * Reset http method
3383
     */
3384
    public function cleanopt() {
3385
        unset($this->options['CURLOPT_HTTPGET']);
3386
        unset($this->options['CURLOPT_POST']);
3387
        unset($this->options['CURLOPT_POSTFIELDS']);
3388
        unset($this->options['CURLOPT_PUT']);
3389
        unset($this->options['CURLOPT_INFILE']);
3390
        unset($this->options['CURLOPT_INFILESIZE']);
3391
        unset($this->options['CURLOPT_CUSTOMREQUEST']);
3392
        unset($this->options['CURLOPT_FILE']);
3393
    }
3394
 
3395
    /**
3396
     * Resets the HTTP Request headers (to prepare for the new request)
3397
     */
3398
    public function resetHeader() {
3399
        $this->header = array();
3400
    }
3401
 
3402
    /**
3403
     * Set HTTP Request Header
3404
     *
3405
     * @param array|string $header
3406
     */
3407
    public function setHeader($header) {
3408
        if (is_array($header)) {
3409
            foreach ($header as $v) {
3410
                $this->setHeader($v);
3411
            }
3412
        } else {
3413
            // Remove newlines, they are not allowed in headers.
3414
            $newvalue = preg_replace('/[\r\n]/', '', $header);
3415
            if (!in_array($newvalue, $this->header)) {
3416
                $this->header[] = $newvalue;
3417
            }
3418
        }
3419
    }
3420
 
3421
    /**
3422
     * Get HTTP Response Headers
3423
     * @return array of arrays
3424
     */
3425
    public function getResponse() {
3426
        return $this->response;
3427
    }
3428
 
3429
    /**
3430
     * Get raw HTTP Response Headers
3431
     * @return array of strings
3432
     */
3433
    public function get_raw_response() {
3434
        return $this->rawresponse;
3435
    }
3436
 
3437
    /**
3438
     * private callback function
3439
     * Formatting HTTP Response Header
3440
     *
3441
     * We only keep the last headers returned. For example during a redirect the
3442
     * redirect headers will not appear in {@link self::getResponse()}, if you need
3443
     * to use those headers, refer to {@link self::get_raw_response()}.
3444
     *
3445
     * @param resource $ch Apparently not used
3446
     * @param string $header
3447
     * @return int The strlen of the header
3448
     */
3449
    private function formatHeader($ch, $header) {
3450
        $this->rawresponse[] = $header;
3451
 
3452
        if (trim($header, "\r\n") === '') {
3453
            // This must be the last header.
3454
            $this->responsefinished = true;
3455
        }
3456
 
3457
        if (strlen($header) > 2) {
3458
            if ($this->responsefinished) {
3459
                // We still have headers after the supposedly last header, we must be
3460
                // in a redirect so let's empty the response to keep the last headers.
3461
                $this->responsefinished = false;
3462
                $this->response = array();
3463
            }
3464
            $parts = explode(" ", rtrim($header, "\r\n"), 2);
3465
            $key = rtrim($parts[0], ':');
3466
            $value = isset($parts[1]) ? $parts[1] : null;
3467
            if (!empty($this->response[$key])) {
3468
                if (is_array($this->response[$key])) {
3469
                    $this->response[$key][] = $value;
3470
                } else {
3471
                    $tmp = $this->response[$key];
3472
                    $this->response[$key] = array();
3473
                    $this->response[$key][] = $tmp;
3474
                    $this->response[$key][] = $value;
3475
 
3476
                }
3477
            } else {
3478
                $this->response[$key] = $value;
3479
            }
3480
        }
3481
        return strlen($header);
3482
    }
3483
 
3484
    /**
3485
     * Set options for individual curl instance
3486
     *
3487
     * @param resource|CurlHandle $curl A curl handle
3488
     * @param array $options
3489
     * @return resource The curl handle
3490
     */
3491
    private function apply_opt($curl, $options) {
3492
        // Clean up
3493
        $this->cleanopt();
3494
        // set cookie
3495
        if (!empty($this->cookie) || !empty($options['cookie'])) {
3496
            $this->setopt(array('cookiejar'=>$this->cookie,
3497
                            'cookiefile'=>$this->cookie
3498
                             ));
3499
        }
3500
 
3501
        // Bypass proxy if required.
3502
        if ($this->proxy === null) {
3503
            if (!empty($this->options['CURLOPT_URL']) and is_proxybypass($this->options['CURLOPT_URL'])) {
3504
                $proxy = false;
3505
            } else {
3506
                $proxy = true;
3507
            }
3508
        } else {
3509
            $proxy = (bool)$this->proxy;
3510
        }
3511
 
3512
        // Set proxy.
3513
        if ($proxy) {
3514
            $options['CURLOPT_PROXY'] = $this->proxy_host;
3515
        } else {
3516
            unset($this->options['CURLOPT_PROXY']);
3517
        }
3518
 
3519
        $this->setopt($options);
3520
 
3521
        // Reset before set options.
3522
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this,'formatHeader'));
3523
 
3524
        // Setting the User-Agent based on options provided.
3525
        $useragent = '';
3526
 
3527
        if (!empty($options['CURLOPT_USERAGENT'])) {
3528
            $useragent = $options['CURLOPT_USERAGENT'];
3529
        } else if (!empty($this->options['CURLOPT_USERAGENT'])) {
3530
            $useragent = $this->options['CURLOPT_USERAGENT'];
3531
        } else {
3532
            $useragent = \core_useragent::get_moodlebot_useragent();
3533
        }
3534
 
3535
        // Set headers.
3536
        if (empty($this->header)) {
3537
            $this->setHeader(array(
3538
                'User-Agent: ' . $useragent,
3539
                'Connection: keep-alive'
3540
                ));
3541
        } else if (!in_array('User-Agent: ' . $useragent, $this->header)) {
3542
            // Remove old User-Agent if one existed.
3543
            // We have to partial search since we don't know what the original User-Agent is.
3544
            if ($match = preg_grep('/User-Agent.*/', $this->header)) {
3545
                $key = array_keys($match)[0];
3546
                unset($this->header[$key]);
3547
            }
3548
            $this->setHeader(array('User-Agent: ' . $useragent));
3549
        }
3550
        curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
3551
 
3552
        if ($this->debug) {
3553
            echo '<h1>Options</h1>';
3554
            var_dump($this->options);
3555
            echo '<h1>Header</h1>';
3556
            var_dump($this->header);
3557
        }
3558
 
3559
        // Do not allow infinite redirects.
3560
        if (!isset($this->options['CURLOPT_MAXREDIRS'])) {
3561
            $this->options['CURLOPT_MAXREDIRS'] = 0;
3562
        } else if ($this->options['CURLOPT_MAXREDIRS'] > 100) {
3563
            $this->options['CURLOPT_MAXREDIRS'] = 100;
3564
        } else {
3565
            $this->options['CURLOPT_MAXREDIRS'] = (int)$this->options['CURLOPT_MAXREDIRS'];
3566
        }
3567
 
3568
        // Make sure we always know if redirects expected.
3569
        if (!isset($this->options['CURLOPT_FOLLOWLOCATION'])) {
3570
            $this->options['CURLOPT_FOLLOWLOCATION'] = 0;
3571
        }
3572
 
3573
        // Limit the protocols to HTTP and HTTPS.
3574
        if (defined('CURLOPT_PROTOCOLS')) {
3575
            $this->options['CURLOPT_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);
3576
            $this->options['CURLOPT_REDIR_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);
3577
        }
3578
 
3579
        // Set options.
3580
        foreach($this->options as $name => $val) {
3581
            if ($name === 'CURLOPT_FOLLOWLOCATION') {
3582
                // All the redirects are emulated at PHP level.
3583
                curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 0);
3584
                continue;
3585
            }
3586
            $name = constant($name);
3587
            curl_setopt($curl, $name, $val);
3588
        }
3589
 
3590
        return $curl;
3591
    }
3592
 
3593
    /**
3594
     * Download multiple files in parallel
3595
     *
3596
     * Calls {@link multi()} with specific download headers
3597
     *
3598
     * <code>
3599
     * $c = new curl();
3600
     * $file1 = fopen('a', 'wb');
3601
     * $file2 = fopen('b', 'wb');
3602
     * $c->download(array(
3603
     *     array('url'=>'http://localhost/', 'file'=>$file1),
3604
     *     array('url'=>'http://localhost/20/', 'file'=>$file2)
3605
     * ));
3606
     * fclose($file1);
3607
     * fclose($file2);
3608
     * </code>
3609
     *
3610
     * or
3611
     *
3612
     * <code>
3613
     * $c = new curl();
3614
     * $c->download(array(
3615
     *              array('url'=>'http://localhost/', 'filepath'=>'/tmp/file1.tmp'),
3616
     *              array('url'=>'http://localhost/20/', 'filepath'=>'/tmp/file2.tmp')
3617
     *              ));
3618
     * </code>
3619
     *
3620
     * @param array $requests An array of files to request {
3621
     *                  url => url to download the file [required]
3622
     *                  file => file handler, or
3623
     *                  filepath => file path
3624
     * }
3625
     * If 'file' and 'filepath' parameters are both specified in one request, the
3626
     * open file handle in the 'file' parameter will take precedence and 'filepath'
3627
     * will be ignored.
3628
     *
3629
     * @param array $options An array of options to set
3630
     * @return array An array of results
3631
     */
3632
    public function download($requests, $options = array()) {
3633
        $options['RETURNTRANSFER'] = false;
3634
        return $this->multi($requests, $options);
3635
    }
3636
 
3637
    /**
3638
     * Returns the current curl security helper.
3639
     *
3640
     * @return \core\files\curl_security_helper instance.
3641
     */
3642
    public function get_security() {
3643
        return $this->securityhelper;
3644
    }
3645
 
3646
    /**
3647
     * Sets the curl security helper.
3648
     *
3649
     * @param \core\files\curl_security_helper $securityobject instance/subclass of the base curl_security_helper class.
3650
     * @return bool true if the security helper could be set, false otherwise.
3651
     */
3652
    public function set_security($securityobject) {
3653
        if ($securityobject instanceof \core\files\curl_security_helper) {
3654
            $this->securityhelper = $securityobject;
3655
            return true;
3656
        }
3657
        return false;
3658
    }
3659
 
3660
    /**
3661
     * Multi HTTP Requests
3662
     * This function could run multi-requests in parallel.
3663
     *
3664
     * @param array $requests An array of files to request
3665
     * @param array $options An array of options to set
3666
     * @return array An array of results
3667
     */
3668
    protected function multi($requests, $options = array()) {
3669
        $count   = count($requests);
3670
        $handles = array();
3671
        $results = array();
3672
        $main    = curl_multi_init();
3673
        for ($i = 0; $i < $count; $i++) {
3674
            if (!empty($requests[$i]['filepath']) and empty($requests[$i]['file'])) {
3675
                // open file
3676
                $requests[$i]['file'] = fopen($requests[$i]['filepath'], 'w');
3677
                $requests[$i]['auto-handle'] = true;
3678
            }
3679
            foreach($requests[$i] as $n=>$v) {
3680
                $options[$n] = $v;
3681
            }
3682
            $handles[$i] = curl_init($requests[$i]['url']);
3683
            $this->apply_opt($handles[$i], $options);
3684
            curl_multi_add_handle($main, $handles[$i]);
3685
        }
3686
        $running = 0;
3687
        do {
3688
            curl_multi_exec($main, $running);
3689
        } while($running > 0);
3690
        for ($i = 0; $i < $count; $i++) {
3691
            if (!empty($options['CURLOPT_RETURNTRANSFER'])) {
3692
                $results[] = true;
3693
            } else {
3694
                $results[] = curl_multi_getcontent($handles[$i]);
3695
            }
3696
            curl_multi_remove_handle($main, $handles[$i]);
3697
        }
3698
        curl_multi_close($main);
3699
 
3700
        for ($i = 0; $i < $count; $i++) {
3701
            if (!empty($requests[$i]['filepath']) and !empty($requests[$i]['auto-handle'])) {
3702
                // close file handler if file is opened in this function
3703
                fclose($requests[$i]['file']);
3704
            }
3705
        }
3706
        return $results;
3707
    }
3708
 
3709
    /**
3710
     * Helper function to reset the request state vars.
3711
     *
3712
     * @return void.
3713
     */
3714
    protected function reset_request_state_vars() {
3715
        $this->info             = array();
3716
        $this->error            = '';
3717
        $this->errno            = 0;
3718
        $this->response         = array();
3719
        $this->rawresponse      = array();
3720
        $this->responsefinished = false;
3721
    }
3722
 
3723
    /**
3724
     * For use only in unit tests - we can pre-set the next curl response.
3725
     * This is useful for unit testing APIs that call external systems.
3726
     * @param string $response
3727
     */
3728
    public static function mock_response($response) {
3729
        if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
3730
            array_push(self::$mockresponses, $response);
3731
        } else {
3732
            throw new coding_exception('mock_response function is only available for unit tests.');
3733
        }
3734
    }
3735
 
3736
    /**
3737
     * check_securityhelper_blocklist.
3738
     * Checks whether the given URL is blocked by checking both plugin's security helpers
3739
     * and core curl security helper or any curl security helper that passed to curl class constructor.
3740
     * If ignoresecurity is set to true, skip checking and consider the url is not blocked.
3741
     * This augments all installed plugin's security helpers if there is any.
3742
     *
3743
     * @param string $url the url to check.
3744
     * @return ?string - an error message if URL is blocked or null if URL is not blocked.
3745
     */
3746
    protected function check_securityhelper_blocklist(string $url): ?string {
3747
 
3748
        // If curl security is not enabled, do not proceed.
3749
        if ($this->ignoresecurity) {
3750
            return null;
3751
        }
3752
 
3753
        // Augment all installed plugin's security helpers if there is any.
3754
        // The plugin's function has to be defined as plugintype_pluginname_curl_security_helper in pluginname/lib.php.
3755
        $plugintypes = get_plugins_with_function('curl_security_helper');
3756
 
3757
        // If any of the security helper's function returns true, treat as URL is blocked.
3758
        foreach ($plugintypes as $plugins) {
3759
            foreach ($plugins as $pluginfunction) {
3760
                // Get curl security helper object from plugin lib.php.
3761
                $pluginsecurityhelper = $pluginfunction();
3762
                if ($pluginsecurityhelper instanceof \core\files\curl_security_helper_base) {
3763
                    if ($pluginsecurityhelper->url_is_blocked($url)) {
3764
                        $this->error = $pluginsecurityhelper->get_blocked_url_string();
3765
                        return $this->error;
3766
                    }
3767
                }
3768
            }
3769
        }
3770
 
3771
        // Check if the URL is blocked in core curl_security_helper or
3772
        // curl security helper that passed to curl class constructor.
3773
        if ($this->securityhelper->url_is_blocked($url)) {
3774
            $this->error = $this->securityhelper->get_blocked_url_string();
3775
            return $this->error;
3776
        }
3777
 
1441 ariadna 3778
        // Set allowed resolve info if the URL is not blocked.
3779
        $this->curlresolveinfo = $this->securityhelper->get_resolve_info();
3780
 
1 efrain 3781
        return null;
3782
    }
3783
 
3784
    /**
3785
     * Single HTTP Request
3786
     *
3787
     * @param string $url The URL to request
3788
     * @param array $options
3789
     * @return string
3790
     */
3791
    protected function request($url, $options = array()) {
3792
        // Reset here so that the data is valid when result returned from cache, or if we return due to a blocked URL hit.
3793
        $this->reset_request_state_vars();
3794
 
3795
        if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
3796
            $mockresponse = array_pop(self::$mockresponses);
3797
            if ($mockresponse !== null) {
3798
                $this->info = [ 'http_code' => 200 ];
3799
                return $mockresponse;
3800
            }
3801
        }
3802
 
3803
        if (empty($this->emulateredirects)) {
3804
            // Just in case someone had tried to explicitly disable emulated redirects in legacy code.
3805
            debugging('Attempting to disable emulated redirects has no effect any more!', DEBUG_DEVELOPER);
3806
        }
3807
 
3808
        $urlisblocked = $this->check_securityhelper_blocklist($url);
3809
        if (!is_null($urlisblocked)) {
3810
            $this->trigger_url_blocked_event($url, $urlisblocked);
3811
            return $urlisblocked;
3812
        }
3813
 
3814
        // Set the URL as a curl option.
3815
        $this->setopt(array('CURLOPT_URL' => $url));
3816
 
1441 ariadna 3817
        // Force cURL to only resolve the URL from IP/port combinations that were validated by the security helper.
3818
        // This prevents re-fetching DNS data on subsequent requests, which could return un-validated hosts/ports.
3819
        $this->setopt(['CURLOPT_RESOLVE' => $this->curlresolveinfo]);
3820
 
1 efrain 3821
        // Create curl instance.
3822
        $curl = curl_init();
3823
 
3824
        $this->apply_opt($curl, $options);
3825
        if ($this->cache && $ret = $this->cache->get($this->options)) {
3826
            return $ret;
3827
        }
3828
 
3829
        $ret = curl_exec($curl);
3830
        $this->info  = curl_getinfo($curl);
3831
        $this->error = curl_error($curl);
3832
        $this->errno = curl_errno($curl);
3833
        // Note: $this->response and $this->rawresponse are filled by $hits->formatHeader callback.
3834
 
3835
        if (intval($this->info['redirect_count']) > 0) {
3836
            // For security reasons we do not allow the cURL handle to follow redirects on its own.
3837
            // See setting CURLOPT_FOLLOWLOCATION in {@see self::apply_opt()} method.
3838
            throw new coding_exception('Internal cURL handle should never follow redirects on its own!',
3839
                'Reported number of redirects: ' . $this->info['redirect_count']);
3840
        }
3841
 
3842
        if ($this->options['CURLOPT_FOLLOWLOCATION'] && $this->info['http_code'] != 200) {
3843
            $redirects = 0;
3844
 
3845
            while($redirects <= $this->options['CURLOPT_MAXREDIRS']) {
3846
 
3847
                if ($this->info['http_code'] == 301) {
3848
                    // Moved Permanently - repeat the same request on new URL.
3849
 
3850
                } else if ($this->info['http_code'] == 302) {
3851
                    // Found - the standard redirect - repeat the same request on new URL.
3852
 
3853
                } else if ($this->info['http_code'] == 303) {
3854
                    // 303 See Other - repeat only if GET, do not bother with POSTs.
3855
                    if (empty($this->options['CURLOPT_HTTPGET'])) {
3856
                        break;
3857
                    }
3858
 
3859
                } else if ($this->info['http_code'] == 307) {
3860
                    // Temporary Redirect - must repeat using the same request type.
3861
 
3862
                } else if ($this->info['http_code'] == 308) {
3863
                    // Permanent Redirect - must repeat using the same request type.
3864
 
3865
                } else {
3866
                    // Some other http code means do not retry!
3867
                    break;
3868
                }
3869
 
3870
                $redirects++;
3871
 
3872
                $currenturl = $redirecturl ?? $url;
3873
                $redirecturl = null;
3874
                if (isset($this->info['redirect_url'])) {
3875
                    if (preg_match('|^https?://|i', $this->info['redirect_url'])) {
3876
                        $redirecturl = $this->info['redirect_url'];
3877
                    } else {
3878
                        // Emulate CURLOPT_REDIR_PROTOCOLS behaviour which we have set to (CURLPROTO_HTTP | CURLPROTO_HTTPS) only.
3879
                        $this->errno = CURLE_UNSUPPORTED_PROTOCOL;
3880
                        $this->error = 'Redirect to a URL with unsuported protocol: ' . $this->info['redirect_url'];
3881
                        curl_close($curl);
3882
                        return $this->error;
3883
                    }
3884
                }
3885
                if (!$redirecturl) {
3886
                    foreach ($this->response as $k => $v) {
3887
                        if (strtolower($k) === 'location') {
3888
                            $redirecturl = $v;
3889
                            break;
3890
                        }
3891
                    }
3892
                    if (preg_match('|^https?://|i', $redirecturl)) {
3893
                        // Great, this is the correct location format!
3894
 
3895
                    } else if ($redirecturl) {
3896
                        $current = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
3897
                        if (strpos($redirecturl, '/') === 0) {
3898
                            // Relative to server root - just guess.
3899
                            $pos = strpos('/', $current, 8);
3900
                            if ($pos === false) {
3901
                                $redirecturl = $current.$redirecturl;
3902
                            } else {
3903
                                $redirecturl = substr($current, 0, $pos).$redirecturl;
3904
                            }
3905
                        } else {
3906
                            // Relative to current script.
3907
                            $redirecturl = dirname($current).'/'.$redirecturl;
3908
                        }
3909
                    }
3910
                }
3911
 
3912
                $urlisblocked = $this->check_securityhelper_blocklist($redirecturl);
3913
                if (!is_null($urlisblocked)) {
3914
                    $this->reset_request_state_vars();
3915
                    curl_close($curl);
3916
                    $this->trigger_url_blocked_event($redirecturl, $urlisblocked, true);
3917
                    return $urlisblocked;
3918
                }
3919
 
3920
                // If the response body is written to a seekable stream resource, reset the stream pointer to avoid
3921
                // appending multiple response bodies to the same resource.
3922
                if (!empty($this->options['CURLOPT_FILE'])) {
3923
                    $streammetadata = stream_get_meta_data($this->options['CURLOPT_FILE']);
3924
                    if ($streammetadata['seekable']) {
3925
                        ftruncate($this->options['CURLOPT_FILE'], 0);
3926
                        rewind($this->options['CURLOPT_FILE']);
3927
                    }
3928
                }
3929
 
3930
                curl_setopt($curl, CURLOPT_URL, $redirecturl);
3931
 
1441 ariadna 3932
                // Force cURL to only resolve the URL from IP/port combinations that were validated by the security helper.
3933
                // This prevents re-fetching DNS data on subsequent requests, which could return un-validated hosts/ports.
3934
                $this->setopt(['CURLOPT_RESOLVE' => $this->curlresolveinfo]);
3935
 
11 efrain 3936
                // If CURLOPT_UNRESTRICTED_AUTH is empty/false, don't send credentials to other hosts.
3937
                // Ref: https://curl.se/libcurl/c/CURLOPT_UNRESTRICTED_AUTH.html.
3938
                $isdifferenthost = parse_url($currenturl)['host'] !== parse_url($redirecturl)['host'];
3939
                $sendauthentication = !empty($this->options['CURLOPT_UNRESTRICTED_AUTH']);
3940
                if ($isdifferenthost && !$sendauthentication) {
1 efrain 3941
                    curl_setopt($curl, CURLOPT_HTTPAUTH, null);
3942
                    curl_setopt($curl, CURLOPT_USERPWD, null);
11 efrain 3943
                    // Check whether the CURLOPT_HTTPHEADER is specified.
3944
                    if (!empty($this->options['CURLOPT_HTTPHEADER'])) {
3945
                        // Remove the "Authorization:" header, if any.
3946
                        $headerredirect = array_filter(
3947
                            $this->options['CURLOPT_HTTPHEADER'],
3948
                            fn($header) => strpos($header, 'Authorization:') === false
3949
                        );
3950
                        curl_setopt($curl, CURLOPT_HTTPHEADER, $headerredirect);
3951
                    }
1 efrain 3952
                }
3953
 
3954
                $ret = curl_exec($curl);
3955
 
3956
                $this->info  = curl_getinfo($curl);
3957
                $this->error = curl_error($curl);
3958
                $this->errno = curl_errno($curl);
3959
 
3960
                $this->info['redirect_count'] = $redirects;
3961
 
3962
                if ($this->info['http_code'] === 200) {
3963
                    // Finally this is what we wanted.
3964
                    break;
3965
                }
3966
                if ($this->errno != CURLE_OK) {
3967
                    // Something wrong is going on.
3968
                    break;
3969
                }
3970
            }
3971
            if ($redirects > $this->options['CURLOPT_MAXREDIRS']) {
3972
                $this->errno = CURLE_TOO_MANY_REDIRECTS;
3973
                $this->error = 'Maximum ('.$this->options['CURLOPT_MAXREDIRS'].') redirects followed';
3974
            }
3975
        }
3976
 
3977
        if ($this->cache) {
3978
            $this->cache->set($this->options, $ret);
3979
        }
3980
 
3981
        if ($this->debug) {
3982
            echo '<h1>Return Data</h1>';
3983
            var_dump($ret);
3984
            echo '<h1>Info</h1>';
3985
            var_dump($this->info);
3986
            echo '<h1>Error</h1>';
3987
            var_dump($this->error);
3988
        }
3989
 
3990
        curl_close($curl);
3991
 
3992
        if (empty($this->error)) {
3993
            return $ret;
3994
        } else {
3995
            return $this->error;
3996
            // exception is not ajax friendly
3997
            //throw new moodle_exception($this->error, 'curl');
3998
        }
3999
    }
4000
 
4001
    /**
4002
     * Trigger url_blocked event
4003
     *
4004
     * @param string $url      The URL to request
4005
     * @param string $reason   Reason for blocking
4006
     * @param bool   $redirect true if it was a redirect
4007
     */
4008
    private function trigger_url_blocked_event($url, $reason, $redirect = false): void {
4009
        $params = [
4010
            'url' => $url,
4011
            'reason' => $reason,
4012
            'redirect' => $redirect,
4013
        ];
4014
        $event = core\event\url_blocked::create(['other' => $params]);
4015
        $event->trigger();
4016
    }
4017
 
4018
    /**
4019
     * HTTP HEAD method
4020
     *
4021
     * @see request()
4022
     *
4023
     * @param string $url
4024
     * @param array $options
4025
     * @return string
4026
     */
4027
    public function head($url, $options = array()) {
4028
        $options['CURLOPT_HTTPGET'] = 0;
4029
        $options['CURLOPT_HEADER']  = 1;
4030
        $options['CURLOPT_NOBODY']  = 1;
4031
        return $this->request($url, $options);
4032
    }
4033
 
4034
    /**
4035
     * HTTP PATCH method
4036
     *
4037
     * @param string $url
4038
     * @param array|string $params
4039
     * @param array $options
4040
     * @return string
4041
     */
4042
    public function patch($url, $params = '', $options = array()) {
4043
        $options['CURLOPT_CUSTOMREQUEST'] = 'PATCH';
4044
        if (is_array($params)) {
4045
            $this->_tmp_file_post_params = array();
4046
            foreach ($params as $key => $value) {
4047
                if ($value instanceof stored_file) {
4048
                    $value->add_to_curl_request($this, $key);
4049
                } else {
4050
                    $this->_tmp_file_post_params[$key] = $value;
4051
                }
4052
            }
4053
            $options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params;
4054
            unset($this->_tmp_file_post_params);
4055
        } else {
4056
            // The variable $params is the raw post data.
4057
            $options['CURLOPT_POSTFIELDS'] = $params;
4058
        }
4059
        return $this->request($url, $options);
4060
    }
4061
 
4062
    /**
4063
     * HTTP POST method
4064
     *
4065
     * @param string $url
4066
     * @param array|string $params
4067
     * @param array $options
4068
     * @return string
4069
     */
4070
    public function post($url, $params = '', $options = array()) {
4071
        $options['CURLOPT_POST']       = 1;
4072
        if (is_array($params)) {
4073
            $this->_tmp_file_post_params = array();
4074
            foreach ($params as $key => $value) {
4075
                if ($value instanceof stored_file) {
4076
                    $value->add_to_curl_request($this, $key);
4077
                } else {
4078
                    $this->_tmp_file_post_params[$key] = $value;
4079
                }
4080
            }
4081
            $options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params;
4082
            unset($this->_tmp_file_post_params);
4083
        } else {
4084
            // $params is the raw post data
4085
            $options['CURLOPT_POSTFIELDS'] = $params;
4086
        }
4087
        return $this->request($url, $options);
4088
    }
4089
 
4090
    /**
4091
     * HTTP GET method
4092
     *
4093
     * @param string $url
4094
     * @param ?array $params
4095
     * @param array $options
4096
     * @return string
4097
     */
4098
    public function get($url, $params = array(), $options = array()) {
4099
        $options['CURLOPT_HTTPGET'] = 1;
4100
 
4101
        if (!empty($params)) {
4102
            $url .= (stripos($url, '?') !== false) ? '&' : '?';
4103
            $url .= http_build_query($params, '', '&');
4104
        }
4105
        return $this->request($url, $options);
4106
    }
4107
 
4108
    /**
4109
     * Downloads one file and writes it to the specified file handler
4110
     *
4111
     * <code>
4112
     * $c = new curl();
4113
     * $file = fopen('savepath', 'w');
4114
     * $result = $c->download_one('http://localhost/', null,
4115
     *   array('file' => $file, 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
4116
     * fclose($file);
4117
     * $download_info = $c->get_info();
4118
     * if ($result === true) {
4119
     *   // file downloaded successfully
4120
     * } else {
4121
     *   $error_text = $result;
4122
     *   $error_code = $c->get_errno();
4123
     * }
4124
     * </code>
4125
     *
4126
     * <code>
4127
     * $c = new curl();
4128
     * $result = $c->download_one('http://localhost/', null,
4129
     *   array('filepath' => 'savepath', 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
4130
     * // ... see above, no need to close handle and remove file if unsuccessful
4131
     * </code>
4132
     *
4133
     * @param string $url
4134
     * @param array|null $params key-value pairs to be added to $url as query string
4135
     * @param array $options request options. Must include either 'file' or 'filepath'
4136
     * @return bool|string true on success or error string on failure
4137
     */
4138
    public function download_one($url, $params, $options = array()) {
4139
        $options['CURLOPT_HTTPGET'] = 1;
4140
        if (!empty($params)) {
4141
            $url .= (stripos($url, '?') !== false) ? '&' : '?';
4142
            $url .= http_build_query($params, '', '&');
4143
        }
4144
        if (!empty($options['filepath']) && empty($options['file'])) {
4145
            // open file
4146
            if (!($options['file'] = fopen($options['filepath'], 'w'))) {
4147
                $this->errno = 100;
4148
                return get_string('cannotwritefile', 'error', $options['filepath']);
4149
            }
4150
            $filepath = $options['filepath'];
4151
        }
4152
        unset($options['filepath']);
4153
        $result = $this->request($url, $options);
4154
        if (isset($filepath)) {
4155
            fclose($options['file']);
4156
            if ($result !== true) {
4157
                unlink($filepath);
4158
            }
4159
        }
4160
        return $result;
4161
    }
4162
 
4163
    /**
4164
     * HTTP PUT method
4165
     *
4166
     * @param string $url
4167
     * @param array $params
4168
     * @param array $options
4169
     * @return ?string
4170
     */
4171
    public function put($url, $params = array(), $options = array()) {
4172
        $file = '';
4173
        $fp = false;
4174
        if (isset($params['file'])) {
4175
            $file = $params['file'];
4176
            if (is_file($file)) {
4177
                $fp   = fopen($file, 'r');
4178
                $size = filesize($file);
4179
                $options['CURLOPT_PUT']        = 1;
4180
                $options['CURLOPT_INFILESIZE'] = $size;
4181
                $options['CURLOPT_INFILE']     = $fp;
4182
            } else {
4183
                return null;
4184
            }
4185
            if (!isset($this->options['CURLOPT_USERPWD'])) {
4186
                $this->setopt(array('CURLOPT_USERPWD' => 'anonymous: noreply@moodle.org'));
4187
            }
4188
        } else {
4189
            $options['CURLOPT_CUSTOMREQUEST'] = 'PUT';
4190
            $options['CURLOPT_POSTFIELDS'] = $params;
4191
        }
4192
 
4193
        $ret = $this->request($url, $options);
4194
        if ($fp !== false) {
4195
            fclose($fp);
4196
        }
4197
        return $ret;
4198
    }
4199
 
4200
    /**
4201
     * HTTP DELETE method
4202
     *
4203
     * @param string $url
4204
     * @param array $param
4205
     * @param array $options
4206
     * @return string
4207
     */
4208
    public function delete($url, $param = array(), $options = array()) {
4209
        $options['CURLOPT_CUSTOMREQUEST'] = 'DELETE';
4210
        if (!isset($options['CURLOPT_USERPWD'])) {
4211
            $options['CURLOPT_USERPWD'] = 'anonymous: noreply@moodle.org';
4212
        }
4213
        $ret = $this->request($url, $options);
4214
        return $ret;
4215
    }
4216
 
4217
    /**
4218
     * HTTP TRACE method
4219
     *
4220
     * @param string $url
4221
     * @param array $options
4222
     * @return string
4223
     */
4224
    public function trace($url, $options = array()) {
4225
        $options['CURLOPT_CUSTOMREQUEST'] = 'TRACE';
4226
        $ret = $this->request($url, $options);
4227
        return $ret;
4228
    }
4229
 
4230
    /**
4231
     * HTTP OPTIONS method
4232
     *
4233
     * @param string $url
4234
     * @param array $options
4235
     * @return string
4236
     */
4237
    public function options($url, $options = array()) {
4238
        $options['CURLOPT_CUSTOMREQUEST'] = 'OPTIONS';
4239
        $ret = $this->request($url, $options);
4240
        return $ret;
4241
    }
4242
 
4243
    /**
4244
     * Get curl information
4245
     *
4246
     * @return array
4247
     */
4248
    public function get_info() {
4249
        return $this->info;
4250
    }
4251
 
4252
    /**
4253
     * Get curl error code
4254
     *
4255
     * @return int
4256
     */
4257
    public function get_errno() {
4258
        return $this->errno;
4259
    }
4260
 
4261
    /**
4262
     * When using a proxy, an additional HTTP response code may appear at
4263
     * the start of the header. For example, when using https over a proxy
4264
     * there may be 'HTTP/1.0 200 Connection Established'. Other codes are
4265
     * also possible and some may come with their own headers.
4266
     *
4267
     * If using the return value containing all headers, this function can be
4268
     * called to remove unwanted doubles.
4269
     *
4270
     * Note that it is not possible to distinguish this situation from valid
4271
     * data unless you know the actual response part (below the headers)
4272
     * will not be included in this string, or else will not 'look like' HTTP
4273
     * headers. As a result it is not safe to call this function for general
4274
     * data.
4275
     *
4276
     * @param string $input Input HTTP response
4277
     * @return string HTTP response with additional headers stripped if any
4278
     */
4279
    public static function strip_double_headers($input) {
4280
        // I have tried to make this regular expression as specific as possible
4281
        // to avoid any case where it does weird stuff if you happen to put
4282
        // HTTP/1.1 200 at the start of any line in your RSS file. This should
4283
        // also make it faster because it can abandon regex processing as soon
4284
        // as it hits something that doesn't look like an http header. The
4285
        // header definition is taken from RFC 822, except I didn't support
4286
        // folding which is never used in practice.
4287
        $crlf = "\r\n";
4288
        return preg_replace(
4289
                // HTTP version and status code (ignore value of code).
4290
                '~^HTTP/[1-9](\.[0-9])?.*' . $crlf .
4291
                // Header name: character between 33 and 126 decimal, except colon.
4292
                // Colon. Header value: any character except \r and \n. CRLF.
4293
                '(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .
4294
                // Headers are terminated by another CRLF (blank line).
4295
                $crlf .
4296
                // Second HTTP status code, this time must be 200.
4297
                '(HTTP/[1-9](\.[0-9])? 200)~', '$2', $input);
4298
    }
4299
}
4300
 
4301
/**
4302
 * This class is used by cURL class, use case:
4303
 *
4304
 * <code>
4305
 * $CFG->repositorycacheexpire = 120;
4306
 * $CFG->curlcache = 120;
4307
 *
4308
 * $c = new curl(array('cache'=>true), 'module_cache'=>'repository');
4309
 * $ret = $c->get('http://www.google.com');
4310
 * </code>
4311
 *
4312
 * @package   core_files
4313
 * @copyright Dongsheng Cai <dongsheng@moodle.com>
4314
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4315
 */
4316
class curl_cache {
4317
    /** @var string Path to cache directory */
4318
    public $dir = '';
4319
 
4320
    /** @var int the repositorycacheexpire config value. */
4321
    private $ttl;
4322
 
4323
    /**
4324
     * Constructor
4325
     *
4326
     * @global stdClass $CFG
4327
     * @param string $module which module is using curl_cache
4328
     */
4329
    public function __construct($module = 'repository') {
4330
        global $CFG;
4331
        if (!empty($module)) {
4332
            $this->dir = $CFG->cachedir.'/'.$module.'/';
4333
        } else {
4334
            $this->dir = $CFG->cachedir.'/misc/';
4335
        }
4336
        if (!file_exists($this->dir)) {
4337
            mkdir($this->dir, $CFG->directorypermissions, true);
4338
        }
4339
        if ($module == 'repository') {
4340
            if (empty($CFG->repositorycacheexpire)) {
4341
                $CFG->repositorycacheexpire = 120;
4342
            }
4343
            $this->ttl = $CFG->repositorycacheexpire;
4344
        } else {
4345
            if (empty($CFG->curlcache)) {
4346
                $CFG->curlcache = 120;
4347
            }
4348
            $this->ttl = $CFG->curlcache;
4349
        }
4350
    }
4351
 
4352
    /**
4353
     * Get cached value
4354
     *
4355
     * @global stdClass $CFG
4356
     * @global stdClass $USER
4357
     * @param mixed $param
4358
     * @return bool|string
4359
     */
4360
    public function get($param) {
4361
        global $CFG, $USER;
4362
        $this->cleanup($this->ttl);
4363
        $filename = 'u'.$USER->id.'_'.md5(serialize($param));
4364
        if(file_exists($this->dir.$filename)) {
4365
            $lasttime = filemtime($this->dir.$filename);
4366
            if (time()-$lasttime > $this->ttl) {
4367
                return false;
4368
            } else {
4369
                $fp = fopen($this->dir.$filename, 'r');
4370
                $size = filesize($this->dir.$filename);
4371
                $content = fread($fp, $size);
4372
                return unserialize($content);
4373
            }
4374
        }
4375
        return false;
4376
    }
4377
 
4378
    /**
4379
     * Set cache value
4380
     *
4381
     * @global object $CFG
4382
     * @global object $USER
4383
     * @param mixed $param
4384
     * @param mixed $val
4385
     */
4386
    public function set($param, $val) {
4387
        global $CFG, $USER;
4388
        $filename = 'u'.$USER->id.'_'.md5(serialize($param));
4389
        $fp = fopen($this->dir.$filename, 'w');
4390
        fwrite($fp, serialize($val));
4391
        fclose($fp);
4392
        @chmod($this->dir.$filename, $CFG->filepermissions);
4393
    }
4394
 
4395
    /**
4396
     * Remove cache files
4397
     *
4398
     * @param int $expire The number of seconds before expiry
4399
     */
4400
    public function cleanup($expire) {
4401
        if ($dir = opendir($this->dir)) {
4402
            while (false !== ($file = readdir($dir))) {
4403
                if(!is_dir($file) && $file != '.' && $file != '..') {
4404
                    $lasttime = @filemtime($this->dir.$file);
4405
                    if (time() - $lasttime > $expire) {
4406
                        @unlink($this->dir.$file);
4407
                    }
4408
                }
4409
            }
4410
            closedir($dir);
4411
        }
4412
    }
4413
    /**
4414
     * delete current user's cache file
4415
     *
4416
     * @global object $CFG
4417
     * @global object $USER
4418
     */
4419
    public function refresh() {
4420
        global $CFG, $USER;
4421
        if ($dir = opendir($this->dir)) {
4422
            while (false !== ($file = readdir($dir))) {
4423
                if (!is_dir($file) && $file != '.' && $file != '..') {
4424
                    if (strpos($file, 'u'.$USER->id.'_') !== false) {
4425
                        @unlink($this->dir.$file);
4426
                    }
4427
                }
4428
            }
4429
        }
4430
    }
4431
}
4432
 
4433
/**
4434
 * This function delegates file serving to individual plugins
4435
 *
4436
 * @param string $relativepath
4437
 * @param bool $forcedownload
4438
 * @param null|string $preview the preview mode, defaults to serving the original file
4439
 * @param boolean $offline If offline is requested - don't serve a redirect to an external file, return a file suitable for viewing
4440
 *                         offline (e.g. mobile app).
4441
 * @param bool $embed Whether this file will be served embed into an iframe.
4442
 * @todo MDL-31088 file serving improments
4443
 */
4444
function file_pluginfile($relativepath, $forcedownload, $preview = null, $offline = false, $embed = false) {
4445
    global $DB, $CFG, $USER, $OUTPUT;
4446
    // relative path must start with '/'
4447
    if (!$relativepath) {
4448
        throw new \moodle_exception('invalidargorconf');
4449
    } else if ($relativepath[0] != '/') {
4450
        throw new \moodle_exception('pathdoesnotstartslash');
4451
    }
4452
 
4453
    // extract relative path components
4454
    $args = explode('/', ltrim($relativepath, '/'));
4455
 
4456
    if (count($args) < 3) { // always at least context, component and filearea
4457
        throw new \moodle_exception('invalidarguments');
4458
    }
4459
 
4460
    $contextid = (int)array_shift($args);
4461
    $component = clean_param(array_shift($args), PARAM_COMPONENT);
4462
    $filearea  = clean_param(array_shift($args), PARAM_AREA);
4463
 
4464
    list($context, $course, $cm) = get_context_info_array($contextid);
4465
 
4466
    $fs = get_file_storage();
4467
 
4468
    $sendfileoptions = ['preview' => $preview, 'offline' => $offline, 'embed' => $embed];
4469
 
4470
    // ========================================================================================================================
4471
    if ($component === 'blog') {
4472
        // Blog file serving
4473
        if ($context->contextlevel != CONTEXT_SYSTEM) {
4474
            send_file_not_found();
4475
        }
4476
        if ($filearea !== 'attachment' and $filearea !== 'post') {
4477
            send_file_not_found();
4478
        }
4479
 
4480
        if (empty($CFG->enableblogs)) {
4481
            throw new \moodle_exception('siteblogdisable', 'blog');
4482
        }
4483
 
4484
        $entryid = (int)array_shift($args);
4485
        if (!$entry = $DB->get_record('post', array('module'=>'blog', 'id'=>$entryid))) {
4486
            send_file_not_found();
4487
        }
4488
        if ($CFG->bloglevel < BLOG_GLOBAL_LEVEL) {
4489
            require_login();
4490
            if (isguestuser()) {
4491
                throw new \moodle_exception('noguest');
4492
            }
4493
            if ($CFG->bloglevel == BLOG_USER_LEVEL) {
4494
                if ($USER->id != $entry->userid) {
4495
                    send_file_not_found();
4496
                }
4497
            }
4498
        }
4499
 
4500
        if ($entry->publishstate === 'public') {
4501
            if ($CFG->forcelogin) {
4502
                require_login();
4503
            }
4504
 
4505
        } else if ($entry->publishstate === 'site') {
4506
            require_login();
4507
            //ok
4508
        } else if ($entry->publishstate === 'draft') {
4509
            require_login();
4510
            if ($USER->id != $entry->userid) {
4511
                send_file_not_found();
4512
            }
4513
        }
4514
 
4515
        $filename = array_pop($args);
4516
        $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4517
 
4518
        if (!$file = $fs->get_file($context->id, $component, $filearea, $entryid, $filepath, $filename) or $file->is_directory()) {
4519
            send_file_not_found();
4520
        }
4521
 
4522
        send_stored_file($file, 10*60, 0, true, $sendfileoptions); // download MUST be forced - security!
4523
 
4524
    // ========================================================================================================================
4525
    } else if ($component === 'grade') {
4526
 
4527
        require_once($CFG->libdir . '/grade/constants.php');
4528
 
4529
        if (($filearea === 'outcome' or $filearea === 'scale') and $context->contextlevel == CONTEXT_SYSTEM) {
4530
            // Global gradebook files
4531
            if ($CFG->forcelogin) {
4532
                require_login();
4533
            }
4534
 
4535
            $fullpath = "/$context->id/$component/$filearea/".implode('/', $args);
4536
 
4537
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4538
                send_file_not_found();
4539
            }
4540
 
4541
            \core\session\manager::write_close(); // Unlock session during file serving.
4542
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4543
 
4544
        } else if ($filearea == GRADE_FEEDBACK_FILEAREA || $filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {
4545
            if ($context->contextlevel != CONTEXT_MODULE) {
4546
                send_file_not_found();
4547
            }
4548
 
4549
            require_login($course, false);
4550
 
4551
            $gradeid = (int) array_shift($args);
4552
            $filename = array_pop($args);
4553
            if ($filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {
4554
                $grade = $DB->get_record('grade_grades_history', ['id' => $gradeid]);
4555
            } else {
4556
                $grade = $DB->get_record('grade_grades', ['id' => $gradeid]);
4557
            }
4558
 
4559
            if (!$grade) {
4560
                send_file_not_found();
4561
            }
4562
 
4563
            $iscurrentuser = $USER->id == $grade->userid;
4564
 
4565
            if (!$iscurrentuser) {
4566
                $coursecontext = context_course::instance($course->id);
4567
                if (!has_capability('moodle/grade:viewall', $coursecontext)) {
4568
                    send_file_not_found();
4569
                }
4570
            }
4571
 
4572
            $fullpath = "/$context->id/$component/$filearea/$gradeid/$filename";
4573
 
4574
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4575
                send_file_not_found();
4576
            }
4577
 
4578
            \core\session\manager::write_close(); // Unlock session during file serving.
4579
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4580
        } else {
4581
            send_file_not_found();
4582
        }
4583
 
4584
    // ========================================================================================================================
4585
    } else if ($component === 'tag') {
4586
        if ($filearea === 'description' and $context->contextlevel == CONTEXT_SYSTEM) {
4587
 
4588
            // All tag descriptions are going to be public but we still need to respect forcelogin
4589
            if ($CFG->forcelogin) {
4590
                require_login();
4591
            }
4592
 
4593
            $fullpath = "/$context->id/tag/description/".implode('/', $args);
4594
 
4595
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4596
                send_file_not_found();
4597
            }
4598
 
4599
            \core\session\manager::write_close(); // Unlock session during file serving.
4600
            send_stored_file($file, 60*60, 0, true, $sendfileoptions);
4601
 
4602
        } else {
4603
            send_file_not_found();
4604
        }
4605
    // ========================================================================================================================
4606
    } else if ($component === 'badges') {
4607
        require_once($CFG->libdir . '/badgeslib.php');
4608
 
4609
        $badgeid = (int)array_shift($args);
4610
        $badge = new badge($badgeid);
4611
        $filename = array_pop($args);
4612
 
4613
        if ($filearea === 'badgeimage') {
4614
            if ($filename !== 'f1' && $filename !== 'f2' && $filename !== 'f3') {
4615
                send_file_not_found();
4616
            }
4617
            if (!$file = $fs->get_file($context->id, 'badges', 'badgeimage', $badge->id, '/', $filename.'.png')) {
4618
                send_file_not_found();
4619
            }
4620
 
4621
            \core\session\manager::write_close();
4622
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4623
        } else if ($filearea === 'userbadge'  and $context->contextlevel == CONTEXT_USER) {
4624
            if (!$file = $fs->get_file($context->id, 'badges', 'userbadge', $badge->id, '/', $filename.'.png')) {
4625
                send_file_not_found();
4626
            }
4627
 
4628
            \core\session\manager::write_close();
4629
            send_stored_file($file, 60*60, 0, true, $sendfileoptions);
4630
        }
4631
    // ========================================================================================================================
4632
    } else if ($component === 'calendar') {
4633
        if ($filearea === 'event_description'  and $context->contextlevel == CONTEXT_SYSTEM) {
4634
 
4635
            // All events here are public the one requirement is that we respect forcelogin
4636
            if ($CFG->forcelogin) {
4637
                require_login();
4638
            }
4639
 
4640
            // Get the event if from the args array
4641
            $eventid = array_shift($args);
4642
 
4643
            // Load the event from the database
4644
            if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'eventtype'=>'site'))) {
4645
                send_file_not_found();
4646
            }
4647
 
4648
            // Get the file and serve if successful
4649
            $filename = array_pop($args);
4650
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4651
            if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {
4652
                send_file_not_found();
4653
            }
4654
 
4655
            \core\session\manager::write_close(); // Unlock session during file serving.
4656
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4657
 
4658
        } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_USER) {
4659
 
4660
            // Must be logged in, if they are not then they obviously can't be this user
4661
            require_login();
4662
 
4663
            // Don't want guests here, potentially saves a DB call
4664
            if (isguestuser()) {
4665
                send_file_not_found();
4666
            }
4667
 
4668
            // Get the event if from the args array
4669
            $eventid = array_shift($args);
4670
 
4671
            // Load the event from the database - user id must match
4672
            if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'userid'=>$USER->id, 'eventtype'=>'user'))) {
4673
                send_file_not_found();
4674
            }
4675
 
4676
            // Get the file and serve if successful
4677
            $filename = array_pop($args);
4678
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4679
            if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {
4680
                send_file_not_found();
4681
            }
4682
 
4683
            \core\session\manager::write_close(); // Unlock session during file serving.
4684
            send_stored_file($file, 0, 0, true, $sendfileoptions);
4685
 
4686
        } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSECAT) {
4687
            if ($CFG->forcelogin) {
4688
                require_login();
4689
            }
4690
 
4691
            // Get category, this will also validate access.
4692
            $category = core_course_category::get($context->instanceid);
4693
 
4694
            // Get the event ID from the args array, load event.
4695
            $eventid = array_shift($args);
4696
            $event = $DB->get_record('event', [
4697
                'id' => (int) $eventid,
4698
                'eventtype' => 'category',
4699
                'categoryid' => $category->id,
4700
            ]);
4701
 
4702
            if (!$event) {
4703
                send_file_not_found();
4704
            }
4705
 
4706
            // Retrieve file from storage, and serve.
4707
            $filename = array_pop($args);
4708
            $filepath = $args ? '/' . implode('/', $args) .'/' : '/';
4709
            $file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename);
4710
            if (!$file || $file->is_directory()) {
4711
                send_file_not_found();
4712
            }
4713
 
4714
            // Unlock session during file serving.
4715
            \core\session\manager::write_close();
4716
            send_stored_file($file, HOURSECS, 0, $forcedownload, $sendfileoptions);
4717
        } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSE) {
4718
 
4719
            // Respect forcelogin and require login unless this is the site.... it probably
4720
            // should NEVER be the site
4721
            if ($CFG->forcelogin || $course->id != SITEID) {
4722
                require_login($course);
4723
            }
4724
 
4725
            // Must be able to at least view the course. This does not apply to the front page.
4726
            if ($course->id != SITEID && (!is_enrolled($context)) && (!is_viewing($context))) {
4727
                //TODO: hmm, do we really want to block guests here?
4728
                send_file_not_found();
4729
            }
4730
 
4731
            // Get the event id
4732
            $eventid = array_shift($args);
4733
 
4734
            // Load the event from the database we need to check whether it is
4735
            // a) valid course event
4736
            // b) a group event
4737
            // Group events use the course context (there is no group context)
4738
            if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'courseid'=>$course->id))) {
4739
                send_file_not_found();
4740
            }
4741
 
4742
            // If its a group event require either membership of view all groups capability
4743
            if ($event->eventtype === 'group') {
4744
                if (!has_capability('moodle/site:accessallgroups', $context) && !groups_is_member($event->groupid, $USER->id)) {
4745
                    send_file_not_found();
4746
                }
4747
            } else if ($event->eventtype === 'course' || $event->eventtype === 'site') {
4748
                // Ok. Please note that the event type 'site' still uses a course context.
4749
            } else {
4750
                // Some other type.
4751
                send_file_not_found();
4752
            }
4753
 
4754
            // If we get this far we can serve the file
4755
            $filename = array_pop($args);
4756
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4757
            if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {
4758
                send_file_not_found();
4759
            }
4760
 
4761
            \core\session\manager::write_close(); // Unlock session during file serving.
4762
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4763
 
4764
        } else {
4765
            send_file_not_found();
4766
        }
4767
 
4768
    // ========================================================================================================================
4769
    } else if ($component === 'user') {
4770
        if ($filearea === 'icon' and $context->contextlevel == CONTEXT_USER) {
4771
            if (count($args) == 1) {
4772
                $themename = theme_config::DEFAULT_THEME;
4773
                $filename = array_shift($args);
4774
            } else {
4775
                $themename = array_shift($args);
4776
                $filename = array_shift($args);
4777
            }
4778
 
4779
            // fix file name automatically
4780
            if ($filename !== 'f1' and $filename !== 'f2' and $filename !== 'f3') {
4781
                $filename = 'f1';
4782
            }
4783
 
1441 ariadna 4784
            if (!\core\output\user_picture::allow_view($context->instanceid)) {
1 efrain 4785
                // protect images if login required and not logged in;
4786
                // also if login is required for profile images and is not logged in or guest
4787
                // do not use require_login() because it is expensive and not suitable here anyway
4788
                $theme = theme_config::load($themename);
4789
                redirect($theme->image_url('u/'.$filename, 'moodle')); // intentionally not cached
4790
            }
4791
 
4792
            if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.png')) {
4793
                if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.jpg')) {
4794
                    if ($filename === 'f3') {
4795
                        // f3 512x512px was introduced in 2.3, there might be only the smaller version.
4796
                        if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.png')) {
4797
                            $file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.jpg');
4798
                        }
4799
                    }
4800
                }
4801
            }
4802
            if (!$file) {
4803
                // bad reference - try to prevent future retries as hard as possible!
4804
                if ($user = $DB->get_record('user', array('id'=>$context->instanceid), 'id, picture')) {
4805
                    if ($user->picture > 0) {
4806
                        $DB->set_field('user', 'picture', 0, array('id'=>$user->id));
4807
                    }
4808
                }
4809
                // no redirect here because it is not cached
4810
                $theme = theme_config::load($themename);
4811
                $imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle', null);
4812
                send_file($imagefile, basename($imagefile), 60*60*24*14);
4813
            }
4814
 
4815
            $options = $sendfileoptions;
4816
            if (empty($CFG->forcelogin) && empty($CFG->forceloginforprofileimage)) {
4817
                // Profile images should be cache-able by both browsers and proxies according
4818
                // to $CFG->forcelogin and $CFG->forceloginforprofileimage.
4819
                $options['cacheability'] = 'public';
4820
            }
4821
            send_stored_file($file, 60*60*24*365, 0, false, $options); // enable long caching, there are many images on each page
4822
 
4823
        } else if ($filearea === 'private' and $context->contextlevel == CONTEXT_USER) {
4824
            require_login();
4825
 
4826
            if (isguestuser()) {
4827
                send_file_not_found();
4828
            }
4829
 
4830
            if ($USER->id !== $context->instanceid) {
4831
                send_file_not_found();
4832
            }
4833
 
4834
            $filename = array_pop($args);
4835
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4836
            if (!$file = $fs->get_file($context->id, $component, $filearea, 0, $filepath, $filename) or $file->is_directory()) {
4837
                send_file_not_found();
4838
            }
4839
 
4840
            \core\session\manager::write_close(); // Unlock session during file serving.
4841
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4842
 
4843
        } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_USER) {
4844
 
4845
            if ($CFG->forcelogin) {
4846
                require_login();
4847
            }
4848
 
4849
            $userid = $context->instanceid;
4850
 
4851
            if (!empty($CFG->forceloginforprofiles)) {
4852
                require_once("{$CFG->dirroot}/user/lib.php");
4853
 
4854
                require_login();
4855
 
4856
                // Verify the current user is able to view the profile of the supplied user anywhere.
4857
                $user = core_user::get_user($userid);
4858
                if (!user_can_view_profile($user, null, $context)) {
4859
                    send_file_not_found();
4860
                }
4861
            }
4862
 
4863
            $filename = array_pop($args);
4864
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4865
            if (!$file = $fs->get_file($context->id, $component, $filearea, 0, $filepath, $filename) or $file->is_directory()) {
4866
                send_file_not_found();
4867
            }
4868
 
4869
            \core\session\manager::write_close(); // Unlock session during file serving.
4870
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4871
 
4872
        } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_COURSE) {
4873
            $userid = (int)array_shift($args);
4874
            $usercontext = context_user::instance($userid);
4875
 
4876
            if ($CFG->forcelogin) {
4877
                require_login();
4878
            }
4879
 
4880
            if (!empty($CFG->forceloginforprofiles)) {
4881
                require_once("{$CFG->dirroot}/user/lib.php");
4882
 
4883
                require_login();
4884
 
4885
                // Verify the current user is able to view the profile of the supplied user in current course.
4886
                $user = core_user::get_user($userid);
4887
                if (!user_can_view_profile($user, $course, $usercontext)) {
4888
                    send_file_not_found();
4889
                }
4890
            }
4891
 
4892
            $filename = array_pop($args);
4893
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4894
            if (!$file = $fs->get_file($usercontext->id, 'user', 'profile', 0, $filepath, $filename) or $file->is_directory()) {
4895
                send_file_not_found();
4896
            }
4897
 
4898
            \core\session\manager::write_close(); // Unlock session during file serving.
4899
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4900
 
4901
        } else if ($filearea === 'backup' and $context->contextlevel == CONTEXT_USER) {
4902
            require_login();
4903
 
4904
            if (isguestuser()) {
4905
                send_file_not_found();
4906
            }
4907
            $userid = $context->instanceid;
4908
 
4909
            if ($USER->id != $userid) {
4910
                send_file_not_found();
4911
            }
4912
 
4913
            $filename = array_pop($args);
4914
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4915
            if (!$file = $fs->get_file($context->id, 'user', 'backup', 0, $filepath, $filename) or $file->is_directory()) {
4916
                send_file_not_found();
4917
            }
4918
 
4919
            \core\session\manager::write_close(); // Unlock session during file serving.
4920
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4921
 
4922
        } else {
4923
            send_file_not_found();
4924
        }
4925
 
4926
    // ========================================================================================================================
4927
    } else if ($component === 'coursecat') {
4928
        if ($context->contextlevel != CONTEXT_COURSECAT) {
4929
            send_file_not_found();
4930
        }
4931
 
4932
        if ($filearea === 'description') {
4933
            if ($CFG->forcelogin) {
4934
                // no login necessary - unless login forced everywhere
4935
                require_login();
4936
            }
4937
 
4938
            // Check if user can view this category.
4939
            if (!core_course_category::get($context->instanceid, IGNORE_MISSING)) {
4940
                send_file_not_found();
4941
            }
4942
 
4943
            $filename = array_pop($args);
4944
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4945
            if (!$file = $fs->get_file($context->id, 'coursecat', 'description', 0, $filepath, $filename) or $file->is_directory()) {
4946
                send_file_not_found();
4947
            }
4948
 
4949
            \core\session\manager::write_close(); // Unlock session during file serving.
4950
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4951
        } else {
4952
            send_file_not_found();
4953
        }
4954
 
4955
    // ========================================================================================================================
4956
    } else if ($component === 'course') {
4957
        if ($context->contextlevel != CONTEXT_COURSE) {
4958
            send_file_not_found();
4959
        }
4960
 
4961
        if ($filearea === 'summary' || $filearea === 'overviewfiles') {
4962
            if ($CFG->forcelogin) {
4963
                require_login();
4964
            }
4965
 
4966
            $filename = array_pop($args);
4967
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4968
            if (!$file = $fs->get_file($context->id, 'course', $filearea, 0, $filepath, $filename) or $file->is_directory()) {
4969
                send_file_not_found();
4970
            }
4971
 
4972
            \core\session\manager::write_close(); // Unlock session during file serving.
4973
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4974
 
4975
        } else if ($filearea === 'section') {
4976
            if ($CFG->forcelogin) {
4977
                require_login($course);
4978
            } else if ($course->id != SITEID) {
4979
                require_login($course);
4980
            }
4981
 
4982
            $sectionid = (int)array_shift($args);
4983
 
4984
            if (!$section = $DB->get_record('course_sections', array('id'=>$sectionid, 'course'=>$course->id))) {
4985
                send_file_not_found();
4986
            }
4987
 
4988
            $filename = array_pop($args);
4989
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4990
            if (!$file = $fs->get_file($context->id, 'course', 'section', $sectionid, $filepath, $filename) or $file->is_directory()) {
4991
                send_file_not_found();
4992
            }
4993
 
4994
            \core\session\manager::write_close(); // Unlock session during file serving.
4995
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4996
 
4997
        } else if ($filearea === 'generated') {
4998
            if ($CFG->forcelogin) {
4999
                require_login($course);
5000
            } else if ($course->id != SITEID) {
5001
                require_login($course);
5002
            }
5003
 
5004
            $svg = $OUTPUT->get_generated_svg_for_id($course->id);
5005
 
5006
            \core\session\manager::write_close(); // Unlock session during file serving.
5007
            send_file($svg, 'course.svg', 60 * 60, 0, true, $forcedownload);
5008
 
5009
        } else {
5010
            send_file_not_found();
5011
        }
5012
 
5013
    } else if ($component === 'cohort') {
5014
 
5015
        $cohortid = (int)array_shift($args);
5016
        $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
5017
        $cohortcontext = context::instance_by_id($cohort->contextid);
5018
 
5019
        // The context in the file URL must be either cohort context or context of the course underneath the cohort's context.
5020
        if ($context->id != $cohort->contextid &&
5021
            ($context->contextlevel != CONTEXT_COURSE || !in_array($cohort->contextid, $context->get_parent_context_ids()))) {
5022
            send_file_not_found();
5023
        }
5024
 
5025
        // User is able to access cohort if they have view cap on cohort level or
5026
        // the cohort is visible and they have view cap on course level.
5027
        $canview = has_capability('moodle/cohort:view', $cohortcontext) ||
5028
                ($cohort->visible && has_capability('moodle/cohort:view', $context));
5029
 
5030
        if ($filearea === 'description' && $canview) {
5031
            $filename = array_pop($args);
5032
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5033
            if (($file = $fs->get_file($cohortcontext->id, 'cohort', 'description', $cohort->id, $filepath, $filename))
5034
                    && !$file->is_directory()) {
5035
                \core\session\manager::write_close(); // Unlock session during file serving.
5036
                send_stored_file($file, 60 * 60, 0, $forcedownload, $sendfileoptions);
5037
            }
5038
        }
5039
 
5040
        send_file_not_found();
5041
 
5042
    } else if ($component === 'group') {
5043
        if ($context->contextlevel != CONTEXT_COURSE) {
5044
            send_file_not_found();
5045
        }
5046
 
5047
        require_course_login($course, true, null, false);
5048
 
5049
        $groupid = (int)array_shift($args);
5050
 
5051
        $group = $DB->get_record('groups', array('id'=>$groupid, 'courseid'=>$course->id), '*', MUST_EXIST);
5052
        if (($course->groupmodeforce and $course->groupmode == SEPARATEGROUPS) and !has_capability('moodle/site:accessallgroups', $context) and !groups_is_member($group->id, $USER->id)) {
5053
            // do not allow access to separate group info if not member or teacher
5054
            send_file_not_found();
5055
        }
5056
 
5057
        if ($filearea === 'description') {
5058
 
5059
            require_login($course);
5060
 
5061
            $filename = array_pop($args);
5062
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5063
            if (!$file = $fs->get_file($context->id, 'group', 'description', $group->id, $filepath, $filename) or $file->is_directory()) {
5064
                send_file_not_found();
5065
            }
5066
 
5067
            \core\session\manager::write_close(); // Unlock session during file serving.
5068
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5069
 
5070
        } else if ($filearea === 'icon') {
5071
            $filename = array_pop($args);
5072
 
5073
            if ($filename !== 'f1' and $filename !== 'f2') {
5074
                send_file_not_found();
5075
            }
5076
            if (!$file = $fs->get_file($context->id, 'group', 'icon', $group->id, '/', $filename.'.png')) {
5077
                if (!$file = $fs->get_file($context->id, 'group', 'icon', $group->id, '/', $filename.'.jpg')) {
5078
                    send_file_not_found();
5079
                }
5080
            }
5081
 
5082
            \core\session\manager::write_close(); // Unlock session during file serving.
5083
            send_stored_file($file, 60*60, 0, false, $sendfileoptions);
5084
 
5085
        } else if ($filearea === 'generated') {
5086
            if ($CFG->forcelogin) {
5087
                require_login($course);
5088
            } else if ($course->id != SITEID) {
5089
                require_login($course);
5090
            }
5091
 
5092
            $svg = $OUTPUT->get_generated_svg_for_id($group->id);
5093
 
5094
            \core\session\manager::write_close(); // Unlock session during file serving.
5095
            send_file($svg, 'group.svg', 60 * 60, 0, true, $forcedownload);
5096
 
5097
        } else {
5098
            send_file_not_found();
5099
        }
5100
 
5101
    } else if ($component === 'grouping') {
5102
        if ($context->contextlevel != CONTEXT_COURSE) {
5103
            send_file_not_found();
5104
        }
5105
 
5106
        require_login($course);
5107
 
5108
        $groupingid = (int)array_shift($args);
5109
 
5110
        // note: everybody has access to grouping desc images for now
5111
        if ($filearea === 'description') {
5112
 
5113
            $filename = array_pop($args);
5114
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5115
            if (!$file = $fs->get_file($context->id, 'grouping', 'description', $groupingid, $filepath, $filename) or $file->is_directory()) {
5116
                send_file_not_found();
5117
            }
5118
 
5119
            \core\session\manager::write_close(); // Unlock session during file serving.
5120
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5121
 
5122
        } else {
5123
            send_file_not_found();
5124
        }
5125
 
5126
    // ========================================================================================================================
5127
    } else if ($component === 'backup') {
5128
        if ($filearea === 'course' and $context->contextlevel == CONTEXT_COURSE) {
5129
            require_login($course);
5130
            require_capability('moodle/backup:downloadfile', $context);
5131
 
5132
            $filename = array_pop($args);
5133
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5134
            if (!$file = $fs->get_file($context->id, 'backup', 'course', 0, $filepath, $filename) or $file->is_directory()) {
5135
                send_file_not_found();
5136
            }
5137
 
5138
            \core\session\manager::write_close(); // Unlock session during file serving.
5139
            send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions);
5140
 
5141
        } else if ($filearea === 'section' and $context->contextlevel == CONTEXT_COURSE) {
5142
            require_login($course);
5143
            require_capability('moodle/backup:downloadfile', $context);
5144
 
5145
            $sectionid = (int)array_shift($args);
5146
 
5147
            $filename = array_pop($args);
5148
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5149
            if (!$file = $fs->get_file($context->id, 'backup', 'section', $sectionid, $filepath, $filename) or $file->is_directory()) {
5150
                send_file_not_found();
5151
            }
5152
 
5153
            \core\session\manager::write_close();
5154
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5155
 
5156
        } else if ($filearea === 'activity' and $context->contextlevel == CONTEXT_MODULE) {
5157
            require_login($course, false, $cm);
5158
            require_capability('moodle/backup:downloadfile', $context);
5159
 
5160
            $filename = array_pop($args);
5161
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5162
            if (!$file = $fs->get_file($context->id, 'backup', 'activity', 0, $filepath, $filename) or $file->is_directory()) {
5163
                send_file_not_found();
5164
            }
5165
 
5166
            \core\session\manager::write_close();
5167
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5168
 
5169
        } else if ($filearea === 'automated' and $context->contextlevel == CONTEXT_COURSE) {
5170
            // Backup files that were generated by the automated backup systems.
5171
 
5172
            require_login($course);
5173
            require_capability('moodle/backup:downloadfile', $context);
5174
            require_capability('moodle/restore:userinfo', $context);
5175
 
5176
            $filename = array_pop($args);
5177
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5178
            if (!$file = $fs->get_file($context->id, 'backup', 'automated', 0, $filepath, $filename) or $file->is_directory()) {
5179
                send_file_not_found();
5180
            }
5181
 
5182
            \core\session\manager::write_close(); // Unlock session during file serving.
5183
            send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions);
5184
 
5185
        } else {
5186
            send_file_not_found();
5187
        }
5188
 
5189
    // ========================================================================================================================
5190
    } else if ($component === 'question') {
5191
        require_once($CFG->libdir . '/questionlib.php');
5192
        question_pluginfile($course, $context, 'question', $filearea, $args, $forcedownload, $sendfileoptions);
5193
        send_file_not_found();
5194
 
5195
    // ========================================================================================================================
5196
    } else if ($component === 'grading') {
5197
        if ($filearea === 'description') {
5198
            // files embedded into the form definition description
5199
 
5200
            if ($context->contextlevel == CONTEXT_SYSTEM) {
5201
                require_login();
5202
 
5203
            } else if ($context->contextlevel >= CONTEXT_COURSE) {
5204
                require_login($course, false, $cm);
5205
 
5206
            } else {
5207
                send_file_not_found();
5208
            }
5209
 
5210
            $formid = (int)array_shift($args);
5211
 
5212
            $sql = "SELECT ga.id
5213
                FROM {grading_areas} ga
5214
                JOIN {grading_definitions} gd ON (gd.areaid = ga.id)
5215
                WHERE gd.id = ? AND ga.contextid = ?";
5216
            $areaid = $DB->get_field_sql($sql, array($formid, $context->id), IGNORE_MISSING);
5217
 
5218
            if (!$areaid) {
5219
                send_file_not_found();
5220
            }
5221
 
5222
            $fullpath = "/$context->id/$component/$filearea/$formid/".implode('/', $args);
5223
 
5224
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
5225
                send_file_not_found();
5226
            }
5227
 
5228
            \core\session\manager::write_close(); // Unlock session during file serving.
5229
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5230
        }
5231
    } else if ($component === 'contentbank') {
5232
        if ($filearea != 'public' || isguestuser()) {
5233
            send_file_not_found();
5234
        }
5235
 
5236
        if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
5237
            require_login();
5238
        } else if ($context->contextlevel == CONTEXT_COURSE) {
5239
            require_login($course);
5240
        } else {
5241
            send_file_not_found();
5242
        }
5243
 
5244
        $componentargs = fullclone($args);
5245
        $itemid = (int)array_shift($args);
5246
        $filename = array_pop($args);
5247
        $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5248
 
5249
        \core\session\manager::write_close(); // Unlock session during file serving.
5250
 
5251
        $contenttype = $DB->get_field('contentbank_content', 'contenttype', ['id' => $itemid]);
5252
        if (component_class_callback("\\{$contenttype}\\contenttype", 'pluginfile',
5253
                [$course, null, $context, $filearea, $componentargs, $forcedownload, $sendfileoptions], false) === false) {
5254
 
5255
            if (!$file = $fs->get_file($context->id, $component, $filearea, $itemid, $filepath, $filename) or
5256
 
5257
                $file->is_directory()) {
5258
                send_file_not_found();
5259
 
5260
            } else {
5261
                send_stored_file($file, 0, 0, true, $sendfileoptions); // Must force download - security!
5262
            }
5263
        }
5264
    } else if (strpos($component, 'mod_') === 0) {
5265
        $modname = substr($component, 4);
5266
        if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
5267
            send_file_not_found();
5268
        }
5269
        require_once("$CFG->dirroot/mod/$modname/lib.php");
5270
 
5271
        if ($context->contextlevel == CONTEXT_MODULE) {
5272
            if ($cm->modname !== $modname) {
5273
                // somebody tries to gain illegal access, cm type must match the component!
5274
                send_file_not_found();
5275
            }
5276
        }
5277
 
5278
        if ($filearea === 'intro') {
5279
            if (!plugin_supports('mod', $modname, FEATURE_MOD_INTRO, true)) {
5280
                send_file_not_found();
5281
            }
5282
 
5283
            // Require login to the course first (without login to the module).
5284
            require_course_login($course, true);
5285
 
5286
            // Now check if module is available OR it is restricted but the intro is shown on the course page.
5287
            $cminfo = cm_info::create($cm);
5288
            if (!$cminfo->uservisible) {
5289
                if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
5290
                    // Module intro is not visible on the course page and module is not available, show access error.
5291
                    require_course_login($course, true, $cminfo);
5292
                }
5293
            }
5294
 
5295
            // all users may access it
5296
            $filename = array_pop($args);
5297
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5298
            if (!$file = $fs->get_file($context->id, 'mod_'.$modname, 'intro', 0, $filepath, $filename) or $file->is_directory()) {
5299
                send_file_not_found();
5300
            }
5301
 
5302
            // finally send the file
5303
            send_stored_file($file, null, 0, false, $sendfileoptions);
5304
        }
5305
 
5306
        $filefunction = $component.'_pluginfile';
5307
        $filefunctionold = $modname.'_pluginfile';
5308
        if (function_exists($filefunction)) {
5309
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5310
            $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5311
        } else if (function_exists($filefunctionold)) {
5312
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5313
            $filefunctionold($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5314
        }
5315
 
5316
        send_file_not_found();
5317
 
5318
    // ========================================================================================================================
5319
    } else if (strpos($component, 'block_') === 0) {
5320
        $blockname = substr($component, 6);
5321
        // note: no more class methods in blocks please, that is ....
5322
        if (!file_exists("$CFG->dirroot/blocks/$blockname/lib.php")) {
5323
            send_file_not_found();
5324
        }
5325
        require_once("$CFG->dirroot/blocks/$blockname/lib.php");
5326
 
5327
        if ($context->contextlevel == CONTEXT_BLOCK) {
5328
            $birecord = $DB->get_record('block_instances', array('id'=>$context->instanceid), '*',MUST_EXIST);
5329
            if ($birecord->blockname !== $blockname) {
5330
                // somebody tries to gain illegal access, cm type must match the component!
5331
                send_file_not_found();
5332
            }
5333
 
5334
            if ($context->get_course_context(false)) {
5335
                // If block is in course context, then check if user has capability to access course.
5336
                require_course_login($course);
5337
            } else if ($CFG->forcelogin) {
5338
                // If user is logged out, bp record will not be visible, even if the user would have access if logged in.
5339
                require_login();
5340
            }
5341
 
5342
            $bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
5343
            // User can't access file, if block is hidden or doesn't have block:view capability
5344
            if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
5345
                 send_file_not_found();
5346
            }
5347
        } else {
5348
            $birecord = null;
5349
        }
5350
 
5351
        $filefunction = $component.'_pluginfile';
5352
        if (function_exists($filefunction)) {
5353
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5354
            $filefunction($course, $birecord, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5355
        }
5356
 
5357
        send_file_not_found();
5358
 
5359
    // ========================================================================================================================
5360
    } else if (strpos($component, '_') === false) {
5361
        // all core subsystems have to be specified above, no more guessing here!
5362
        send_file_not_found();
5363
 
5364
    } else {
5365
        // try to serve general plugin file in arbitrary context
5366
        $dir = core_component::get_component_directory($component);
5367
        if (!file_exists("$dir/lib.php")) {
5368
            send_file_not_found();
5369
        }
5370
        include_once("$dir/lib.php");
5371
 
5372
        $filefunction = $component.'_pluginfile';
5373
        if (function_exists($filefunction)) {
5374
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5375
            $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5376
        }
5377
 
5378
        send_file_not_found();
5379
    }
5380
 
5381
}