Proyectos de Subversion Moodle

Rev

Rev 1 | | Comparar con el anterior | Ultima modificación | Ver Log |

Rev Autor Línea Nro. Línea
1 efrain 1
<?php
2
// This file is part of Moodle - http://moodle.org/
3
//
4
// Moodle is free software: you can redistribute it and/or modify
5
// it under the terms of the GNU General Public License as published by
6
// the Free Software Foundation, either version 3 of the License, or
7
// (at your option) any later version.
8
//
9
// Moodle is distributed in the hope that it will be useful,
10
// but WITHOUT ANY WARRANTY; without even the implied warranty of
11
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
12
// GNU General Public License for more details.
13
//
14
// You should have received a copy of the GNU General Public License
15
// along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
16
 
17
/**
18
 * 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
 */
408
function file_prepare_draft_area(&$draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null) {
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
 */
492
function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $filearea, $itemid, array $options=null) {
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) {
858
            if ($draftfile->type == 'file') {
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
 */
1107
function file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null, $forcehttps=false) {
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
/**
1317
 * Convert the draft file area URLs in some content to @@PLUGINFILE@@ tokens
1318
 * ready to be saved in the database. Normally, this is done automatically by
1319
 * {@link file_save_draft_area_files()}.
1320
 *
1321
 * @category files
1322
 * @param string $text the content to process.
1323
 * @param int $draftitemid the draft file area the content was using.
1324
 * @param bool $forcehttps whether the content contains https URLs. Default false.
1325
 * @return string the processed content.
1326
 */
1327
function file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps = false) {
1328
    global $CFG, $USER;
1329
 
1330
    $usercontext = context_user::instance($USER->id);
1331
 
1332
    $wwwroot = $CFG->wwwroot;
1333
    if ($forcehttps) {
1334
        $wwwroot = str_replace('http://', 'https://', $wwwroot);
1335
    }
1336
 
1337
    // relink embedded files if text submitted - no absolute links allowed in database!
1338
    $text = str_ireplace("$wwwroot/draftfile.php/$usercontext->id/user/draft/$draftitemid/", '@@PLUGINFILE@@/', $text);
1339
 
1340
    if (strpos($text, 'draftfile.php?file=') !== false) {
1341
        $matches = array();
1342
        preg_match_all("!$wwwroot/draftfile.php\?file=%2F{$usercontext->id}%2Fuser%2Fdraft%2F{$draftitemid}%2F[^'\",&<>|`\s:\\\\]+!iu", $text, $matches);
1343
        if ($matches) {
1344
            foreach ($matches[0] as $match) {
1345
                $replace = str_ireplace('%2F', '/', $match);
1346
                $text = str_replace($match, $replace, $text);
1347
            }
1348
        }
1349
        $text = str_ireplace("$wwwroot/draftfile.php?file=/$usercontext->id/user/draft/$draftitemid/", '@@PLUGINFILE@@/', $text);
1350
    }
1351
 
1352
    return $text;
1353
}
1354
 
1355
/**
1356
 * Set file sort order
1357
 *
1358
 * @global moodle_database $DB
1359
 * @param int $contextid the context id
1360
 * @param string $component file component
1361
 * @param string $filearea file area.
1362
 * @param int $itemid itemid.
1363
 * @param string $filepath file path.
1364
 * @param string $filename file name.
1365
 * @param int $sortorder the sort order of file.
1366
 * @return bool
1367
 */
1368
function file_set_sortorder($contextid, $component, $filearea, $itemid, $filepath, $filename, $sortorder) {
1369
    global $DB;
1370
    $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename);
1371
    if ($file_record = $DB->get_record('files', $conditions)) {
1372
        $sortorder = (int)$sortorder;
1373
        $file_record->sortorder = $sortorder;
1374
        $DB->update_record('files', $file_record);
1375
        return true;
1376
    }
1377
    return false;
1378
}
1379
 
1380
/**
1381
 * reset file sort order number to 0
1382
 * @global moodle_database $DB
1383
 * @param int $contextid the context id
1384
 * @param string $component
1385
 * @param string $filearea file area.
1386
 * @param int|bool $itemid itemid.
1387
 * @return bool
1388
 */
1389
function file_reset_sortorder($contextid, $component, $filearea, $itemid=false) {
1390
    global $DB;
1391
 
1392
    $conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);
1393
    if ($itemid !== false) {
1394
        $conditions['itemid'] = $itemid;
1395
    }
1396
 
1397
    $file_records = $DB->get_records('files', $conditions);
1398
    foreach ($file_records as $file_record) {
1399
        $file_record->sortorder = 0;
1400
        $DB->update_record('files', $file_record);
1401
    }
1402
    return true;
1403
}
1404
 
1405
/**
1406
 * Returns description of upload error
1407
 *
1408
 * @param int $errorcode found in $_FILES['filename.ext']['error']
1409
 * @return string error description string, '' if ok
1410
 */
1411
function file_get_upload_error($errorcode) {
1412
 
1413
    switch ($errorcode) {
1414
    case 0: // UPLOAD_ERR_OK - no error
1415
        $errmessage = '';
1416
        break;
1417
 
1418
    case 1: // UPLOAD_ERR_INI_SIZE
1419
        $errmessage = get_string('uploadserverlimit');
1420
        break;
1421
 
1422
    case 2: // UPLOAD_ERR_FORM_SIZE
1423
        $errmessage = get_string('uploadformlimit');
1424
        break;
1425
 
1426
    case 3: // UPLOAD_ERR_PARTIAL
1427
        $errmessage = get_string('uploadpartialfile');
1428
        break;
1429
 
1430
    case 4: // UPLOAD_ERR_NO_FILE
1431
        $errmessage = get_string('uploadnofilefound');
1432
        break;
1433
 
1434
    // Note: there is no error with a value of 5
1435
 
1436
    case 6: // UPLOAD_ERR_NO_TMP_DIR
1437
        $errmessage = get_string('uploadnotempdir');
1438
        break;
1439
 
1440
    case 7: // UPLOAD_ERR_CANT_WRITE
1441
        $errmessage = get_string('uploadcantwrite');
1442
        break;
1443
 
1444
    case 8: // UPLOAD_ERR_EXTENSION
1445
        $errmessage = get_string('uploadextension');
1446
        break;
1447
 
1448
    default:
1449
        $errmessage = get_string('uploadproblem');
1450
    }
1451
 
1452
    return $errmessage;
1453
}
1454
 
1455
/**
1456
 * Recursive function formating an array in POST parameter
1457
 * @param array $arraydata - the array that we are going to format and add into &$data array
1458
 * @param string $currentdata - a row of the final postdata array at instant T
1459
 *                when finish, it's assign to $data under this format: name[keyname][][]...[]='value'
1460
 * @param array $data - the final data array containing all POST parameters : 1 row = 1 parameter
1461
 */
1462
function format_array_postdata_for_curlcall($arraydata, $currentdata, &$data) {
1463
        foreach ($arraydata as $k=>$v) {
1464
            $newcurrentdata = $currentdata;
1465
            if (is_array($v)) { //the value is an array, call the function recursively
1466
                $newcurrentdata = $newcurrentdata.'['.urlencode($k).']';
1467
                format_array_postdata_for_curlcall($v, $newcurrentdata, $data);
1468
            }  else { //add the POST parameter to the $data array
1469
                $data[] = $newcurrentdata.'['.urlencode($k).']='.urlencode($v);
1470
            }
1471
        }
1472
}
1473
 
1474
/**
1475
 * Transform a PHP array into POST parameter
1476
 * (see the recursive function format_array_postdata_for_curlcall)
1477
 * @param array $postdata
1478
 * @return string containing all POST parameters  (1 row = 1 POST parameter)
1479
 */
1480
function format_postdata_for_curlcall($postdata) {
1481
        $data = array();
1482
        foreach ($postdata as $k=>$v) {
1483
            if (is_array($v)) {
1484
                $currentdata = urlencode($k);
1485
                format_array_postdata_for_curlcall($v, $currentdata, $data);
1486
            }  else {
1487
                $data[] = urlencode($k).'='.urlencode($v ?? '');
1488
            }
1489
        }
1490
        $convertedpostdata = implode('&', $data);
1491
        return $convertedpostdata;
1492
}
1493
 
1494
/**
1495
 * Fetches content of file from Internet (using proxy if defined). Uses cURL extension if present.
1496
 * Due to security concerns only downloads from http(s) sources are supported.
1497
 *
1498
 * @category files
1499
 * @param string $url file url starting with http(s)://
1500
 * @param array $headers http headers, null if none. If set, should be an
1501
 *   associative array of header name => value pairs.
1502
 * @param array $postdata array means use POST request with given parameters
1503
 * @param bool $fullresponse return headers, responses, etc in a similar way snoopy does
1504
 *   (if false, just returns content)
1505
 * @param int $timeout timeout for complete download process including all file transfer
1506
 *   (default 5 minutes)
1507
 * @param int $connecttimeout timeout for connection to server; this is the timeout that
1508
 *   usually happens if the remote server is completely down (default 20 seconds);
1509
 *   may not work when using proxy
1510
 * @param bool $skipcertverify If true, the peer's SSL certificate will not be checked.
1511
 *   Only use this when already in a trusted location.
1512
 * @param string $tofile store the downloaded content to file instead of returning it.
1513
 * @param bool $calctimeout false by default, true enables an extra head request to try and determine
1514
 *   filesize and appropriately larger timeout based on $CFG->curltimeoutkbitrate
1515
 * @return stdClass|string|bool stdClass object if $fullresponse is true, false if request failed, true
1516
 *   if file downloaded into $tofile successfully or the file content as a string.
1517
 */
1518
function download_file_content($url, $headers=null, $postdata=null, $fullresponse=false, $timeout=300, $connecttimeout=20, $skipcertverify=false, $tofile=NULL, $calctimeout=false) {
1519
    global $CFG;
1520
 
1521
    // Only http and https links supported.
1522
    if (!preg_match('|^https?://|i', $url)) {
1523
        if ($fullresponse) {
1524
            $response = new stdClass();
1525
            $response->status        = 0;
1526
            $response->headers       = array();
1527
            $response->response_code = 'Invalid protocol specified in url';
1528
            $response->results       = '';
1529
            $response->error         = 'Invalid protocol specified in url';
1530
            return $response;
1531
        } else {
1532
            return false;
1533
        }
1534
    }
1535
 
1536
    $options = array();
1537
 
1538
    $headers2 = array();
1539
    if (is_array($headers)) {
1540
        foreach ($headers as $key => $value) {
1541
            if (is_numeric($key)) {
1542
                $headers2[] = $value;
1543
            } else {
1544
                $headers2[] = "$key: $value";
1545
            }
1546
        }
1547
    }
1548
 
1549
    if ($skipcertverify) {
1550
        $options['CURLOPT_SSL_VERIFYPEER'] = false;
1551
    } else {
1552
        $options['CURLOPT_SSL_VERIFYPEER'] = true;
1553
    }
1554
 
1555
    $options['CURLOPT_CONNECTTIMEOUT'] = $connecttimeout;
1556
 
1557
    $options['CURLOPT_FOLLOWLOCATION'] = 1;
1558
    $options['CURLOPT_MAXREDIRS'] = 5;
1559
 
1560
    // Use POST if requested.
1561
    if (is_array($postdata)) {
1562
        $postdata = format_postdata_for_curlcall($postdata);
1563
    } else if (empty($postdata)) {
1564
        $postdata = null;
1565
    }
1566
 
1567
    // Optionally attempt to get more correct timeout by fetching the file size.
1568
    if (!isset($CFG->curltimeoutkbitrate)) {
1569
        // Use very slow rate of 56kbps as a timeout speed when not set.
1570
        $bitrate = 56;
1571
    } else {
1572
        $bitrate = $CFG->curltimeoutkbitrate;
1573
    }
1574
    if ($calctimeout and !isset($postdata)) {
1575
        $curl = new curl();
1576
        $curl->setHeader($headers2);
1577
 
1578
        $curl->head($url, $postdata, $options);
1579
 
1580
        $info = $curl->get_info();
1581
        $error_no = $curl->get_errno();
1582
        if (!$error_no && $info['download_content_length'] > 0) {
1583
            // No curl errors - adjust for large files only - take max timeout.
1584
            $timeout = max($timeout, ceil($info['download_content_length'] * 8 / ($bitrate * 1024)));
1585
        }
1586
    }
1587
 
1588
    $curl = new curl();
1589
    $curl->setHeader($headers2);
1590
 
1591
    $options['CURLOPT_RETURNTRANSFER'] = true;
1592
    $options['CURLOPT_NOBODY'] = false;
1593
    $options['CURLOPT_TIMEOUT'] = $timeout;
1594
 
1595
    if ($tofile) {
1596
        $fh = fopen($tofile, 'w');
1597
        if (!$fh) {
1598
            if ($fullresponse) {
1599
                $response = new stdClass();
1600
                $response->status        = 0;
1601
                $response->headers       = array();
1602
                $response->response_code = 'Can not write to file';
1603
                $response->results       = false;
1604
                $response->error         = 'Can not write to file';
1605
                return $response;
1606
            } else {
1607
                return false;
1608
            }
1609
        }
1610
        $options['CURLOPT_FILE'] = $fh;
1611
    }
1612
 
1613
    if (isset($postdata)) {
1614
        $content = $curl->post($url, $postdata, $options);
1615
    } else {
1616
        $content = $curl->get($url, null, $options);
1617
    }
1618
 
1619
    if ($tofile) {
1620
        fclose($fh);
1621
        @chmod($tofile, $CFG->filepermissions);
1622
    }
1623
 
1624
/*
1625
    // Try to detect encoding problems.
1626
    if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {
1627
        curl_setopt($ch, CURLOPT_ENCODING, 'none');
1628
        $result = curl_exec($ch);
1629
    }
1630
*/
1631
 
1632
    $info       = $curl->get_info();
1633
    $error_no   = $curl->get_errno();
1634
    $rawheaders = $curl->get_raw_response();
1635
 
1636
    if ($error_no) {
1637
        $error = $content;
1638
        if (!$fullresponse) {
1639
            debugging("cURL request for \"$url\" failed with: $error ($error_no)", DEBUG_ALL);
1640
            return false;
1641
        }
1642
 
1643
        $response = new stdClass();
1644
        if ($error_no == 28) {
1645
            $response->status    = '-100'; // Mimic snoopy.
1646
        } else {
1647
            $response->status    = '0';
1648
        }
1649
        $response->headers       = array();
1650
        $response->response_code = $error;
1651
        $response->results       = false;
1652
        $response->error         = $error;
1653
        return $response;
1654
    }
1655
 
1656
    if ($tofile) {
1657
        $content = true;
1658
    }
1659
 
1660
    if (empty($info['http_code'])) {
1661
        // For security reasons we support only true http connections (Location: file:// exploit prevention).
1662
        $response = new stdClass();
1663
        $response->status        = '0';
1664
        $response->headers       = array();
1665
        $response->response_code = 'Unknown cURL error';
1666
        $response->results       = false; // do NOT change this, we really want to ignore the result!
1667
        $response->error         = 'Unknown cURL error';
1668
 
1669
    } else {
1670
        $response = new stdClass();
1671
        $response->status        = (string)$info['http_code'];
1672
        $response->headers       = $rawheaders;
1673
        $response->results       = $content;
1674
        $response->error         = '';
1675
 
1676
        // There might be multiple headers on redirect, find the status of the last one.
1677
        $firstline = true;
1678
        foreach ($rawheaders as $line) {
1679
            if ($firstline) {
1680
                $response->response_code = $line;
1681
                $firstline = false;
1682
            }
1683
            if (trim($line, "\r\n") === '') {
1684
                $firstline = true;
1685
            }
1686
        }
1687
    }
1688
 
1689
    if ($fullresponse) {
1690
        return $response;
1691
    }
1692
 
1693
    if ($info['http_code'] != 200) {
1694
        debugging("cURL request for \"$url\" failed, HTTP response code: ".$response->response_code, DEBUG_ALL);
1695
        return false;
1696
    }
1697
    return $response->results;
1698
}
1699
 
1700
/**
1701
 * Returns a list of information about file types based on extensions.
1702
 *
1703
 * The following elements expected in value array for each extension:
1704
 * 'type' - mimetype
1705
 * 'icon' - location of the icon file. If value is FILENAME, then either pix/f/FILENAME.gif
1706
 *     or pix/f/FILENAME.png must be present in moodle and contain 16x16 filetype icon;
1707
 *     also files with bigger sizes under names
1708
 *     FILENAME-24, FILENAME-32, FILENAME-64, FILENAME-128, FILENAME-256 are recommended.
1709
 * 'groups' (optional) - array of filetype groups this filetype extension is part of;
1710
 *     commonly used in moodle the following groups:
1711
 *       - web_image - image that can be included as <img> in HTML
1712
 *       - image - image that we can parse using GD to find it's dimensions, also used for portfolio format
1713
 *       - optimised_image - image that will be processed and optimised
1714
 *       - video - file that can be imported as video in text editor
1715
 *       - audio - file that can be imported as audio in text editor
1716
 *       - archive - we can extract files from this archive
1717
 *       - spreadsheet - used for portfolio format
1718
 *       - document - used for portfolio format
1719
 *       - presentation - used for portfolio format
1720
 * 'string' (optional) - the name of the string from lang/en/mimetypes.php that displays
1721
 *     human-readable description for this filetype;
1722
 *     Function {@link get_mimetype_description()} first looks at the presence of string for
1723
 *     particular mimetype (value of 'type'), if not found looks for string specified in 'string'
1724
 *     attribute, if not found returns the value of 'type';
1725
 * 'defaulticon' (boolean, optional) - used by function {@link file_mimetype_icon()} to find
1726
 *     an icon for mimetype. If an entry with 'defaulticon' is not found for a particular mimetype,
1727
 *     this function will return first found icon; Especially usefull for types such as 'text/plain'
1728
 *
1729
 * @category files
1730
 * @return array List of information about file types based on extensions.
1731
 *   Associative array of extension (lower-case) to associative array
1732
 *   from 'element name' to data. Current element names are 'type' and 'icon'.
1733
 *   Unknown types should use the 'xxx' entry which includes defaults.
1734
 */
1735
function &get_mimetypes_array() {
1736
    // Get types from the core_filetypes function, which includes caching.
1737
    return core_filetypes::get_types();
1738
}
1739
 
1740
/**
1741
 * Determine a file's MIME type based on the given filename using the function mimeinfo.
1742
 *
1743
 * This function retrieves a file's MIME type for a file that will be sent to the user.
1744
 * This should only be used for file-sending purposes just like in send_stored_file, send_file, and send_temp_file.
1745
 * Should the file's MIME type cannot be determined by mimeinfo, it will return 'application/octet-stream' as a default
1746
 * MIME type which should tell the browser "I don't know what type of file this is, so just download it.".
1747
 *
1748
 * @param string $filename The file's filename.
1749
 * @return string The file's MIME type or 'application/octet-stream' if it cannot be determined.
1750
 */
1751
function get_mimetype_for_sending($filename = '') {
1752
    // Guess the file's MIME type using mimeinfo.
1753
    $mimetype = mimeinfo('type', $filename);
1754
 
1755
    // Use octet-stream as fallback if MIME type cannot be determined by mimeinfo.
1756
    if (!$mimetype || $mimetype === 'document/unknown') {
1757
        $mimetype = 'application/octet-stream';
1758
    }
1759
 
1760
    return $mimetype;
1761
}
1762
 
1763
/**
1764
 * Obtains information about a filetype based on its extension. Will
1765
 * use a default if no information is present about that particular
1766
 * extension.
1767
 *
1768
 * @category files
1769
 * @param string $element Desired information (usually 'icon'
1770
 *   for icon filename or 'type' for MIME type. Can also be
1771
 *   'icon24', ...32, 48, 64, 72, 80, 96, 128, 256)
1772
 * @param string $filename Filename we're looking up
1773
 * @return string Requested piece of information from array
1774
 */
1775
function mimeinfo($element, $filename) {
1776
    global $CFG;
1777
    $mimeinfo = & get_mimetypes_array();
1778
    static $iconpostfixes = array(256=>'-256', 128=>'-128', 96=>'-96', 80=>'-80', 72=>'-72', 64=>'-64', 48=>'-48', 32=>'-32', 24=>'-24', 16=>'');
1779
 
1780
    $filetype = strtolower(pathinfo($filename ?? '', PATHINFO_EXTENSION));
1781
    if (empty($filetype)) {
1782
        $filetype = 'xxx'; // file without extension
1783
    }
1784
    if (preg_match('/^icon(\d*)$/', $element, $iconsizematch)) {
1785
        $iconsize = max(array(16, (int)$iconsizematch[1]));
1786
        $filenames = array($mimeinfo['xxx']['icon']);
1787
        if ($filetype != 'xxx' && isset($mimeinfo[$filetype]['icon'])) {
1788
            array_unshift($filenames, $mimeinfo[$filetype]['icon']);
1789
        }
1790
        // find the file with the closest size, first search for specific icon then for default icon
1791
        foreach ($filenames as $filename) {
1792
            foreach ($iconpostfixes as $size => $postfix) {
1793
                $fullname = $CFG->dirroot.'/pix/f/'.$filename.$postfix;
1794
                if ($iconsize >= $size &&
1795
                        (file_exists($fullname.'.svg') || file_exists($fullname.'.png') || file_exists($fullname.'.gif'))) {
1796
                    return $filename.$postfix;
1797
                }
1798
            }
1799
        }
1800
    } else if (isset($mimeinfo[$filetype][$element])) {
1801
        return $mimeinfo[$filetype][$element];
1802
    } else if (isset($mimeinfo['xxx'][$element])) {
1803
        return $mimeinfo['xxx'][$element];   // By default
1804
    } else {
1805
        return null;
1806
    }
1807
}
1808
 
1809
/**
1810
 * Obtains information about a filetype based on the MIME type rather than
1811
 * the other way around.
1812
 *
1813
 * @category files
1814
 * @param string $element Desired information ('extension', 'icon', etc.)
1815
 * @param string $mimetype MIME type we're looking up
1816
 * @return string Requested piece of information from array
1817
 */
1818
function mimeinfo_from_type($element, $mimetype) {
1819
    /* array of cached mimetype->extension associations */
1820
    static $cached = array();
1821
    $mimeinfo = & get_mimetypes_array();
1822
 
1823
    if (!array_key_exists($mimetype, $cached)) {
1824
        $cached[$mimetype] = null;
1825
        foreach($mimeinfo as $filetype => $values) {
1826
            if ($values['type'] == $mimetype) {
1827
                if ($cached[$mimetype] === null) {
1828
                    $cached[$mimetype] = '.'.$filetype;
1829
                }
1830
                if (!empty($values['defaulticon'])) {
1831
                    $cached[$mimetype] = '.'.$filetype;
1832
                    break;
1833
                }
1834
            }
1835
        }
1836
        if (empty($cached[$mimetype])) {
1837
            $cached[$mimetype] = '.xxx';
1838
        }
1839
    }
1840
    if ($element === 'extension') {
1841
        return $cached[$mimetype];
1842
    } else {
1843
        return mimeinfo($element, $cached[$mimetype]);
1844
    }
1845
}
1846
 
1847
/**
1848
 * Return the relative icon path for a given file
1849
 *
1850
 * Usage:
1851
 * <code>
1852
 * // $file - instance of stored_file or file_info
1853
 * $icon = $OUTPUT->image_url(file_file_icon($file))->out();
1854
 * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($file)));
1855
 * </code>
1856
 * or
1857
 * <code>
1858
 * echo $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file));
1859
 * </code>
1860
 *
1861
 * @param stored_file|file_info|stdClass|array $file (in case of object attributes $file->filename
1862
 *     and $file->mimetype are expected)
1863
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1864
 * @return string
1865
 */
1866
function file_file_icon($file, $unused = null) {
1867
    if ($unused !== null) {
1868
        debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
1869
    }
1870
 
1871
    if (!is_object($file)) {
1872
        $file = (object)$file;
1873
    }
1874
    if (isset($file->filename)) {
1875
        $filename = $file->filename;
1876
    } else if (method_exists($file, 'get_filename')) {
1877
        $filename = $file->get_filename();
1878
    } else if (method_exists($file, 'get_visible_name')) {
1879
        $filename = $file->get_visible_name();
1880
    } else {
1881
        $filename = '';
1882
    }
1883
    if (isset($file->mimetype)) {
1884
        $mimetype = $file->mimetype;
1885
    } else if (method_exists($file, 'get_mimetype')) {
1886
        $mimetype = $file->get_mimetype();
1887
    } else {
1888
        $mimetype = '';
1889
    }
1890
    $mimetypes = &get_mimetypes_array();
1891
    if ($filename) {
1892
        $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
1893
        if ($extension && !empty($mimetypes[$extension])) {
1894
            // if file name has known extension, return icon for this extension
1895
            return file_extension_icon($filename);
1896
        }
1897
    }
1898
    return file_mimetype_icon($mimetype);
1899
}
1900
 
1901
/**
1902
 * Return the relative icon path for a folder image.
1903
 *
1904
 * Usage:
1905
 * <code>
1906
 * $icon = $OUTPUT->image_url(file_folder_icon())->out();
1907
 * echo html_writer::empty_tag('img', array('src' => $icon));
1908
 * </code>
1909
 * or
1910
 * <code>
1911
 * echo $OUTPUT->pix_icon(file_folder_icon(), '');
1912
 * </code>
1913
 *
1914
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1915
 * @return string
1916
 */
1917
function file_folder_icon($unused = null) {
1918
    global $CFG;
1919
 
1920
    if ($unused !== null) {
1921
        debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
1922
    }
1923
 
1924
    return 'f/folder';
1925
}
1926
 
1927
/**
1928
 * Returns the relative icon path for a given mime type
1929
 *
1930
 * This function should be used in conjunction with $OUTPUT->image_url to produce
1931
 * a return the full path to an icon.
1932
 *
1933
 * <code>
1934
 * $mimetype = 'image/jpg';
1935
 * $icon = $OUTPUT->image_url(file_mimetype_icon($mimetype))->out();
1936
 * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($mimetype)));
1937
 * </code>
1938
 *
1939
 * @category files
1940
 * @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered
1941
 * to conform with that.
1942
 * @param string $mimetype The mimetype to fetch an icon for
1943
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1944
 * @return string The relative path to the icon
1945
 */
1946
function file_mimetype_icon($mimetype, $unused = null) {
1947
    return 'f/'.mimeinfo_from_type('icon', $mimetype);
1948
}
1949
 
1950
/**
1951
 * Returns the relative icon path for a given file name
1952
 *
1953
 * This function should be used in conjunction with $OUTPUT->image_url to produce
1954
 * a return the full path to an icon.
1955
 *
1956
 * <code>
1957
 * $filename = '.jpg';
1958
 * $icon = $OUTPUT->image_url(file_extension_icon($filename))->out();
1959
 * echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => '...'));
1960
 * </code>
1961
 *
1962
 * @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered
1963
 * to conform with that.
1964
 * @todo MDL-31074 Implement $size
1965
 * @category files
1966
 * @param string $filename The filename to get the icon for
1967
 * @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.
1968
 * @return string
1969
 */
1970
function file_extension_icon($filename, $unused = null) {
1971
    if ($unused !== null) {
1972
        debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);
1973
    }
1974
    return 'f/'.mimeinfo('icon', $filename);
1975
}
1976
 
1977
/**
1978
 * Obtains descriptions for file types (e.g. 'Microsoft Word document') from the
1979
 * mimetypes.php language file.
1980
 *
1981
 * @param mixed $obj - instance of stored_file or file_info or array/stdClass with field
1982
 *   'filename' and 'mimetype', or just a string with mimetype (though it is recommended to
1983
 *   have filename); In case of array/stdClass the field 'mimetype' is optional.
1984
 * @param bool $capitalise If true, capitalises first character of result
1985
 * @return string Text description
1986
 */
1987
function get_mimetype_description($obj, $capitalise=false) {
1988
    $filename = $mimetype = '';
1989
    if (is_object($obj) && method_exists($obj, 'get_filename') && method_exists($obj, 'get_mimetype')) {
1990
        // this is an instance of stored_file
1991
        $mimetype = $obj->get_mimetype();
1992
        $filename = $obj->get_filename();
1993
    } else if (is_object($obj) && method_exists($obj, 'get_visible_name') && method_exists($obj, 'get_mimetype')) {
1994
        // this is an instance of file_info
1995
        $mimetype = $obj->get_mimetype();
1996
        $filename = $obj->get_visible_name();
1997
    } else if (is_array($obj) || is_object ($obj)) {
1998
        $obj = (array)$obj;
1999
        if (!empty($obj['filename'])) {
2000
            $filename = $obj['filename'];
2001
        }
2002
        if (!empty($obj['mimetype'])) {
2003
            $mimetype = $obj['mimetype'];
2004
        }
2005
    } else {
2006
        $mimetype = $obj;
2007
    }
2008
    $mimetypefromext = mimeinfo('type', $filename);
2009
    if (empty($mimetype) || $mimetypefromext !== 'document/unknown') {
2010
        // if file has a known extension, overwrite the specified mimetype
2011
        $mimetype = $mimetypefromext;
2012
    }
2013
    $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
2014
    if (empty($extension)) {
2015
        $mimetypestr = mimeinfo_from_type('string', $mimetype);
2016
        $extension = str_replace('.', '', mimeinfo_from_type('extension', $mimetype));
2017
    } else {
2018
        $mimetypestr = mimeinfo('string', $filename);
2019
    }
2020
    $chunks = explode('/', $mimetype, 2);
2021
    $chunks[] = '';
2022
    $attr = array(
2023
        'mimetype' => $mimetype,
2024
        'ext' => $extension,
2025
        'mimetype1' => $chunks[0],
2026
        'mimetype2' => $chunks[1],
2027
    );
2028
    $a = array();
2029
    foreach ($attr as $key => $value) {
2030
        $a[$key] = $value;
2031
        $a[strtoupper($key)] = strtoupper($value);
2032
        $a[ucfirst($key)] = ucfirst($value);
2033
    }
2034
 
2035
    // MIME types may include + symbol but this is not permitted in string ids.
2036
    $safemimetype = str_replace('+', '_', $mimetype ?? '');
2037
    $safemimetypestr = str_replace('+', '_', $mimetypestr ?? '');
2038
    $customdescription = mimeinfo('customdescription', $filename);
2039
    if ($customdescription) {
2040
        // Call format_string on the custom description so that multilang
2041
        // filter can be used (if enabled on system context). We use system
2042
        // context because it is possible that the page context might not have
2043
        // been defined yet.
2044
        $result = format_string($customdescription, true,
2045
                array('context' => context_system::instance()));
2046
    } else if (get_string_manager()->string_exists($safemimetype, 'mimetypes')) {
2047
        $result = get_string($safemimetype, 'mimetypes', (object)$a);
2048
    } else if (get_string_manager()->string_exists($safemimetypestr, 'mimetypes')) {
2049
        $result = get_string($safemimetypestr, 'mimetypes', (object)$a);
2050
    } else if (get_string_manager()->string_exists('default', 'mimetypes')) {
2051
        $result = get_string('default', 'mimetypes', (object)$a);
2052
    } else {
2053
        $result = $mimetype;
2054
    }
2055
    if ($capitalise) {
2056
        $result=ucfirst($result);
2057
    }
2058
    return $result;
2059
}
2060
 
2061
/**
2062
 * Returns array of elements of type $element in type group(s)
2063
 *
2064
 * @param string $element name of the element we are interested in, usually 'type' or 'extension'
2065
 * @param string|array $groups one group or array of groups/extensions/mimetypes
2066
 * @return array
2067
 */
2068
function file_get_typegroup($element, $groups) {
2069
    static $cached = array();
2070
 
2071
    // Turn groups into a list.
2072
    if (!is_array($groups)) {
2073
        $groups = preg_split('/[\s,;:"\']+/', $groups, -1, PREG_SPLIT_NO_EMPTY);
2074
    }
2075
 
2076
    if (!array_key_exists($element, $cached)) {
2077
        $cached[$element] = array();
2078
    }
2079
    $result = array();
2080
    foreach ($groups as $group) {
2081
        if (!array_key_exists($group, $cached[$element])) {
2082
            // retrieive and cache all elements of type $element for group $group
2083
            $mimeinfo = & get_mimetypes_array();
2084
            $cached[$element][$group] = array();
2085
            foreach ($mimeinfo as $extension => $value) {
2086
                $value['extension'] = '.'.$extension;
2087
                if (empty($value[$element])) {
2088
                    continue;
2089
                }
2090
                if (($group === '.'.$extension || $group === $value['type'] ||
2091
                        (!empty($value['groups']) && in_array($group, $value['groups']))) &&
2092
                        !in_array($value[$element], $cached[$element][$group])) {
2093
                    $cached[$element][$group][] = $value[$element];
2094
                }
2095
            }
2096
        }
2097
        $result = array_merge($result, $cached[$element][$group]);
2098
    }
2099
    return array_values(array_unique($result));
2100
}
2101
 
2102
/**
2103
 * Checks if file with name $filename has one of the extensions in groups $groups
2104
 *
2105
 * @see get_mimetypes_array()
2106
 * @param string $filename name of the file to check
2107
 * @param string|array $groups one group or array of groups to check
2108
 * @param bool $checktype if true and extension check fails, find the mimetype and check if
2109
 * file mimetype is in mimetypes in groups $groups
2110
 * @return bool
2111
 */
2112
function file_extension_in_typegroup($filename, $groups, $checktype = false) {
2113
    $extension = pathinfo($filename, PATHINFO_EXTENSION);
2114
    if (!empty($extension) && in_array('.'.strtolower($extension), file_get_typegroup('extension', $groups))) {
2115
        return true;
2116
    }
2117
    return $checktype && file_mimetype_in_typegroup(mimeinfo('type', $filename), $groups);
2118
}
2119
 
2120
/**
2121
 * Checks if mimetype $mimetype belongs to one of the groups $groups
2122
 *
2123
 * @see get_mimetypes_array()
2124
 * @param string $mimetype
2125
 * @param string|array $groups one group or array of groups to check
2126
 * @return bool
2127
 */
2128
function file_mimetype_in_typegroup($mimetype, $groups) {
2129
    return !empty($mimetype) && in_array($mimetype, file_get_typegroup('type', $groups));
2130
}
2131
 
2132
/**
2133
 * Requested file is not found or not accessible, does not return, terminates script
2134
 *
2135
 * @global stdClass $CFG
2136
 * @global stdClass $COURSE
2137
 */
2138
function send_file_not_found() {
2139
    global $CFG, $COURSE;
2140
 
2141
    // Allow cross-origin requests only for Web Services.
2142
    // This allow to receive requests done by Web Workers or webapps in different domains.
2143
    if (WS_SERVER) {
2144
        header('Access-Control-Allow-Origin: *');
2145
    }
2146
 
2147
    send_header_404();
2148
    throw new \moodle_exception('filenotfound', 'error',
2149
        $CFG->wwwroot.'/course/view.php?id='.$COURSE->id); // This is not displayed on IIS?
2150
}
2151
/**
2152
 * Helper function to send correct 404 for server.
2153
 */
2154
function send_header_404() {
2155
    if (substr(php_sapi_name(), 0, 3) == 'cgi') {
2156
        header("Status: 404 Not Found");
2157
    } else {
2158
        header('HTTP/1.0 404 not found');
2159
    }
2160
}
2161
 
2162
/**
2163
 * The readfile function can fail when files are larger than 2GB (even on 64-bit
2164
 * platforms). This wrapper uses readfile for small files and custom code for
2165
 * large ones.
2166
 *
2167
 * @param string $path Path to file
2168
 * @param int $filesize Size of file (if left out, will get it automatically)
2169
 * @return int|bool Size read (will always be $filesize) or false if failed
2170
 */
2171
function readfile_allow_large($path, $filesize = -1) {
2172
    // Automatically get size if not specified.
2173
    if ($filesize === -1) {
2174
        $filesize = filesize($path);
2175
    }
2176
    if ($filesize <= 2147483647) {
2177
        // If the file is up to 2^31 - 1, send it normally using readfile.
2178
        return readfile($path);
2179
    } else {
2180
        // For large files, read and output in 64KB chunks.
2181
        $handle = fopen($path, 'r');
2182
        if ($handle === false) {
2183
            return false;
2184
        }
2185
        $left = $filesize;
2186
        while ($left > 0) {
2187
            $size = min($left, 65536);
2188
            $buffer = fread($handle, $size);
2189
            if ($buffer === false) {
2190
                return false;
2191
            }
2192
            echo $buffer;
2193
            $left -= $size;
2194
        }
2195
        return $filesize;
2196
    }
2197
}
2198
 
2199
/**
2200
 * Enhanced readfile() with optional acceleration.
2201
 * @param string|stored_file $file
2202
 * @param string $mimetype
2203
 * @param bool $accelerate
2204
 * @return void
2205
 */
2206
function readfile_accel($file, $mimetype, $accelerate) {
2207
    global $CFG;
2208
 
2209
    if ($mimetype === 'text/plain') {
2210
        // there is no encoding specified in text files, we need something consistent
2211
        header('Content-Type: text/plain; charset=utf-8');
2212
    } else {
2213
        header('Content-Type: '.$mimetype);
2214
    }
2215
 
2216
    $lastmodified = is_object($file) ? $file->get_timemodified() : filemtime($file);
2217
    header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
2218
 
2219
    if (is_object($file)) {
2220
        header('Etag: "' . $file->get_contenthash() . '"');
2221
        if (isset($_SERVER['HTTP_IF_NONE_MATCH']) and trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') === $file->get_contenthash()) {
2222
            header('HTTP/1.1 304 Not Modified');
2223
            return;
2224
        }
2225
    }
2226
 
2227
    // if etag present for stored file rely on it exclusively
2228
    if (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) and (empty($_SERVER['HTTP_IF_NONE_MATCH']) or !is_object($file))) {
2229
        // get unixtime of request header; clip extra junk off first
2230
        $since = strtotime(preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]));
2231
        if ($since && $since >= $lastmodified) {
2232
            header('HTTP/1.1 304 Not Modified');
2233
            return;
2234
        }
2235
    }
2236
 
2237
    if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
2238
        header('Accept-Ranges: bytes');
2239
    } else {
2240
        header('Accept-Ranges: none');
2241
    }
2242
 
2243
    if ($accelerate) {
2244
        if (is_object($file)) {
2245
            $fs = get_file_storage();
2246
            if ($fs->supports_xsendfile()) {
2247
                if ($fs->xsendfile_file($file)) {
2248
                    return;
2249
                }
2250
            }
2251
        } else {
2252
            if (!empty($CFG->xsendfile)) {
2253
                require_once("$CFG->libdir/xsendfilelib.php");
2254
                if (xsendfile($file)) {
2255
                    return;
2256
                }
2257
            }
2258
        }
2259
    }
2260
 
2261
    $filesize = is_object($file) ? $file->get_filesize() : filesize($file);
2262
 
2263
    header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
2264
 
2265
    if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {
2266
 
2267
        if (!empty($_SERVER['HTTP_RANGE']) and strpos($_SERVER['HTTP_RANGE'],'bytes=') !== FALSE) {
2268
            // byteserving stuff - for acrobat reader and download accelerators
2269
            // see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35
2270
            // inspired by: http://www.coneural.org/florian/papers/04_byteserving.php
2271
            $ranges = false;
2272
            if (preg_match_all('/(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $ranges, PREG_SET_ORDER)) {
2273
                foreach ($ranges as $key=>$value) {
2274
                    if ($ranges[$key][1] == '') {
2275
                        //suffix case
2276
                        $ranges[$key][1] = $filesize - $ranges[$key][2];
2277
                        $ranges[$key][2] = $filesize - 1;
2278
                    } else if ($ranges[$key][2] == '' || $ranges[$key][2] > $filesize - 1) {
2279
                        //fix range length
2280
                        $ranges[$key][2] = $filesize - 1;
2281
                    }
2282
                    if ($ranges[$key][2] != '' && $ranges[$key][2] < $ranges[$key][1]) {
2283
                        //invalid byte-range ==> ignore header
2284
                        $ranges = false;
2285
                        break;
2286
                    }
2287
                    //prepare multipart header
2288
                    $ranges[$key][0] =  "\r\n--".BYTESERVING_BOUNDARY."\r\nContent-Type: $mimetype\r\n";
2289
                    $ranges[$key][0] .= "Content-Range: bytes {$ranges[$key][1]}-{$ranges[$key][2]}/$filesize\r\n\r\n";
2290
                }
2291
            } else {
2292
                $ranges = false;
2293
            }
2294
            if ($ranges) {
2295
                if (is_object($file)) {
2296
                    $handle = $file->get_content_file_handle();
2297
                    if ($handle === false) {
2298
                        throw new file_exception('storedfilecannotreadfile', $file->get_filename());
2299
                    }
2300
                } else {
2301
                    $handle = fopen($file, 'rb');
2302
                    if ($handle === false) {
2303
                        throw new file_exception('cannotopenfile', $file);
2304
                    }
2305
                }
2306
                byteserving_send_file($handle, $mimetype, $ranges, $filesize);
2307
            }
2308
        }
2309
    }
2310
 
2311
    header('Content-Length: ' . $filesize);
2312
 
2313
    if (!empty($_SERVER['REQUEST_METHOD']) and $_SERVER['REQUEST_METHOD'] === 'HEAD') {
2314
        exit;
2315
    }
2316
 
2317
    while (ob_get_level()) {
2318
        $handlerstack = ob_list_handlers();
2319
        $activehandler = array_pop($handlerstack);
2320
        if ($activehandler === 'default output handler') {
2321
            // We do not expect any content in the buffer when we are serving files.
2322
            $buffercontents = ob_get_clean();
2323
            if ($buffercontents !== '') {
2324
                error_log('Non-empty default output handler buffer detected while serving the file ' . $file);
2325
            }
2326
        } else {
2327
            // Some handlers such as zlib output compression may have file signature buffered - flush it.
2328
            ob_end_flush();
2329
        }
2330
    }
2331
 
2332
    // send the whole file content
2333
    if (is_object($file)) {
2334
        $file->readfile();
2335
    } else {
2336
        if (readfile_allow_large($file, $filesize) === false) {
2337
            throw new file_exception('cannotopenfile', $file);
2338
        }
2339
    }
2340
}
2341
 
2342
/**
2343
 * Similar to readfile_accel() but designed for strings.
2344
 * @param string $string
2345
 * @param string $mimetype
2346
 * @param bool $accelerate Ignored
2347
 * @return void
2348
 */
2349
function readstring_accel($string, $mimetype, $accelerate = false) {
2350
    global $CFG;
2351
 
2352
    if ($mimetype === 'text/plain') {
2353
        // there is no encoding specified in text files, we need something consistent
2354
        header('Content-Type: text/plain; charset=utf-8');
2355
    } else {
2356
        header('Content-Type: '.$mimetype);
2357
    }
2358
    header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
2359
    header('Accept-Ranges: none');
2360
    header('Content-Length: '.strlen($string));
2361
    echo $string;
2362
}
2363
 
2364
/**
2365
 * Handles the sending of temporary file to user, download is forced.
2366
 * File is deleted after abort or successful sending, does not return, script terminated
2367
 *
2368
 * @param string $path path to file, preferably from moodledata/temp/something; or content of file itself
2369
 * @param string $filename proposed file name when saving file
2370
 * @param bool $pathisstring If the path is string
2371
 */
2372
function send_temp_file($path, $filename, $pathisstring=false) {
2373
    global $CFG;
2374
 
2375
    // Guess the file's MIME type.
2376
    $mimetype = get_mimetype_for_sending($filename);
2377
 
2378
    // close session - not needed anymore
2379
    \core\session\manager::write_close();
2380
 
2381
    if (!$pathisstring) {
2382
        if (!file_exists($path)) {
2383
            send_header_404();
2384
            throw new \moodle_exception('filenotfound', 'error', $CFG->wwwroot.'/');
2385
        }
2386
        // executed after normal finish or abort
2387
        core_shutdown_manager::register_function('send_temp_file_finished', array($path));
2388
    }
2389
 
2390
    // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
2391
    if (core_useragent::is_ie() || core_useragent::is_edge()) {
2392
        $filename = urlencode($filename);
2393
    }
2394
 
2395
    // If this file was requested from a form, then mark download as complete.
2396
    \core_form\util::form_download_complete();
2397
 
2398
    header('Content-Disposition: attachment; filename="'.$filename.'"');
2399
    if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.
2400
        header('Cache-Control: private, max-age=10, no-transform');
2401
        header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2402
        header('Pragma: ');
2403
    } else { //normal http - prevent caching at all cost
2404
        header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0, no-transform');
2405
        header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2406
        header('Pragma: no-cache');
2407
    }
2408
 
2409
    // send the contents - we can not accelerate this because the file will be deleted asap
2410
    if ($pathisstring) {
2411
        readstring_accel($path, $mimetype);
2412
    } else {
2413
        readfile_accel($path, $mimetype, false);
2414
        @unlink($path);
2415
    }
2416
 
2417
    die; //no more chars to output
2418
}
2419
 
2420
/**
2421
 * Internal callback function used by send_temp_file()
2422
 *
2423
 * @param string $path
2424
 */
2425
function send_temp_file_finished($path) {
2426
    if (file_exists($path)) {
2427
        @unlink($path);
2428
    }
2429
}
2430
 
2431
/**
2432
 * Serve content which is not meant to be cached.
2433
 *
2434
 * This is only intended to be used for volatile public files, for instance
2435
 * when development is enabled, or when caching is not required on a public resource.
2436
 *
2437
 * @param string $content Raw content.
2438
 * @param string $filename The file name.
2439
 * @return void
2440
 */
2441
function send_content_uncached($content, $filename) {
2442
    $mimetype = mimeinfo('type', $filename);
2443
    $charset = strpos($mimetype, 'text/') === 0 ? '; charset=utf-8' : '';
2444
 
2445
    header('Content-Disposition: inline; filename="' . $filename . '"');
2446
    header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');
2447
    header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2) . ' GMT');
2448
    header('Pragma: ');
2449
    header('Accept-Ranges: none');
2450
    header('Content-Type: ' . $mimetype . $charset);
2451
    header('Content-Length: ' . strlen($content));
2452
 
2453
    echo $content;
2454
    die();
2455
}
2456
 
2457
/**
2458
 * Safely save content to a certain path.
2459
 *
2460
 * This function tries hard to be atomic by first copying the content
2461
 * to a separate file, and then moving the file across. It also prevents
2462
 * the user to abort a request to prevent half-safed files.
2463
 *
2464
 * This function is intended to be used when saving some content to cache like
2465
 * $CFG->localcachedir. If you're not caching a file you should use the File API.
2466
 *
2467
 * @param string $content The file content.
2468
 * @param string $destination The absolute path of the final file.
2469
 * @return void
2470
 */
2471
function file_safe_save_content($content, $destination) {
2472
    global $CFG;
2473
 
2474
    clearstatcache();
2475
    if (!file_exists(dirname($destination))) {
2476
        @mkdir(dirname($destination), $CFG->directorypermissions, true);
2477
    }
2478
 
2479
    // Prevent serving of incomplete file from concurrent request,
2480
    // the rename() should be more atomic than fwrite().
2481
    ignore_user_abort(true);
2482
    if ($fp = fopen($destination . '.tmp', 'xb')) {
2483
        fwrite($fp, $content);
2484
        fclose($fp);
2485
        rename($destination . '.tmp', $destination);
2486
        @chmod($destination, $CFG->filepermissions);
2487
        @unlink($destination . '.tmp'); // Just in case anything fails.
2488
    }
2489
    ignore_user_abort(false);
2490
    if (connection_aborted()) {
2491
        die();
2492
    }
2493
}
2494
 
2495
/**
2496
 * Handles the sending of file data to the user's browser, including support for
2497
 * byteranges etc.
2498
 *
2499
 * @category files
2500
 * @param string|stored_file $path Path of file on disk (including real filename),
2501
 *                                 or actual content of file as string,
2502
 *                                 or stored_file object
2503
 * @param string $filename Filename to send
2504
 * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
2505
 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
2506
 * @param bool $pathisstring If true (default false), $path is the content to send and not the pathname.
2507
 *                           Forced to false when $path is a stored_file object.
2508
 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
2509
 * @param string $mimetype Include to specify the MIME type; leave blank to have it guess the type from $filename
2510
 * @param bool $dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.
2511
 *                        if this is passed as true, ignore_user_abort is called.  if you don't want your processing to continue on cancel,
2512
 *                        you must detect this case when control is returned using connection_aborted. Please not that session is closed
2513
 *                        and should not be reopened.
2514
 * @param array $options An array of options, currently accepts:
2515
 *                       - (string) cacheability: public, or private.
2516
 *                       - (string|null) immutable
2517
 *                       - (bool) dontforcesvgdownload: true if force download should be disabled on SVGs.
2518
 *                                Note: This overrides a security feature, so should only be applied to "trusted" content
2519
 *                                (eg module content that is created using an XSS risk flagged capability, such as SCORM).
2520
 * @return null script execution stopped unless $dontdie is true
2521
 */
2522
function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring=false, $forcedownload=false, $mimetype='',
2523
                   $dontdie=false, array $options = array()) {
2524
    global $CFG, $COURSE;
2525
 
2526
    if ($dontdie) {
2527
        ignore_user_abort(true);
2528
    }
2529
 
2530
    if ($lifetime === 'default' or is_null($lifetime)) {
2531
        $lifetime = $CFG->filelifetime;
2532
    }
2533
 
2534
    if (is_object($path)) {
2535
        $pathisstring = false;
2536
    }
2537
 
2538
    \core\session\manager::write_close(); // Unlock session during file serving.
2539
 
2540
    // Use given MIME type if specified, otherwise guess it.
2541
    if (!$mimetype || $mimetype === 'document/unknown') {
2542
        $mimetype = get_mimetype_for_sending($filename);
2543
    }
2544
 
2545
    // if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popup
2546
    if (core_useragent::is_ie() || core_useragent::is_edge()) {
2547
        $filename = rawurlencode($filename);
2548
    }
2549
 
2550
    // Make sure we force download of SVG files, unless the module explicitly allows them (eg within SCORM content).
2551
    // This is for security reasons (https://digi.ninja/blog/svg_xss.php).
2552
    if (file_is_svg_image_from_mimetype($mimetype) && empty($options['dontforcesvgdownload'])) {
2553
        $forcedownload = true;
2554
    }
2555
 
2556
    if ($forcedownload) {
2557
        header('Content-Disposition: attachment; filename="'.$filename.'"');
2558
 
2559
        // If this file was requested from a form, then mark download as complete.
2560
        \core_form\util::form_download_complete();
2561
    } else if ($mimetype !== 'application/x-shockwave-flash') {
2562
        // If this is an swf don't pass content-disposition with filename as this makes the flash player treat the file
2563
        // as an upload and enforces security that may prevent the file from being loaded.
2564
 
2565
        header('Content-Disposition: inline; filename="'.$filename.'"');
2566
    }
2567
 
2568
    if ($lifetime > 0) {
2569
        $immutable = '';
2570
        if (!empty($options['immutable'])) {
2571
            $immutable = ', immutable';
2572
            // Overwrite lifetime accordingly:
2573
            // 90 days only - based on Moodle point release cadence being every 3 months.
2574
            $lifetimemin = 60 * 60 * 24 * 90;
2575
            $lifetime = max($lifetime, $lifetimemin);
2576
        }
2577
        $cacheability = ' public,';
2578
        if (!empty($options['cacheability']) && ($options['cacheability'] === 'public')) {
2579
            // This file must be cache-able by both browsers and proxies.
2580
            $cacheability = ' public,';
2581
        } else if (!empty($options['cacheability']) && ($options['cacheability'] === 'private')) {
2582
            // This file must be cache-able only by browsers.
2583
            $cacheability = ' private,';
2584
        } else if (isloggedin() and !isguestuser()) {
2585
            // By default, under the conditions above, this file must be cache-able only by browsers.
2586
            $cacheability = ' private,';
2587
        }
2588
        $nobyteserving = false;
2589
        header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform'.$immutable);
2590
        header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
2591
        header('Pragma: ');
2592
 
2593
    } else { // Do not cache files in proxies and browsers
2594
        $nobyteserving = true;
2595
        if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.
2596
            header('Cache-Control: private, max-age=10, no-transform');
2597
            header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2598
            header('Pragma: ');
2599
        } else { //normal http - prevent caching at all cost
2600
            header('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0, no-transform');
2601
            header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');
2602
            header('Pragma: no-cache');
2603
        }
2604
    }
2605
 
2606
    if (empty($filter)) {
2607
        // send the contents
2608
        if ($pathisstring) {
2609
            readstring_accel($path, $mimetype);
2610
        } else {
2611
            readfile_accel($path, $mimetype, !$dontdie);
2612
        }
2613
 
2614
    } else {
2615
        // Try to put the file through filters
2616
        if ($mimetype == 'text/html' || $mimetype == 'application/xhtml+xml' || file_is_svg_image_from_mimetype($mimetype)) {
2617
            $options = new stdClass();
2618
            $options->noclean = true;
2619
            $options->nocache = true; // temporary workaround for MDL-5136
2620
            if (is_object($path)) {
2621
                $text = $path->get_content();
2622
            } else if ($pathisstring) {
2623
                $text = $path;
2624
            } else {
2625
                $text = implode('', file($path));
2626
            }
2627
            $output = format_text($text, FORMAT_HTML, $options, $COURSE->id);
2628
 
2629
            readstring_accel($output, $mimetype);
2630
 
2631
        } else if (($mimetype == 'text/plain') and ($filter == 1)) {
2632
            // only filter text if filter all files is selected
2633
            $options = new stdClass();
2634
            $options->newlines = false;
2635
            $options->noclean = true;
2636
            if (is_object($path)) {
2637
                $text = htmlentities($path->get_content(), ENT_QUOTES, 'UTF-8');
2638
            } else if ($pathisstring) {
2639
                $text = htmlentities($path, ENT_QUOTES, 'UTF-8');
2640
            } else {
2641
                $text = htmlentities(implode('', file($path)), ENT_QUOTES, 'UTF-8');
2642
            }
2643
            $output = '<pre>'. format_text($text, FORMAT_MOODLE, $options, $COURSE->id) .'</pre>';
2644
 
2645
            readstring_accel($output, $mimetype);
2646
 
2647
        } else {
2648
            // send the contents
2649
            if ($pathisstring) {
2650
                readstring_accel($path, $mimetype);
2651
            } else {
2652
                readfile_accel($path, $mimetype, !$dontdie);
2653
            }
2654
        }
2655
    }
2656
    if ($dontdie) {
2657
        return;
2658
    }
2659
    die; //no more chars to output!!!
2660
}
2661
 
2662
/**
2663
 * Handles the sending of file data to the user's browser, including support for
2664
 * byteranges etc.
2665
 *
2666
 * The $options parameter supports the following keys:
2667
 *  (string|null) preview - send the preview of the file (e.g. "thumb" for a thumbnail)
2668
 *  (string|null) filename - overrides the implicit filename
2669
 *  (bool) dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.
2670
 *      if this is passed as true, ignore_user_abort is called.  if you don't want your processing to continue on cancel,
2671
 *      you must detect this case when control is returned using connection_aborted. Please not that session is closed
2672
 *      and should not be reopened
2673
 *  (string|null) cacheability - force the cacheability setting of the HTTP response, "private" or "public",
2674
 *      when $lifetime is greater than 0. Cacheability defaults to "private" when logged in as other than guest; otherwise,
2675
 *      defaults to "public".
2676
 *  (string|null) immutable - set the immutable cache setting in the HTTP response, when served under HTTPS.
2677
 *      Note: it's up to the consumer to set it properly i.e. when serving a "versioned" URL.
2678
 *
2679
 * @category files
2680
 * @param stored_file $storedfile local file object
2681
 * @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)
2682
 * @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only
2683
 * @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin
2684
 * @param array $options additional options affecting the file serving
2685
 * @return null script execution stopped unless $options['dontdie'] is true
2686
 */
2687
function send_stored_file($storedfile, $lifetime=null, $filter=0, $forcedownload=false, array $options=array()) {
2688
    global $CFG, $COURSE;
2689
 
2690
    static $recursion = 0;
2691
 
2692
    if (empty($options['filename'])) {
2693
        $filename = null;
2694
    } else {
2695
        $filename = $options['filename'];
2696
    }
2697
 
2698
    if (empty($options['dontdie'])) {
2699
        $dontdie = false;
2700
    } else {
2701
        $dontdie = true;
2702
    }
2703
 
2704
    if ($lifetime === 'default' or is_null($lifetime)) {
2705
        $lifetime = $CFG->filelifetime;
2706
    }
2707
 
2708
    if (!empty($options['preview'])) {
2709
        // replace the file with its preview
2710
        $fs = get_file_storage();
2711
        $previewfile = $fs->get_file_preview($storedfile, $options['preview']);
2712
        if (!$previewfile) {
2713
            // Unable to create a preview of the file, send its default mime icon instead.
2714
            $fileicon = file_file_icon($storedfile);
2715
            send_file($CFG->dirroot.'/pix/'.$fileicon.'.svg', basename($fileicon).'.svg');
2716
        } else {
2717
            // preview images have fixed cache lifetime and they ignore forced download
2718
            // (they are generated by GD and therefore they are considered reasonably safe).
2719
            $storedfile = $previewfile;
2720
            $lifetime = DAYSECS;
2721
            $filter = 0;
2722
            $forcedownload = false;
2723
        }
2724
    }
2725
 
2726
    // handle external resource
2727
    if ($storedfile && $storedfile->is_external_file() && !isset($options['sendcachedexternalfile'])) {
2728
 
2729
        // Have we been here before?
2730
        $recursion++;
2731
        if ($recursion > 10) {
2732
            throw new coding_exception('Recursive file serving detected');
2733
        }
2734
 
2735
        $storedfile->send_file($lifetime, $filter, $forcedownload, $options);
2736
        die;
2737
    }
2738
 
2739
    if (!$storedfile || $storedfile->is_directory()) {
2740
        // Nothing to serve.
2741
        if ($dontdie) {
2742
            return;
2743
        }
2744
        die;
2745
    }
2746
 
2747
    $filename = is_null($filename) ? $storedfile->get_filename() : $filename;
2748
 
2749
    // Use given MIME type if specified.
2750
    $mimetype = $storedfile->get_mimetype();
2751
 
2752
    // Allow cross-origin requests only for Web Services.
2753
    // This allow to receive requests done by Web Workers or webapps in different domains.
2754
    if (WS_SERVER) {
2755
        header('Access-Control-Allow-Origin: *');
2756
    }
2757
 
2758
    send_file($storedfile, $filename, $lifetime, $filter, false, $forcedownload, $mimetype, $dontdie, $options);
2759
}
2760
 
2761
/**
2762
 * Recursively delete the file or folder with path $location. That is,
2763
 * if it is a file delete it. If it is a folder, delete all its content
2764
 * then delete it. If $location does not exist to start, that is not
2765
 * considered an error.
2766
 *
2767
 * @param string $location the path to remove.
2768
 * @return bool
2769
 */
2770
function fulldelete($location) {
2771
    if (empty($location)) {
2772
        // extra safety against wrong param
2773
        return false;
2774
    }
2775
    if (is_dir($location)) {
2776
        if (!$currdir = opendir($location)) {
2777
            return false;
2778
        }
2779
        while (false !== ($file = readdir($currdir))) {
2780
            if ($file <> ".." && $file <> ".") {
2781
                $fullfile = $location."/".$file;
2782
                if (is_dir($fullfile)) {
2783
                    if (!fulldelete($fullfile)) {
2784
                        return false;
2785
                    }
2786
                } else {
2787
                    if (!unlink($fullfile)) {
2788
                        return false;
2789
                    }
2790
                }
2791
            }
2792
        }
2793
        closedir($currdir);
2794
        if (! rmdir($location)) {
2795
            return false;
2796
        }
2797
 
2798
    } else if (file_exists($location)) {
2799
        if (!unlink($location)) {
2800
            return false;
2801
        }
2802
    }
2803
    return true;
2804
}
2805
 
2806
/**
2807
 * Send requested byterange of file.
2808
 *
2809
 * @param resource $handle A file handle
2810
 * @param string $mimetype The mimetype for the output
2811
 * @param array $ranges An array of ranges to send
2812
 * @param string $filesize The size of the content if only one range is used
2813
 */
2814
function byteserving_send_file($handle, $mimetype, $ranges, $filesize) {
2815
    // better turn off any kind of compression and buffering
2816
    ini_set('zlib.output_compression', 'Off');
2817
 
2818
    $chunksize = 1*(1024*1024); // 1MB chunks - must be less than 2MB!
2819
    if ($handle === false) {
2820
        die;
2821
    }
2822
    if (count($ranges) == 1) { //only one range requested
2823
        $length = $ranges[0][2] - $ranges[0][1] + 1;
2824
        header('HTTP/1.1 206 Partial content');
2825
        header('Content-Length: '.$length);
2826
        header('Content-Range: bytes '.$ranges[0][1].'-'.$ranges[0][2].'/'.$filesize);
2827
        header('Content-Type: '.$mimetype);
2828
 
2829
        while(@ob_get_level()) {
2830
            if (!@ob_end_flush()) {
2831
                break;
2832
            }
2833
        }
2834
 
2835
        fseek($handle, $ranges[0][1]);
2836
        while (!feof($handle) && $length > 0) {
2837
            core_php_time_limit::raise(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk
2838
            $buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));
2839
            echo $buffer;
2840
            flush();
2841
            $length -= strlen($buffer);
2842
        }
2843
        fclose($handle);
2844
        die;
2845
    } else { // multiple ranges requested - not tested much
2846
        $totallength = 0;
2847
        foreach($ranges as $range) {
2848
            $totallength += strlen($range[0]) + $range[2] - $range[1] + 1;
2849
        }
2850
        $totallength += strlen("\r\n--".BYTESERVING_BOUNDARY."--\r\n");
2851
        header('HTTP/1.1 206 Partial content');
2852
        header('Content-Length: '.$totallength);
2853
        header('Content-Type: multipart/byteranges; boundary='.BYTESERVING_BOUNDARY);
2854
 
2855
        while(@ob_get_level()) {
2856
            if (!@ob_end_flush()) {
2857
                break;
2858
            }
2859
        }
2860
 
2861
        foreach($ranges as $range) {
2862
            $length = $range[2] - $range[1] + 1;
2863
            echo $range[0];
2864
            fseek($handle, $range[1]);
2865
            while (!feof($handle) && $length > 0) {
2866
                core_php_time_limit::raise(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk
2867
                $buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));
2868
                echo $buffer;
2869
                flush();
2870
                $length -= strlen($buffer);
2871
            }
2872
        }
2873
        echo "\r\n--".BYTESERVING_BOUNDARY."--\r\n";
2874
        fclose($handle);
2875
        die;
2876
    }
2877
}
2878
 
2879
/**
2880
 * Tells whether the filename is executable.
2881
 *
2882
 * @link http://php.net/manual/en/function.is-executable.php
2883
 * @link https://bugs.php.net/bug.php?id=41062
2884
 * @param string $filename Path to the file.
2885
 * @return bool True if the filename exists and is executable; otherwise, false.
2886
 */
2887
function file_is_executable($filename) {
2888
    if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
2889
        if (is_executable($filename)) {
2890
            return true;
2891
        } else {
2892
            $fileext = strrchr($filename, '.');
2893
            // If we have an extension we can check if it is listed as executable.
2894
            if ($fileext && file_exists($filename) && !is_dir($filename)) {
2895
                $winpathext = strtolower(getenv('PATHEXT'));
2896
                $winpathexts = explode(';', $winpathext);
2897
 
2898
                return in_array(strtolower($fileext), $winpathexts);
2899
            }
2900
 
2901
            return false;
2902
        }
2903
    } else {
2904
        return is_executable($filename);
2905
    }
2906
}
2907
 
2908
/**
2909
 * Overwrite an existing file in a draft area.
2910
 *
2911
 * @param  stored_file $newfile      the new file with the new content and meta-data
2912
 * @param  stored_file $existingfile the file that will be overwritten
2913
 * @throws moodle_exception
2914
 * @since Moodle 3.2
2915
 */
2916
function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $existingfile) {
2917
    if ($existingfile->get_component() != 'user' or $existingfile->get_filearea() != 'draft') {
2918
        throw new coding_exception('The file to overwrite is not in a draft area.');
2919
    }
2920
 
2921
    $fs = get_file_storage();
2922
    // Remember original file source field.
2923
    $source = @unserialize($existingfile->get_source() ?? '');
2924
    // Remember the original sortorder.
2925
    $sortorder = $existingfile->get_sortorder();
2926
    if ($newfile->is_external_file()) {
2927
        // New file is a reference. Check that existing file does not have any other files referencing to it
2928
        if (isset($source->original) && $fs->search_references_count($source->original)) {
2929
            throw new moodle_exception('errordoublereference', 'repository');
2930
        }
2931
    }
2932
 
2933
    // Delete existing file to release filename.
2934
    $newfilerecord = array(
2935
        'contextid' => $existingfile->get_contextid(),
2936
        'component' => 'user',
2937
        'filearea' => 'draft',
2938
        'itemid' => $existingfile->get_itemid(),
2939
        'timemodified' => time()
2940
    );
2941
    $existingfile->delete();
2942
 
2943
    // Create new file.
2944
    $newfile = $fs->create_file_from_storedfile($newfilerecord, $newfile);
2945
    // Preserve original file location (stored in source field) for handling references.
2946
    if (isset($source->original)) {
2947
        if (!($newfilesource = @unserialize($newfile->get_source() ?? ''))) {
2948
            $newfilesource = new stdClass();
2949
        }
2950
        $newfilesource->original = $source->original;
2951
        $newfile->set_source(serialize($newfilesource));
2952
    }
2953
    $newfile->set_sortorder($sortorder);
2954
}
2955
 
2956
/**
2957
 * Add files from a draft area into a final area.
2958
 *
2959
 * Most of the time you do not want to use this. It is intended to be used
2960
 * by asynchronous services which cannot direcly manipulate a final
2961
 * area through a draft area. Instead they add files to a new draft
2962
 * area and merge that new draft into the final area when ready.
2963
 *
2964
 * @param int $draftitemid the id of the draft area to use.
2965
 * @param int $contextid this parameter and the next two identify the file area to save to.
2966
 * @param string $component component name
2967
 * @param string $filearea indentifies the file area
2968
 * @param int $itemid identifies the item id or false for all items in the file area
2969
 * @param array $options area options (subdirs=false, maxfiles=-1, maxbytes=0, areamaxbytes=FILE_AREA_MAX_BYTES_UNLIMITED)
2970
 * @see file_save_draft_area_files
2971
 * @since Moodle 3.2
2972
 */
2973
function file_merge_files_from_draft_area_into_filearea($draftitemid, $contextid, $component, $filearea, $itemid,
2974
                                                        array $options = null) {
2975
    // We use 0 here so file_prepare_draft_area creates a new one, finaldraftid will be updated with the new draft id.
2976
    $finaldraftid = 0;
2977
    file_prepare_draft_area($finaldraftid, $contextid, $component, $filearea, $itemid, $options);
2978
    file_merge_draft_area_into_draft_area($draftitemid, $finaldraftid);
2979
    file_save_draft_area_files($finaldraftid, $contextid, $component, $filearea, $itemid, $options);
2980
}
2981
 
2982
/**
2983
 * Merge files from two draftarea areas.
2984
 *
2985
 * This does not handle conflict resolution, files in the destination area which appear
2986
 * to be more recent will be kept disregarding the intended ones.
2987
 *
2988
 * @param int $getfromdraftid the id of the draft area where are the files to merge.
2989
 * @param int $mergeintodraftid the id of the draft area where new files will be merged.
2990
 * @throws coding_exception
2991
 * @since Moodle 3.2
2992
 */
2993
function file_merge_draft_area_into_draft_area($getfromdraftid, $mergeintodraftid) {
2994
    global $USER;
2995
 
2996
    $fs = get_file_storage();
2997
    $contextid = context_user::instance($USER->id)->id;
2998
 
2999
    if (!$filestomerge = $fs->get_area_files($contextid, 'user', 'draft', $getfromdraftid)) {
3000
        throw new coding_exception('Nothing to merge or area does not belong to current user');
3001
    }
3002
 
3003
    $currentfiles = $fs->get_area_files($contextid, 'user', 'draft', $mergeintodraftid);
3004
 
3005
    // Get hashes of the files to merge.
3006
    $newhashes = array();
3007
    foreach ($filestomerge as $filetomerge) {
3008
        $filepath = $filetomerge->get_filepath();
3009
        $filename = $filetomerge->get_filename();
3010
 
3011
        $newhash = $fs->get_pathname_hash($contextid, 'user', 'draft', $mergeintodraftid, $filepath, $filename);
3012
        $newhashes[$newhash] = $filetomerge;
3013
    }
3014
 
3015
    // Calculate wich files must be added.
3016
    foreach ($currentfiles as $file) {
3017
        $filehash = $file->get_pathnamehash();
3018
        // One file to be merged already exists.
3019
        if (isset($newhashes[$filehash])) {
3020
            $updatedfile = $newhashes[$filehash];
3021
 
3022
            // Avoid race conditions.
3023
            if ($file->get_timemodified() > $updatedfile->get_timemodified()) {
3024
                // The existing file is more recent, do not copy the suposedly "new" one.
3025
                unset($newhashes[$filehash]);
3026
                continue;
3027
            }
3028
            // Update existing file (not only content, meta-data too).
3029
            file_overwrite_existing_draftfile($updatedfile, $file);
3030
            unset($newhashes[$filehash]);
3031
        }
3032
    }
3033
 
3034
    foreach ($newhashes as $newfile) {
3035
        $newfilerecord = array(
3036
            'contextid' => $contextid,
3037
            'component' => 'user',
3038
            'filearea' => 'draft',
3039
            'itemid' => $mergeintodraftid,
3040
            'timemodified' => time()
3041
        );
3042
 
3043
        $fs->create_file_from_storedfile($newfilerecord, $newfile);
3044
    }
3045
}
3046
 
3047
/**
3048
 * Attempt to determine whether the specified mime-type is an SVG image or not.
3049
 *
3050
 * @param string $mimetype Mime-type
3051
 * @return bool True if it is an SVG file
3052
 */
3053
function file_is_svg_image_from_mimetype(string $mimetype): bool {
3054
    return preg_match('|^image/svg|', $mimetype);
3055
}
3056
 
3057
/**
3058
 * Returns the moodle proxy configuration as a formatted url
3059
 *
3060
 * @return string the string to use for proxy settings.
3061
 */
3062
function get_moodle_proxy_url() {
3063
    global $CFG;
3064
    $proxy = '';
3065
    if (empty($CFG->proxytype)) {
3066
        return $proxy;
3067
    }
3068
    if (empty($CFG->proxyhost)) {
3069
        return $proxy;
3070
    }
3071
    if ($CFG->proxytype === 'SOCKS5') {
3072
        // If it is a SOCKS proxy, append the protocol info.
3073
        $protocol = 'socks5://';
3074
    } else {
3075
        $protocol = '';
3076
    }
3077
    $proxy = $CFG->proxyhost;
3078
    if (!empty($CFG->proxyport)) {
3079
        $proxy .= ':'. $CFG->proxyport;
3080
    }
3081
    if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {
3082
        $proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy;
3083
    }
3084
    return $proxy;
3085
}
3086
 
3087
 
3088
 
3089
/**
3090
 * RESTful cURL class
3091
 *
3092
 * This is a wrapper class for curl, it is quite easy to use:
3093
 * <code>
3094
 * $c = new curl;
3095
 * // enable cache
3096
 * $c = new curl(array('cache'=>true));
3097
 * // enable cookie
3098
 * $c = new curl(array('cookie'=>true));
3099
 * // enable proxy
3100
 * $c = new curl(array('proxy'=>true));
3101
 *
3102
 * // HTTP GET Method
3103
 * $html = $c->get('http://example.com');
3104
 * // HTTP POST Method
3105
 * $html = $c->post('http://example.com/', array('q'=>'words', 'name'=>'moodle'));
3106
 * // HTTP PUT Method
3107
 * $html = $c->put('http://example.com/', array('file'=>'/var/www/test.txt');
3108
 * </code>
3109
 *
3110
 * @package   core_files
3111
 * @category files
3112
 * @copyright Dongsheng Cai <dongsheng@moodle.com>
3113
 * @license   http://www.gnu.org/copyleft/gpl.html GNU Public License
3114
 */
3115
class curl {
3116
    /** @var curl_cache|false Caches http request contents */
3117
    public $cache    = false;
3118
    /** @var bool Uses proxy, null means automatic based on URL */
3119
    public $proxy    = null;
3120
    /** @var string library version */
3121
    public $version  = '0.4 dev';
3122
    /** @var array http's response */
3123
    public $response = array();
3124
    /** @var array Raw response headers, needed for BC in download_file_content(). */
3125
    public $rawresponse = array();
3126
    /** @var array http header */
3127
    public $header   = array();
3128
    /** @var array cURL information */
3129
    public $info;
3130
    /** @var string error */
3131
    public $error;
3132
    /** @var int error code */
3133
    public $errno;
3134
    /** @var bool Perform redirects at PHP level instead of relying on native cURL functionality. Always true now. */
3135
    public $emulateredirects = null;
3136
 
3137
    /** @var array cURL options */
3138
    private $options;
3139
 
3140
    /** @var string Proxy host */
3141
    private $proxy_host = '';
3142
    /** @var string Proxy auth */
3143
    private $proxy_auth = '';
3144
    /** @var string Proxy type */
3145
    private $proxy_type = '';
3146
    /** @var bool Debug mode on */
3147
    private $debug    = false;
3148
    /** @var bool|string Path to cookie file */
3149
    private $cookie   = false;
3150
    /** @var bool tracks multiple headers in response - redirect detection */
3151
    private $responsefinished = false;
3152
    /** @var ?\core\files\curl_security_helper security helper class, responsible for checking host/ports against allowed/blocked entries.*/
3153
    private $securityhelper;
3154
    /** @var bool ignoresecurity a flag which can be supplied to the constructor, allowing security to be bypassed. */
3155
    private $ignoresecurity;
3156
    /** @var array $mockresponses For unit testing only - return the head of this list instead of making the next request. */
3157
    private static $mockresponses = [];
3158
    /** @var array temporary params value if the value is not belongs to class stored_file. */
3159
    public $_tmp_file_post_params = [];
3160
 
3161
    /**
3162
     * Curl constructor.
3163
     *
3164
     * Allowed settings are:
3165
     *  proxy: (bool) use proxy server, null means autodetect non-local from url
3166
     *  debug: (bool) use debug output
3167
     *  cookie: (string) path to cookie file, false if none
3168
     *  cache: (bool) use cache
3169
     *  module_cache: (string) type of cache
3170
     *  securityhelper: (\core\files\curl_security_helper_base) helper object providing URL checking for requests.
3171
     *  ignoresecurity: (bool) set true to override and ignore the security helper when making requests.
3172
     *
3173
     * @param array $settings
3174
     */
3175
    public function __construct($settings = array()) {
3176
        global $CFG;
3177
        if (!function_exists('curl_init')) {
3178
            $this->error = 'cURL module must be enabled!';
3179
            trigger_error($this->error, E_USER_ERROR);
3180
            return;
3181
        }
3182
 
3183
        // All settings of this class should be init here.
3184
        $this->resetopt();
3185
        if (!empty($settings['debug'])) {
3186
            $this->debug = true;
3187
        }
3188
        if (!empty($settings['cookie'])) {
3189
            if($settings['cookie'] === true) {
3190
                $this->cookie = $CFG->dataroot.'/curl_cookie.txt';
3191
            } else {
3192
                $this->cookie = $settings['cookie'];
3193
            }
3194
        }
3195
        if (!empty($settings['cache'])) {
3196
            if (class_exists('curl_cache')) {
3197
                if (!empty($settings['module_cache'])) {
3198
                    $this->cache = new curl_cache($settings['module_cache']);
3199
                } else {
3200
                    $this->cache = new curl_cache('misc');
3201
                }
3202
            }
3203
        }
3204
        if (!empty($CFG->proxyhost)) {
3205
            if (empty($CFG->proxyport)) {
3206
                $this->proxy_host = $CFG->proxyhost;
3207
            } else {
3208
                $this->proxy_host = $CFG->proxyhost.':'.$CFG->proxyport;
3209
            }
3210
            if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {
3211
                $this->proxy_auth = $CFG->proxyuser.':'.$CFG->proxypassword;
3212
                $this->setopt(array(
3213
                            'proxyauth'=> CURLAUTH_BASIC | CURLAUTH_NTLM,
3214
                            'proxyuserpwd'=>$this->proxy_auth));
3215
            }
3216
            if (!empty($CFG->proxytype)) {
3217
                if ($CFG->proxytype == 'SOCKS5') {
3218
                    $this->proxy_type = CURLPROXY_SOCKS5;
3219
                } else {
3220
                    $this->proxy_type = CURLPROXY_HTTP;
3221
                    $this->setopt([
3222
                        'httpproxytunnel' => false,
3223
                    ]);
3224
                    if (defined('CURLOPT_SUPPRESS_CONNECT_HEADERS')) {
3225
                        $this->setopt([
3226
                            'suppress_connect_headers' => true,
3227
                        ]);
3228
                    }
3229
                }
3230
                $this->setopt(array('proxytype'=>$this->proxy_type));
3231
            }
3232
 
3233
            if (isset($settings['proxy'])) {
3234
                $this->proxy = $settings['proxy'];
3235
            }
3236
        } else {
3237
            $this->proxy = false;
3238
        }
3239
 
3240
        // All redirects are performed at PHP level now and each one is checked against blocked URLs rules. We do not
3241
        // want to let cURL naively follow the redirect chain and visit every URL for security reasons. Even when the
3242
        // caller explicitly wants to ignore the security checks, we would need to fall back to the original
3243
        // implementation and use emulated redirects if open_basedir is in effect to avoid the PHP warning
3244
        // "CURLOPT_FOLLOWLOCATION cannot be activated when in safe_mode or an open_basedir". So it is better to simply
3245
        // ignore this property and always handle redirects at this PHP wrapper level and not inside the native cURL.
3246
        $this->emulateredirects = true;
3247
 
3248
        // Curl security setup. Allow injection of a security helper, but if not found, default to the core helper.
3249
        if (isset($settings['securityhelper']) && $settings['securityhelper'] instanceof \core\files\curl_security_helper_base) {
3250
            $this->set_security($settings['securityhelper']);
3251
        } else {
3252
            $this->set_security(new \core\files\curl_security_helper());
3253
        }
3254
        $this->ignoresecurity = isset($settings['ignoresecurity']) ? $settings['ignoresecurity'] : false;
3255
    }
3256
 
3257
    /**
3258
     * Resets the CURL options that have already been set
3259
     */
3260
    public function resetopt() {
3261
        $this->options = array();
3262
        $this->options['CURLOPT_USERAGENT']         = \core_useragent::get_moodlebot_useragent();
3263
        // True to include the header in the output
3264
        $this->options['CURLOPT_HEADER']            = 0;
3265
        // True to Exclude the body from the output
3266
        $this->options['CURLOPT_NOBODY']            = 0;
3267
        // Redirect ny default.
3268
        $this->options['CURLOPT_FOLLOWLOCATION']    = 1;
3269
        $this->options['CURLOPT_MAXREDIRS']         = 10;
3270
        $this->options['CURLOPT_ENCODING']          = '';
3271
        // TRUE to return the transfer as a string of the return
3272
        // value of curl_exec() instead of outputting it out directly.
3273
        $this->options['CURLOPT_RETURNTRANSFER']    = 1;
3274
        $this->options['CURLOPT_SSL_VERIFYPEER']    = 0;
3275
        $this->options['CURLOPT_SSL_VERIFYHOST']    = 2;
3276
        $this->options['CURLOPT_CONNECTTIMEOUT']    = 30;
3277
 
3278
        if ($cacert = self::get_cacert()) {
3279
            $this->options['CURLOPT_CAINFO'] = $cacert;
3280
        }
3281
    }
3282
 
3283
    /**
3284
     * Get the location of ca certificates.
3285
     * @return string absolute file path or empty if default used
3286
     */
3287
    public static function get_cacert() {
3288
        global $CFG;
3289
 
3290
        // Bundle in dataroot always wins.
3291
        if (is_readable("$CFG->dataroot/moodleorgca.crt")) {
3292
            return realpath("$CFG->dataroot/moodleorgca.crt");
3293
        }
3294
 
3295
        // Next comes the default from php.ini
3296
        $cacert = ini_get('curl.cainfo');
3297
        if (!empty($cacert) and is_readable($cacert)) {
3298
            return realpath($cacert);
3299
        }
3300
 
3301
        // Windows PHP does not have any certs, we need to use something.
3302
        if ($CFG->ostype === 'WINDOWS') {
3303
            if (is_readable("$CFG->libdir/cacert.pem")) {
3304
                return realpath("$CFG->libdir/cacert.pem");
3305
            }
3306
        }
3307
 
3308
        // Use default, this should work fine on all properly configured *nix systems.
3309
        return null;
3310
    }
3311
 
3312
    /**
3313
     * Reset Cookie
3314
     */
3315
    public function resetcookie() {
3316
        if (!empty($this->cookie)) {
3317
            if (is_file($this->cookie)) {
3318
                $fp = fopen($this->cookie, 'w');
3319
                if (!empty($fp)) {
3320
                    fwrite($fp, '');
3321
                    fclose($fp);
3322
                }
3323
            }
3324
        }
3325
    }
3326
 
3327
    /**
3328
     * Set curl options.
3329
     *
3330
     * Do not use the curl constants to define the options, pass a string
3331
     * corresponding to that constant. Ie. to set CURLOPT_MAXREDIRS, pass
3332
     * array('CURLOPT_MAXREDIRS' => 10) or array('maxredirs' => 10) to this method.
3333
     *
3334
     * @param array $options If array is null, this function will reset the options to default value.
3335
     * @return void
3336
     * @throws coding_exception If an option uses constant value instead of option name.
3337
     */
3338
    public function setopt($options = array()) {
3339
        if (is_array($options)) {
3340
            foreach ($options as $name => $val) {
3341
                if (!is_string($name)) {
3342
                    throw new coding_exception('Curl options should be defined using strings, not constant values.');
3343
                }
3344
                if (stripos($name, 'CURLOPT_') === false) {
3345
                    // Only prefix with CURLOPT_ if the option doesn't contain CURLINFO_,
3346
                    // which is a valid prefix for at least one option CURLINFO_HEADER_OUT.
3347
                    if (stripos($name, 'CURLINFO_') === false) {
3348
                        $name = strtoupper('CURLOPT_'.$name);
3349
                    }
3350
                } else {
3351
                    $name = strtoupper($name);
3352
                }
3353
                $this->options[$name] = $val;
3354
            }
3355
        }
3356
    }
3357
 
3358
    /**
3359
     * Reset http method
3360
     */
3361
    public function cleanopt() {
3362
        unset($this->options['CURLOPT_HTTPGET']);
3363
        unset($this->options['CURLOPT_POST']);
3364
        unset($this->options['CURLOPT_POSTFIELDS']);
3365
        unset($this->options['CURLOPT_PUT']);
3366
        unset($this->options['CURLOPT_INFILE']);
3367
        unset($this->options['CURLOPT_INFILESIZE']);
3368
        unset($this->options['CURLOPT_CUSTOMREQUEST']);
3369
        unset($this->options['CURLOPT_FILE']);
3370
    }
3371
 
3372
    /**
3373
     * Resets the HTTP Request headers (to prepare for the new request)
3374
     */
3375
    public function resetHeader() {
3376
        $this->header = array();
3377
    }
3378
 
3379
    /**
3380
     * Set HTTP Request Header
3381
     *
3382
     * @param array|string $header
3383
     */
3384
    public function setHeader($header) {
3385
        if (is_array($header)) {
3386
            foreach ($header as $v) {
3387
                $this->setHeader($v);
3388
            }
3389
        } else {
3390
            // Remove newlines, they are not allowed in headers.
3391
            $newvalue = preg_replace('/[\r\n]/', '', $header);
3392
            if (!in_array($newvalue, $this->header)) {
3393
                $this->header[] = $newvalue;
3394
            }
3395
        }
3396
    }
3397
 
3398
    /**
3399
     * Get HTTP Response Headers
3400
     * @return array of arrays
3401
     */
3402
    public function getResponse() {
3403
        return $this->response;
3404
    }
3405
 
3406
    /**
3407
     * Get raw HTTP Response Headers
3408
     * @return array of strings
3409
     */
3410
    public function get_raw_response() {
3411
        return $this->rawresponse;
3412
    }
3413
 
3414
    /**
3415
     * private callback function
3416
     * Formatting HTTP Response Header
3417
     *
3418
     * We only keep the last headers returned. For example during a redirect the
3419
     * redirect headers will not appear in {@link self::getResponse()}, if you need
3420
     * to use those headers, refer to {@link self::get_raw_response()}.
3421
     *
3422
     * @param resource $ch Apparently not used
3423
     * @param string $header
3424
     * @return int The strlen of the header
3425
     */
3426
    private function formatHeader($ch, $header) {
3427
        $this->rawresponse[] = $header;
3428
 
3429
        if (trim($header, "\r\n") === '') {
3430
            // This must be the last header.
3431
            $this->responsefinished = true;
3432
        }
3433
 
3434
        if (strlen($header) > 2) {
3435
            if ($this->responsefinished) {
3436
                // We still have headers after the supposedly last header, we must be
3437
                // in a redirect so let's empty the response to keep the last headers.
3438
                $this->responsefinished = false;
3439
                $this->response = array();
3440
            }
3441
            $parts = explode(" ", rtrim($header, "\r\n"), 2);
3442
            $key = rtrim($parts[0], ':');
3443
            $value = isset($parts[1]) ? $parts[1] : null;
3444
            if (!empty($this->response[$key])) {
3445
                if (is_array($this->response[$key])) {
3446
                    $this->response[$key][] = $value;
3447
                } else {
3448
                    $tmp = $this->response[$key];
3449
                    $this->response[$key] = array();
3450
                    $this->response[$key][] = $tmp;
3451
                    $this->response[$key][] = $value;
3452
 
3453
                }
3454
            } else {
3455
                $this->response[$key] = $value;
3456
            }
3457
        }
3458
        return strlen($header);
3459
    }
3460
 
3461
    /**
3462
     * Set options for individual curl instance
3463
     *
3464
     * @param resource|CurlHandle $curl A curl handle
3465
     * @param array $options
3466
     * @return resource The curl handle
3467
     */
3468
    private function apply_opt($curl, $options) {
3469
        // Clean up
3470
        $this->cleanopt();
3471
        // set cookie
3472
        if (!empty($this->cookie) || !empty($options['cookie'])) {
3473
            $this->setopt(array('cookiejar'=>$this->cookie,
3474
                            'cookiefile'=>$this->cookie
3475
                             ));
3476
        }
3477
 
3478
        // Bypass proxy if required.
3479
        if ($this->proxy === null) {
3480
            if (!empty($this->options['CURLOPT_URL']) and is_proxybypass($this->options['CURLOPT_URL'])) {
3481
                $proxy = false;
3482
            } else {
3483
                $proxy = true;
3484
            }
3485
        } else {
3486
            $proxy = (bool)$this->proxy;
3487
        }
3488
 
3489
        // Set proxy.
3490
        if ($proxy) {
3491
            $options['CURLOPT_PROXY'] = $this->proxy_host;
3492
        } else {
3493
            unset($this->options['CURLOPT_PROXY']);
3494
        }
3495
 
3496
        $this->setopt($options);
3497
 
3498
        // Reset before set options.
3499
        curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this,'formatHeader'));
3500
 
3501
        // Setting the User-Agent based on options provided.
3502
        $useragent = '';
3503
 
3504
        if (!empty($options['CURLOPT_USERAGENT'])) {
3505
            $useragent = $options['CURLOPT_USERAGENT'];
3506
        } else if (!empty($this->options['CURLOPT_USERAGENT'])) {
3507
            $useragent = $this->options['CURLOPT_USERAGENT'];
3508
        } else {
3509
            $useragent = \core_useragent::get_moodlebot_useragent();
3510
        }
3511
 
3512
        // Set headers.
3513
        if (empty($this->header)) {
3514
            $this->setHeader(array(
3515
                'User-Agent: ' . $useragent,
3516
                'Connection: keep-alive'
3517
                ));
3518
        } else if (!in_array('User-Agent: ' . $useragent, $this->header)) {
3519
            // Remove old User-Agent if one existed.
3520
            // We have to partial search since we don't know what the original User-Agent is.
3521
            if ($match = preg_grep('/User-Agent.*/', $this->header)) {
3522
                $key = array_keys($match)[0];
3523
                unset($this->header[$key]);
3524
            }
3525
            $this->setHeader(array('User-Agent: ' . $useragent));
3526
        }
3527
        curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);
3528
 
3529
        if ($this->debug) {
3530
            echo '<h1>Options</h1>';
3531
            var_dump($this->options);
3532
            echo '<h1>Header</h1>';
3533
            var_dump($this->header);
3534
        }
3535
 
3536
        // Do not allow infinite redirects.
3537
        if (!isset($this->options['CURLOPT_MAXREDIRS'])) {
3538
            $this->options['CURLOPT_MAXREDIRS'] = 0;
3539
        } else if ($this->options['CURLOPT_MAXREDIRS'] > 100) {
3540
            $this->options['CURLOPT_MAXREDIRS'] = 100;
3541
        } else {
3542
            $this->options['CURLOPT_MAXREDIRS'] = (int)$this->options['CURLOPT_MAXREDIRS'];
3543
        }
3544
 
3545
        // Make sure we always know if redirects expected.
3546
        if (!isset($this->options['CURLOPT_FOLLOWLOCATION'])) {
3547
            $this->options['CURLOPT_FOLLOWLOCATION'] = 0;
3548
        }
3549
 
3550
        // Limit the protocols to HTTP and HTTPS.
3551
        if (defined('CURLOPT_PROTOCOLS')) {
3552
            $this->options['CURLOPT_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);
3553
            $this->options['CURLOPT_REDIR_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);
3554
        }
3555
 
3556
        // Set options.
3557
        foreach($this->options as $name => $val) {
3558
            if ($name === 'CURLOPT_FOLLOWLOCATION') {
3559
                // All the redirects are emulated at PHP level.
3560
                curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 0);
3561
                continue;
3562
            }
3563
            $name = constant($name);
3564
            curl_setopt($curl, $name, $val);
3565
        }
3566
 
3567
        return $curl;
3568
    }
3569
 
3570
    /**
3571
     * Download multiple files in parallel
3572
     *
3573
     * Calls {@link multi()} with specific download headers
3574
     *
3575
     * <code>
3576
     * $c = new curl();
3577
     * $file1 = fopen('a', 'wb');
3578
     * $file2 = fopen('b', 'wb');
3579
     * $c->download(array(
3580
     *     array('url'=>'http://localhost/', 'file'=>$file1),
3581
     *     array('url'=>'http://localhost/20/', 'file'=>$file2)
3582
     * ));
3583
     * fclose($file1);
3584
     * fclose($file2);
3585
     * </code>
3586
     *
3587
     * or
3588
     *
3589
     * <code>
3590
     * $c = new curl();
3591
     * $c->download(array(
3592
     *              array('url'=>'http://localhost/', 'filepath'=>'/tmp/file1.tmp'),
3593
     *              array('url'=>'http://localhost/20/', 'filepath'=>'/tmp/file2.tmp')
3594
     *              ));
3595
     * </code>
3596
     *
3597
     * @param array $requests An array of files to request {
3598
     *                  url => url to download the file [required]
3599
     *                  file => file handler, or
3600
     *                  filepath => file path
3601
     * }
3602
     * If 'file' and 'filepath' parameters are both specified in one request, the
3603
     * open file handle in the 'file' parameter will take precedence and 'filepath'
3604
     * will be ignored.
3605
     *
3606
     * @param array $options An array of options to set
3607
     * @return array An array of results
3608
     */
3609
    public function download($requests, $options = array()) {
3610
        $options['RETURNTRANSFER'] = false;
3611
        return $this->multi($requests, $options);
3612
    }
3613
 
3614
    /**
3615
     * Returns the current curl security helper.
3616
     *
3617
     * @return \core\files\curl_security_helper instance.
3618
     */
3619
    public function get_security() {
3620
        return $this->securityhelper;
3621
    }
3622
 
3623
    /**
3624
     * Sets the curl security helper.
3625
     *
3626
     * @param \core\files\curl_security_helper $securityobject instance/subclass of the base curl_security_helper class.
3627
     * @return bool true if the security helper could be set, false otherwise.
3628
     */
3629
    public function set_security($securityobject) {
3630
        if ($securityobject instanceof \core\files\curl_security_helper) {
3631
            $this->securityhelper = $securityobject;
3632
            return true;
3633
        }
3634
        return false;
3635
    }
3636
 
3637
    /**
3638
     * Multi HTTP Requests
3639
     * This function could run multi-requests in parallel.
3640
     *
3641
     * @param array $requests An array of files to request
3642
     * @param array $options An array of options to set
3643
     * @return array An array of results
3644
     */
3645
    protected function multi($requests, $options = array()) {
3646
        $count   = count($requests);
3647
        $handles = array();
3648
        $results = array();
3649
        $main    = curl_multi_init();
3650
        for ($i = 0; $i < $count; $i++) {
3651
            if (!empty($requests[$i]['filepath']) and empty($requests[$i]['file'])) {
3652
                // open file
3653
                $requests[$i]['file'] = fopen($requests[$i]['filepath'], 'w');
3654
                $requests[$i]['auto-handle'] = true;
3655
            }
3656
            foreach($requests[$i] as $n=>$v) {
3657
                $options[$n] = $v;
3658
            }
3659
            $handles[$i] = curl_init($requests[$i]['url']);
3660
            $this->apply_opt($handles[$i], $options);
3661
            curl_multi_add_handle($main, $handles[$i]);
3662
        }
3663
        $running = 0;
3664
        do {
3665
            curl_multi_exec($main, $running);
3666
        } while($running > 0);
3667
        for ($i = 0; $i < $count; $i++) {
3668
            if (!empty($options['CURLOPT_RETURNTRANSFER'])) {
3669
                $results[] = true;
3670
            } else {
3671
                $results[] = curl_multi_getcontent($handles[$i]);
3672
            }
3673
            curl_multi_remove_handle($main, $handles[$i]);
3674
        }
3675
        curl_multi_close($main);
3676
 
3677
        for ($i = 0; $i < $count; $i++) {
3678
            if (!empty($requests[$i]['filepath']) and !empty($requests[$i]['auto-handle'])) {
3679
                // close file handler if file is opened in this function
3680
                fclose($requests[$i]['file']);
3681
            }
3682
        }
3683
        return $results;
3684
    }
3685
 
3686
    /**
3687
     * Helper function to reset the request state vars.
3688
     *
3689
     * @return void.
3690
     */
3691
    protected function reset_request_state_vars() {
3692
        $this->info             = array();
3693
        $this->error            = '';
3694
        $this->errno            = 0;
3695
        $this->response         = array();
3696
        $this->rawresponse      = array();
3697
        $this->responsefinished = false;
3698
    }
3699
 
3700
    /**
3701
     * For use only in unit tests - we can pre-set the next curl response.
3702
     * This is useful for unit testing APIs that call external systems.
3703
     * @param string $response
3704
     */
3705
    public static function mock_response($response) {
3706
        if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
3707
            array_push(self::$mockresponses, $response);
3708
        } else {
3709
            throw new coding_exception('mock_response function is only available for unit tests.');
3710
        }
3711
    }
3712
 
3713
    /**
3714
     * check_securityhelper_blocklist.
3715
     * Checks whether the given URL is blocked by checking both plugin's security helpers
3716
     * and core curl security helper or any curl security helper that passed to curl class constructor.
3717
     * If ignoresecurity is set to true, skip checking and consider the url is not blocked.
3718
     * This augments all installed plugin's security helpers if there is any.
3719
     *
3720
     * @param string $url the url to check.
3721
     * @return ?string - an error message if URL is blocked or null if URL is not blocked.
3722
     */
3723
    protected function check_securityhelper_blocklist(string $url): ?string {
3724
 
3725
        // If curl security is not enabled, do not proceed.
3726
        if ($this->ignoresecurity) {
3727
            return null;
3728
        }
3729
 
3730
        // Augment all installed plugin's security helpers if there is any.
3731
        // The plugin's function has to be defined as plugintype_pluginname_curl_security_helper in pluginname/lib.php.
3732
        $plugintypes = get_plugins_with_function('curl_security_helper');
3733
 
3734
        // If any of the security helper's function returns true, treat as URL is blocked.
3735
        foreach ($plugintypes as $plugins) {
3736
            foreach ($plugins as $pluginfunction) {
3737
                // Get curl security helper object from plugin lib.php.
3738
                $pluginsecurityhelper = $pluginfunction();
3739
                if ($pluginsecurityhelper instanceof \core\files\curl_security_helper_base) {
3740
                    if ($pluginsecurityhelper->url_is_blocked($url)) {
3741
                        $this->error = $pluginsecurityhelper->get_blocked_url_string();
3742
                        return $this->error;
3743
                    }
3744
                }
3745
            }
3746
        }
3747
 
3748
        // Check if the URL is blocked in core curl_security_helper or
3749
        // curl security helper that passed to curl class constructor.
3750
        if ($this->securityhelper->url_is_blocked($url)) {
3751
            $this->error = $this->securityhelper->get_blocked_url_string();
3752
            return $this->error;
3753
        }
3754
 
3755
        return null;
3756
    }
3757
 
3758
    /**
3759
     * Single HTTP Request
3760
     *
3761
     * @param string $url The URL to request
3762
     * @param array $options
3763
     * @return string
3764
     */
3765
    protected function request($url, $options = array()) {
3766
        // Reset here so that the data is valid when result returned from cache, or if we return due to a blocked URL hit.
3767
        $this->reset_request_state_vars();
3768
 
3769
        if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {
3770
            $mockresponse = array_pop(self::$mockresponses);
3771
            if ($mockresponse !== null) {
3772
                $this->info = [ 'http_code' => 200 ];
3773
                return $mockresponse;
3774
            }
3775
        }
3776
 
3777
        if (empty($this->emulateredirects)) {
3778
            // Just in case someone had tried to explicitly disable emulated redirects in legacy code.
3779
            debugging('Attempting to disable emulated redirects has no effect any more!', DEBUG_DEVELOPER);
3780
        }
3781
 
3782
        $urlisblocked = $this->check_securityhelper_blocklist($url);
3783
        if (!is_null($urlisblocked)) {
3784
            $this->trigger_url_blocked_event($url, $urlisblocked);
3785
            return $urlisblocked;
3786
        }
3787
 
3788
        // Set the URL as a curl option.
3789
        $this->setopt(array('CURLOPT_URL' => $url));
3790
 
3791
        // Create curl instance.
3792
        $curl = curl_init();
3793
 
3794
        $this->apply_opt($curl, $options);
3795
        if ($this->cache && $ret = $this->cache->get($this->options)) {
3796
            return $ret;
3797
        }
3798
 
3799
        $ret = curl_exec($curl);
3800
        $this->info  = curl_getinfo($curl);
3801
        $this->error = curl_error($curl);
3802
        $this->errno = curl_errno($curl);
3803
        // Note: $this->response and $this->rawresponse are filled by $hits->formatHeader callback.
3804
 
3805
        if (intval($this->info['redirect_count']) > 0) {
3806
            // For security reasons we do not allow the cURL handle to follow redirects on its own.
3807
            // See setting CURLOPT_FOLLOWLOCATION in {@see self::apply_opt()} method.
3808
            throw new coding_exception('Internal cURL handle should never follow redirects on its own!',
3809
                'Reported number of redirects: ' . $this->info['redirect_count']);
3810
        }
3811
 
3812
        if ($this->options['CURLOPT_FOLLOWLOCATION'] && $this->info['http_code'] != 200) {
3813
            $redirects = 0;
3814
 
3815
            while($redirects <= $this->options['CURLOPT_MAXREDIRS']) {
3816
 
3817
                if ($this->info['http_code'] == 301) {
3818
                    // Moved Permanently - repeat the same request on new URL.
3819
 
3820
                } else if ($this->info['http_code'] == 302) {
3821
                    // Found - the standard redirect - repeat the same request on new URL.
3822
 
3823
                } else if ($this->info['http_code'] == 303) {
3824
                    // 303 See Other - repeat only if GET, do not bother with POSTs.
3825
                    if (empty($this->options['CURLOPT_HTTPGET'])) {
3826
                        break;
3827
                    }
3828
 
3829
                } else if ($this->info['http_code'] == 307) {
3830
                    // Temporary Redirect - must repeat using the same request type.
3831
 
3832
                } else if ($this->info['http_code'] == 308) {
3833
                    // Permanent Redirect - must repeat using the same request type.
3834
 
3835
                } else {
3836
                    // Some other http code means do not retry!
3837
                    break;
3838
                }
3839
 
3840
                $redirects++;
3841
 
3842
                $currenturl = $redirecturl ?? $url;
3843
                $redirecturl = null;
3844
                if (isset($this->info['redirect_url'])) {
3845
                    if (preg_match('|^https?://|i', $this->info['redirect_url'])) {
3846
                        $redirecturl = $this->info['redirect_url'];
3847
                    } else {
3848
                        // Emulate CURLOPT_REDIR_PROTOCOLS behaviour which we have set to (CURLPROTO_HTTP | CURLPROTO_HTTPS) only.
3849
                        $this->errno = CURLE_UNSUPPORTED_PROTOCOL;
3850
                        $this->error = 'Redirect to a URL with unsuported protocol: ' . $this->info['redirect_url'];
3851
                        curl_close($curl);
3852
                        return $this->error;
3853
                    }
3854
                }
3855
                if (!$redirecturl) {
3856
                    foreach ($this->response as $k => $v) {
3857
                        if (strtolower($k) === 'location') {
3858
                            $redirecturl = $v;
3859
                            break;
3860
                        }
3861
                    }
3862
                    if (preg_match('|^https?://|i', $redirecturl)) {
3863
                        // Great, this is the correct location format!
3864
 
3865
                    } else if ($redirecturl) {
3866
                        $current = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);
3867
                        if (strpos($redirecturl, '/') === 0) {
3868
                            // Relative to server root - just guess.
3869
                            $pos = strpos('/', $current, 8);
3870
                            if ($pos === false) {
3871
                                $redirecturl = $current.$redirecturl;
3872
                            } else {
3873
                                $redirecturl = substr($current, 0, $pos).$redirecturl;
3874
                            }
3875
                        } else {
3876
                            // Relative to current script.
3877
                            $redirecturl = dirname($current).'/'.$redirecturl;
3878
                        }
3879
                    }
3880
                }
3881
 
3882
                $urlisblocked = $this->check_securityhelper_blocklist($redirecturl);
3883
                if (!is_null($urlisblocked)) {
3884
                    $this->reset_request_state_vars();
3885
                    curl_close($curl);
3886
                    $this->trigger_url_blocked_event($redirecturl, $urlisblocked, true);
3887
                    return $urlisblocked;
3888
                }
3889
 
3890
                // If the response body is written to a seekable stream resource, reset the stream pointer to avoid
3891
                // appending multiple response bodies to the same resource.
3892
                if (!empty($this->options['CURLOPT_FILE'])) {
3893
                    $streammetadata = stream_get_meta_data($this->options['CURLOPT_FILE']);
3894
                    if ($streammetadata['seekable']) {
3895
                        ftruncate($this->options['CURLOPT_FILE'], 0);
3896
                        rewind($this->options['CURLOPT_FILE']);
3897
                    }
3898
                }
3899
 
3900
                curl_setopt($curl, CURLOPT_URL, $redirecturl);
3901
 
11 efrain 3902
                // If CURLOPT_UNRESTRICTED_AUTH is empty/false, don't send credentials to other hosts.
3903
                // Ref: https://curl.se/libcurl/c/CURLOPT_UNRESTRICTED_AUTH.html.
3904
                $isdifferenthost = parse_url($currenturl)['host'] !== parse_url($redirecturl)['host'];
3905
                $sendauthentication = !empty($this->options['CURLOPT_UNRESTRICTED_AUTH']);
3906
                if ($isdifferenthost && !$sendauthentication) {
1 efrain 3907
                    curl_setopt($curl, CURLOPT_HTTPAUTH, null);
3908
                    curl_setopt($curl, CURLOPT_USERPWD, null);
11 efrain 3909
                    // Check whether the CURLOPT_HTTPHEADER is specified.
3910
                    if (!empty($this->options['CURLOPT_HTTPHEADER'])) {
3911
                        // Remove the "Authorization:" header, if any.
3912
                        $headerredirect = array_filter(
3913
                            $this->options['CURLOPT_HTTPHEADER'],
3914
                            fn($header) => strpos($header, 'Authorization:') === false
3915
                        );
3916
                        curl_setopt($curl, CURLOPT_HTTPHEADER, $headerredirect);
3917
                    }
1 efrain 3918
                }
3919
 
3920
                $ret = curl_exec($curl);
3921
 
3922
                $this->info  = curl_getinfo($curl);
3923
                $this->error = curl_error($curl);
3924
                $this->errno = curl_errno($curl);
3925
 
3926
                $this->info['redirect_count'] = $redirects;
3927
 
3928
                if ($this->info['http_code'] === 200) {
3929
                    // Finally this is what we wanted.
3930
                    break;
3931
                }
3932
                if ($this->errno != CURLE_OK) {
3933
                    // Something wrong is going on.
3934
                    break;
3935
                }
3936
            }
3937
            if ($redirects > $this->options['CURLOPT_MAXREDIRS']) {
3938
                $this->errno = CURLE_TOO_MANY_REDIRECTS;
3939
                $this->error = 'Maximum ('.$this->options['CURLOPT_MAXREDIRS'].') redirects followed';
3940
            }
3941
        }
3942
 
3943
        if ($this->cache) {
3944
            $this->cache->set($this->options, $ret);
3945
        }
3946
 
3947
        if ($this->debug) {
3948
            echo '<h1>Return Data</h1>';
3949
            var_dump($ret);
3950
            echo '<h1>Info</h1>';
3951
            var_dump($this->info);
3952
            echo '<h1>Error</h1>';
3953
            var_dump($this->error);
3954
        }
3955
 
3956
        curl_close($curl);
3957
 
3958
        if (empty($this->error)) {
3959
            return $ret;
3960
        } else {
3961
            return $this->error;
3962
            // exception is not ajax friendly
3963
            //throw new moodle_exception($this->error, 'curl');
3964
        }
3965
    }
3966
 
3967
    /**
3968
     * Trigger url_blocked event
3969
     *
3970
     * @param string $url      The URL to request
3971
     * @param string $reason   Reason for blocking
3972
     * @param bool   $redirect true if it was a redirect
3973
     */
3974
    private function trigger_url_blocked_event($url, $reason, $redirect = false): void {
3975
        $params = [
3976
            'url' => $url,
3977
            'reason' => $reason,
3978
            'redirect' => $redirect,
3979
        ];
3980
        $event = core\event\url_blocked::create(['other' => $params]);
3981
        $event->trigger();
3982
    }
3983
 
3984
    /**
3985
     * HTTP HEAD method
3986
     *
3987
     * @see request()
3988
     *
3989
     * @param string $url
3990
     * @param array $options
3991
     * @return string
3992
     */
3993
    public function head($url, $options = array()) {
3994
        $options['CURLOPT_HTTPGET'] = 0;
3995
        $options['CURLOPT_HEADER']  = 1;
3996
        $options['CURLOPT_NOBODY']  = 1;
3997
        return $this->request($url, $options);
3998
    }
3999
 
4000
    /**
4001
     * HTTP PATCH method
4002
     *
4003
     * @param string $url
4004
     * @param array|string $params
4005
     * @param array $options
4006
     * @return string
4007
     */
4008
    public function patch($url, $params = '', $options = array()) {
4009
        $options['CURLOPT_CUSTOMREQUEST'] = 'PATCH';
4010
        if (is_array($params)) {
4011
            $this->_tmp_file_post_params = array();
4012
            foreach ($params as $key => $value) {
4013
                if ($value instanceof stored_file) {
4014
                    $value->add_to_curl_request($this, $key);
4015
                } else {
4016
                    $this->_tmp_file_post_params[$key] = $value;
4017
                }
4018
            }
4019
            $options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params;
4020
            unset($this->_tmp_file_post_params);
4021
        } else {
4022
            // The variable $params is the raw post data.
4023
            $options['CURLOPT_POSTFIELDS'] = $params;
4024
        }
4025
        return $this->request($url, $options);
4026
    }
4027
 
4028
    /**
4029
     * HTTP POST method
4030
     *
4031
     * @param string $url
4032
     * @param array|string $params
4033
     * @param array $options
4034
     * @return string
4035
     */
4036
    public function post($url, $params = '', $options = array()) {
4037
        $options['CURLOPT_POST']       = 1;
4038
        if (is_array($params)) {
4039
            $this->_tmp_file_post_params = array();
4040
            foreach ($params as $key => $value) {
4041
                if ($value instanceof stored_file) {
4042
                    $value->add_to_curl_request($this, $key);
4043
                } else {
4044
                    $this->_tmp_file_post_params[$key] = $value;
4045
                }
4046
            }
4047
            $options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params;
4048
            unset($this->_tmp_file_post_params);
4049
        } else {
4050
            // $params is the raw post data
4051
            $options['CURLOPT_POSTFIELDS'] = $params;
4052
        }
4053
        return $this->request($url, $options);
4054
    }
4055
 
4056
    /**
4057
     * HTTP GET method
4058
     *
4059
     * @param string $url
4060
     * @param ?array $params
4061
     * @param array $options
4062
     * @return string
4063
     */
4064
    public function get($url, $params = array(), $options = array()) {
4065
        $options['CURLOPT_HTTPGET'] = 1;
4066
 
4067
        if (!empty($params)) {
4068
            $url .= (stripos($url, '?') !== false) ? '&' : '?';
4069
            $url .= http_build_query($params, '', '&');
4070
        }
4071
        return $this->request($url, $options);
4072
    }
4073
 
4074
    /**
4075
     * Downloads one file and writes it to the specified file handler
4076
     *
4077
     * <code>
4078
     * $c = new curl();
4079
     * $file = fopen('savepath', 'w');
4080
     * $result = $c->download_one('http://localhost/', null,
4081
     *   array('file' => $file, 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
4082
     * fclose($file);
4083
     * $download_info = $c->get_info();
4084
     * if ($result === true) {
4085
     *   // file downloaded successfully
4086
     * } else {
4087
     *   $error_text = $result;
4088
     *   $error_code = $c->get_errno();
4089
     * }
4090
     * </code>
4091
     *
4092
     * <code>
4093
     * $c = new curl();
4094
     * $result = $c->download_one('http://localhost/', null,
4095
     *   array('filepath' => 'savepath', 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));
4096
     * // ... see above, no need to close handle and remove file if unsuccessful
4097
     * </code>
4098
     *
4099
     * @param string $url
4100
     * @param array|null $params key-value pairs to be added to $url as query string
4101
     * @param array $options request options. Must include either 'file' or 'filepath'
4102
     * @return bool|string true on success or error string on failure
4103
     */
4104
    public function download_one($url, $params, $options = array()) {
4105
        $options['CURLOPT_HTTPGET'] = 1;
4106
        if (!empty($params)) {
4107
            $url .= (stripos($url, '?') !== false) ? '&' : '?';
4108
            $url .= http_build_query($params, '', '&');
4109
        }
4110
        if (!empty($options['filepath']) && empty($options['file'])) {
4111
            // open file
4112
            if (!($options['file'] = fopen($options['filepath'], 'w'))) {
4113
                $this->errno = 100;
4114
                return get_string('cannotwritefile', 'error', $options['filepath']);
4115
            }
4116
            $filepath = $options['filepath'];
4117
        }
4118
        unset($options['filepath']);
4119
        $result = $this->request($url, $options);
4120
        if (isset($filepath)) {
4121
            fclose($options['file']);
4122
            if ($result !== true) {
4123
                unlink($filepath);
4124
            }
4125
        }
4126
        return $result;
4127
    }
4128
 
4129
    /**
4130
     * HTTP PUT method
4131
     *
4132
     * @param string $url
4133
     * @param array $params
4134
     * @param array $options
4135
     * @return ?string
4136
     */
4137
    public function put($url, $params = array(), $options = array()) {
4138
        $file = '';
4139
        $fp = false;
4140
        if (isset($params['file'])) {
4141
            $file = $params['file'];
4142
            if (is_file($file)) {
4143
                $fp   = fopen($file, 'r');
4144
                $size = filesize($file);
4145
                $options['CURLOPT_PUT']        = 1;
4146
                $options['CURLOPT_INFILESIZE'] = $size;
4147
                $options['CURLOPT_INFILE']     = $fp;
4148
            } else {
4149
                return null;
4150
            }
4151
            if (!isset($this->options['CURLOPT_USERPWD'])) {
4152
                $this->setopt(array('CURLOPT_USERPWD' => 'anonymous: noreply@moodle.org'));
4153
            }
4154
        } else {
4155
            $options['CURLOPT_CUSTOMREQUEST'] = 'PUT';
4156
            $options['CURLOPT_POSTFIELDS'] = $params;
4157
        }
4158
 
4159
        $ret = $this->request($url, $options);
4160
        if ($fp !== false) {
4161
            fclose($fp);
4162
        }
4163
        return $ret;
4164
    }
4165
 
4166
    /**
4167
     * HTTP DELETE method
4168
     *
4169
     * @param string $url
4170
     * @param array $param
4171
     * @param array $options
4172
     * @return string
4173
     */
4174
    public function delete($url, $param = array(), $options = array()) {
4175
        $options['CURLOPT_CUSTOMREQUEST'] = 'DELETE';
4176
        if (!isset($options['CURLOPT_USERPWD'])) {
4177
            $options['CURLOPT_USERPWD'] = 'anonymous: noreply@moodle.org';
4178
        }
4179
        $ret = $this->request($url, $options);
4180
        return $ret;
4181
    }
4182
 
4183
    /**
4184
     * HTTP TRACE method
4185
     *
4186
     * @param string $url
4187
     * @param array $options
4188
     * @return string
4189
     */
4190
    public function trace($url, $options = array()) {
4191
        $options['CURLOPT_CUSTOMREQUEST'] = 'TRACE';
4192
        $ret = $this->request($url, $options);
4193
        return $ret;
4194
    }
4195
 
4196
    /**
4197
     * HTTP OPTIONS method
4198
     *
4199
     * @param string $url
4200
     * @param array $options
4201
     * @return string
4202
     */
4203
    public function options($url, $options = array()) {
4204
        $options['CURLOPT_CUSTOMREQUEST'] = 'OPTIONS';
4205
        $ret = $this->request($url, $options);
4206
        return $ret;
4207
    }
4208
 
4209
    /**
4210
     * Get curl information
4211
     *
4212
     * @return array
4213
     */
4214
    public function get_info() {
4215
        return $this->info;
4216
    }
4217
 
4218
    /**
4219
     * Get curl error code
4220
     *
4221
     * @return int
4222
     */
4223
    public function get_errno() {
4224
        return $this->errno;
4225
    }
4226
 
4227
    /**
4228
     * When using a proxy, an additional HTTP response code may appear at
4229
     * the start of the header. For example, when using https over a proxy
4230
     * there may be 'HTTP/1.0 200 Connection Established'. Other codes are
4231
     * also possible and some may come with their own headers.
4232
     *
4233
     * If using the return value containing all headers, this function can be
4234
     * called to remove unwanted doubles.
4235
     *
4236
     * Note that it is not possible to distinguish this situation from valid
4237
     * data unless you know the actual response part (below the headers)
4238
     * will not be included in this string, or else will not 'look like' HTTP
4239
     * headers. As a result it is not safe to call this function for general
4240
     * data.
4241
     *
4242
     * @param string $input Input HTTP response
4243
     * @return string HTTP response with additional headers stripped if any
4244
     */
4245
    public static function strip_double_headers($input) {
4246
        // I have tried to make this regular expression as specific as possible
4247
        // to avoid any case where it does weird stuff if you happen to put
4248
        // HTTP/1.1 200 at the start of any line in your RSS file. This should
4249
        // also make it faster because it can abandon regex processing as soon
4250
        // as it hits something that doesn't look like an http header. The
4251
        // header definition is taken from RFC 822, except I didn't support
4252
        // folding which is never used in practice.
4253
        $crlf = "\r\n";
4254
        return preg_replace(
4255
                // HTTP version and status code (ignore value of code).
4256
                '~^HTTP/[1-9](\.[0-9])?.*' . $crlf .
4257
                // Header name: character between 33 and 126 decimal, except colon.
4258
                // Colon. Header value: any character except \r and \n. CRLF.
4259
                '(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .
4260
                // Headers are terminated by another CRLF (blank line).
4261
                $crlf .
4262
                // Second HTTP status code, this time must be 200.
4263
                '(HTTP/[1-9](\.[0-9])? 200)~', '$2', $input);
4264
    }
4265
}
4266
 
4267
/**
4268
 * This class is used by cURL class, use case:
4269
 *
4270
 * <code>
4271
 * $CFG->repositorycacheexpire = 120;
4272
 * $CFG->curlcache = 120;
4273
 *
4274
 * $c = new curl(array('cache'=>true), 'module_cache'=>'repository');
4275
 * $ret = $c->get('http://www.google.com');
4276
 * </code>
4277
 *
4278
 * @package   core_files
4279
 * @copyright Dongsheng Cai <dongsheng@moodle.com>
4280
 * @license   http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4281
 */
4282
class curl_cache {
4283
    /** @var string Path to cache directory */
4284
    public $dir = '';
4285
 
4286
    /** @var int the repositorycacheexpire config value. */
4287
    private $ttl;
4288
 
4289
    /**
4290
     * Constructor
4291
     *
4292
     * @global stdClass $CFG
4293
     * @param string $module which module is using curl_cache
4294
     */
4295
    public function __construct($module = 'repository') {
4296
        global $CFG;
4297
        if (!empty($module)) {
4298
            $this->dir = $CFG->cachedir.'/'.$module.'/';
4299
        } else {
4300
            $this->dir = $CFG->cachedir.'/misc/';
4301
        }
4302
        if (!file_exists($this->dir)) {
4303
            mkdir($this->dir, $CFG->directorypermissions, true);
4304
        }
4305
        if ($module == 'repository') {
4306
            if (empty($CFG->repositorycacheexpire)) {
4307
                $CFG->repositorycacheexpire = 120;
4308
            }
4309
            $this->ttl = $CFG->repositorycacheexpire;
4310
        } else {
4311
            if (empty($CFG->curlcache)) {
4312
                $CFG->curlcache = 120;
4313
            }
4314
            $this->ttl = $CFG->curlcache;
4315
        }
4316
    }
4317
 
4318
    /**
4319
     * Get cached value
4320
     *
4321
     * @global stdClass $CFG
4322
     * @global stdClass $USER
4323
     * @param mixed $param
4324
     * @return bool|string
4325
     */
4326
    public function get($param) {
4327
        global $CFG, $USER;
4328
        $this->cleanup($this->ttl);
4329
        $filename = 'u'.$USER->id.'_'.md5(serialize($param));
4330
        if(file_exists($this->dir.$filename)) {
4331
            $lasttime = filemtime($this->dir.$filename);
4332
            if (time()-$lasttime > $this->ttl) {
4333
                return false;
4334
            } else {
4335
                $fp = fopen($this->dir.$filename, 'r');
4336
                $size = filesize($this->dir.$filename);
4337
                $content = fread($fp, $size);
4338
                return unserialize($content);
4339
            }
4340
        }
4341
        return false;
4342
    }
4343
 
4344
    /**
4345
     * Set cache value
4346
     *
4347
     * @global object $CFG
4348
     * @global object $USER
4349
     * @param mixed $param
4350
     * @param mixed $val
4351
     */
4352
    public function set($param, $val) {
4353
        global $CFG, $USER;
4354
        $filename = 'u'.$USER->id.'_'.md5(serialize($param));
4355
        $fp = fopen($this->dir.$filename, 'w');
4356
        fwrite($fp, serialize($val));
4357
        fclose($fp);
4358
        @chmod($this->dir.$filename, $CFG->filepermissions);
4359
    }
4360
 
4361
    /**
4362
     * Remove cache files
4363
     *
4364
     * @param int $expire The number of seconds before expiry
4365
     */
4366
    public function cleanup($expire) {
4367
        if ($dir = opendir($this->dir)) {
4368
            while (false !== ($file = readdir($dir))) {
4369
                if(!is_dir($file) && $file != '.' && $file != '..') {
4370
                    $lasttime = @filemtime($this->dir.$file);
4371
                    if (time() - $lasttime > $expire) {
4372
                        @unlink($this->dir.$file);
4373
                    }
4374
                }
4375
            }
4376
            closedir($dir);
4377
        }
4378
    }
4379
    /**
4380
     * delete current user's cache file
4381
     *
4382
     * @global object $CFG
4383
     * @global object $USER
4384
     */
4385
    public function refresh() {
4386
        global $CFG, $USER;
4387
        if ($dir = opendir($this->dir)) {
4388
            while (false !== ($file = readdir($dir))) {
4389
                if (!is_dir($file) && $file != '.' && $file != '..') {
4390
                    if (strpos($file, 'u'.$USER->id.'_') !== false) {
4391
                        @unlink($this->dir.$file);
4392
                    }
4393
                }
4394
            }
4395
        }
4396
    }
4397
}
4398
 
4399
/**
4400
 * This function delegates file serving to individual plugins
4401
 *
4402
 * @param string $relativepath
4403
 * @param bool $forcedownload
4404
 * @param null|string $preview the preview mode, defaults to serving the original file
4405
 * @param boolean $offline If offline is requested - don't serve a redirect to an external file, return a file suitable for viewing
4406
 *                         offline (e.g. mobile app).
4407
 * @param bool $embed Whether this file will be served embed into an iframe.
4408
 * @todo MDL-31088 file serving improments
4409
 */
4410
function file_pluginfile($relativepath, $forcedownload, $preview = null, $offline = false, $embed = false) {
4411
    global $DB, $CFG, $USER, $OUTPUT;
4412
    // relative path must start with '/'
4413
    if (!$relativepath) {
4414
        throw new \moodle_exception('invalidargorconf');
4415
    } else if ($relativepath[0] != '/') {
4416
        throw new \moodle_exception('pathdoesnotstartslash');
4417
    }
4418
 
4419
    // extract relative path components
4420
    $args = explode('/', ltrim($relativepath, '/'));
4421
 
4422
    if (count($args) < 3) { // always at least context, component and filearea
4423
        throw new \moodle_exception('invalidarguments');
4424
    }
4425
 
4426
    $contextid = (int)array_shift($args);
4427
    $component = clean_param(array_shift($args), PARAM_COMPONENT);
4428
    $filearea  = clean_param(array_shift($args), PARAM_AREA);
4429
 
4430
    list($context, $course, $cm) = get_context_info_array($contextid);
4431
 
4432
    $fs = get_file_storage();
4433
 
4434
    $sendfileoptions = ['preview' => $preview, 'offline' => $offline, 'embed' => $embed];
4435
 
4436
    // ========================================================================================================================
4437
    if ($component === 'blog') {
4438
        // Blog file serving
4439
        if ($context->contextlevel != CONTEXT_SYSTEM) {
4440
            send_file_not_found();
4441
        }
4442
        if ($filearea !== 'attachment' and $filearea !== 'post') {
4443
            send_file_not_found();
4444
        }
4445
 
4446
        if (empty($CFG->enableblogs)) {
4447
            throw new \moodle_exception('siteblogdisable', 'blog');
4448
        }
4449
 
4450
        $entryid = (int)array_shift($args);
4451
        if (!$entry = $DB->get_record('post', array('module'=>'blog', 'id'=>$entryid))) {
4452
            send_file_not_found();
4453
        }
4454
        if ($CFG->bloglevel < BLOG_GLOBAL_LEVEL) {
4455
            require_login();
4456
            if (isguestuser()) {
4457
                throw new \moodle_exception('noguest');
4458
            }
4459
            if ($CFG->bloglevel == BLOG_USER_LEVEL) {
4460
                if ($USER->id != $entry->userid) {
4461
                    send_file_not_found();
4462
                }
4463
            }
4464
        }
4465
 
4466
        if ($entry->publishstate === 'public') {
4467
            if ($CFG->forcelogin) {
4468
                require_login();
4469
            }
4470
 
4471
        } else if ($entry->publishstate === 'site') {
4472
            require_login();
4473
            //ok
4474
        } else if ($entry->publishstate === 'draft') {
4475
            require_login();
4476
            if ($USER->id != $entry->userid) {
4477
                send_file_not_found();
4478
            }
4479
        }
4480
 
4481
        $filename = array_pop($args);
4482
        $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4483
 
4484
        if (!$file = $fs->get_file($context->id, $component, $filearea, $entryid, $filepath, $filename) or $file->is_directory()) {
4485
            send_file_not_found();
4486
        }
4487
 
4488
        send_stored_file($file, 10*60, 0, true, $sendfileoptions); // download MUST be forced - security!
4489
 
4490
    // ========================================================================================================================
4491
    } else if ($component === 'grade') {
4492
 
4493
        require_once($CFG->libdir . '/grade/constants.php');
4494
 
4495
        if (($filearea === 'outcome' or $filearea === 'scale') and $context->contextlevel == CONTEXT_SYSTEM) {
4496
            // Global gradebook files
4497
            if ($CFG->forcelogin) {
4498
                require_login();
4499
            }
4500
 
4501
            $fullpath = "/$context->id/$component/$filearea/".implode('/', $args);
4502
 
4503
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4504
                send_file_not_found();
4505
            }
4506
 
4507
            \core\session\manager::write_close(); // Unlock session during file serving.
4508
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4509
 
4510
        } else if ($filearea == GRADE_FEEDBACK_FILEAREA || $filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {
4511
            if ($context->contextlevel != CONTEXT_MODULE) {
4512
                send_file_not_found();
4513
            }
4514
 
4515
            require_login($course, false);
4516
 
4517
            $gradeid = (int) array_shift($args);
4518
            $filename = array_pop($args);
4519
            if ($filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {
4520
                $grade = $DB->get_record('grade_grades_history', ['id' => $gradeid]);
4521
            } else {
4522
                $grade = $DB->get_record('grade_grades', ['id' => $gradeid]);
4523
            }
4524
 
4525
            if (!$grade) {
4526
                send_file_not_found();
4527
            }
4528
 
4529
            $iscurrentuser = $USER->id == $grade->userid;
4530
 
4531
            if (!$iscurrentuser) {
4532
                $coursecontext = context_course::instance($course->id);
4533
                if (!has_capability('moodle/grade:viewall', $coursecontext)) {
4534
                    send_file_not_found();
4535
                }
4536
            }
4537
 
4538
            $fullpath = "/$context->id/$component/$filearea/$gradeid/$filename";
4539
 
4540
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4541
                send_file_not_found();
4542
            }
4543
 
4544
            \core\session\manager::write_close(); // Unlock session during file serving.
4545
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4546
        } else {
4547
            send_file_not_found();
4548
        }
4549
 
4550
    // ========================================================================================================================
4551
    } else if ($component === 'tag') {
4552
        if ($filearea === 'description' and $context->contextlevel == CONTEXT_SYSTEM) {
4553
 
4554
            // All tag descriptions are going to be public but we still need to respect forcelogin
4555
            if ($CFG->forcelogin) {
4556
                require_login();
4557
            }
4558
 
4559
            $fullpath = "/$context->id/tag/description/".implode('/', $args);
4560
 
4561
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
4562
                send_file_not_found();
4563
            }
4564
 
4565
            \core\session\manager::write_close(); // Unlock session during file serving.
4566
            send_stored_file($file, 60*60, 0, true, $sendfileoptions);
4567
 
4568
        } else {
4569
            send_file_not_found();
4570
        }
4571
    // ========================================================================================================================
4572
    } else if ($component === 'badges') {
4573
        require_once($CFG->libdir . '/badgeslib.php');
4574
 
4575
        $badgeid = (int)array_shift($args);
4576
        $badge = new badge($badgeid);
4577
        $filename = array_pop($args);
4578
 
4579
        if ($filearea === 'badgeimage') {
4580
            if ($filename !== 'f1' && $filename !== 'f2' && $filename !== 'f3') {
4581
                send_file_not_found();
4582
            }
4583
            if (!$file = $fs->get_file($context->id, 'badges', 'badgeimage', $badge->id, '/', $filename.'.png')) {
4584
                send_file_not_found();
4585
            }
4586
 
4587
            \core\session\manager::write_close();
4588
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4589
        } else if ($filearea === 'userbadge'  and $context->contextlevel == CONTEXT_USER) {
4590
            if (!$file = $fs->get_file($context->id, 'badges', 'userbadge', $badge->id, '/', $filename.'.png')) {
4591
                send_file_not_found();
4592
            }
4593
 
4594
            \core\session\manager::write_close();
4595
            send_stored_file($file, 60*60, 0, true, $sendfileoptions);
4596
        }
4597
    // ========================================================================================================================
4598
    } else if ($component === 'calendar') {
4599
        if ($filearea === 'event_description'  and $context->contextlevel == CONTEXT_SYSTEM) {
4600
 
4601
            // All events here are public the one requirement is that we respect forcelogin
4602
            if ($CFG->forcelogin) {
4603
                require_login();
4604
            }
4605
 
4606
            // Get the event if from the args array
4607
            $eventid = array_shift($args);
4608
 
4609
            // Load the event from the database
4610
            if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'eventtype'=>'site'))) {
4611
                send_file_not_found();
4612
            }
4613
 
4614
            // Get the file and serve if successful
4615
            $filename = array_pop($args);
4616
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4617
            if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {
4618
                send_file_not_found();
4619
            }
4620
 
4621
            \core\session\manager::write_close(); // Unlock session during file serving.
4622
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4623
 
4624
        } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_USER) {
4625
 
4626
            // Must be logged in, if they are not then they obviously can't be this user
4627
            require_login();
4628
 
4629
            // Don't want guests here, potentially saves a DB call
4630
            if (isguestuser()) {
4631
                send_file_not_found();
4632
            }
4633
 
4634
            // Get the event if from the args array
4635
            $eventid = array_shift($args);
4636
 
4637
            // Load the event from the database - user id must match
4638
            if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'userid'=>$USER->id, 'eventtype'=>'user'))) {
4639
                send_file_not_found();
4640
            }
4641
 
4642
            // Get the file and serve if successful
4643
            $filename = array_pop($args);
4644
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4645
            if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {
4646
                send_file_not_found();
4647
            }
4648
 
4649
            \core\session\manager::write_close(); // Unlock session during file serving.
4650
            send_stored_file($file, 0, 0, true, $sendfileoptions);
4651
 
4652
        } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSECAT) {
4653
            if ($CFG->forcelogin) {
4654
                require_login();
4655
            }
4656
 
4657
            // Get category, this will also validate access.
4658
            $category = core_course_category::get($context->instanceid);
4659
 
4660
            // Get the event ID from the args array, load event.
4661
            $eventid = array_shift($args);
4662
            $event = $DB->get_record('event', [
4663
                'id' => (int) $eventid,
4664
                'eventtype' => 'category',
4665
                'categoryid' => $category->id,
4666
            ]);
4667
 
4668
            if (!$event) {
4669
                send_file_not_found();
4670
            }
4671
 
4672
            // Retrieve file from storage, and serve.
4673
            $filename = array_pop($args);
4674
            $filepath = $args ? '/' . implode('/', $args) .'/' : '/';
4675
            $file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename);
4676
            if (!$file || $file->is_directory()) {
4677
                send_file_not_found();
4678
            }
4679
 
4680
            // Unlock session during file serving.
4681
            \core\session\manager::write_close();
4682
            send_stored_file($file, HOURSECS, 0, $forcedownload, $sendfileoptions);
4683
        } else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSE) {
4684
 
4685
            // Respect forcelogin and require login unless this is the site.... it probably
4686
            // should NEVER be the site
4687
            if ($CFG->forcelogin || $course->id != SITEID) {
4688
                require_login($course);
4689
            }
4690
 
4691
            // Must be able to at least view the course. This does not apply to the front page.
4692
            if ($course->id != SITEID && (!is_enrolled($context)) && (!is_viewing($context))) {
4693
                //TODO: hmm, do we really want to block guests here?
4694
                send_file_not_found();
4695
            }
4696
 
4697
            // Get the event id
4698
            $eventid = array_shift($args);
4699
 
4700
            // Load the event from the database we need to check whether it is
4701
            // a) valid course event
4702
            // b) a group event
4703
            // Group events use the course context (there is no group context)
4704
            if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'courseid'=>$course->id))) {
4705
                send_file_not_found();
4706
            }
4707
 
4708
            // If its a group event require either membership of view all groups capability
4709
            if ($event->eventtype === 'group') {
4710
                if (!has_capability('moodle/site:accessallgroups', $context) && !groups_is_member($event->groupid, $USER->id)) {
4711
                    send_file_not_found();
4712
                }
4713
            } else if ($event->eventtype === 'course' || $event->eventtype === 'site') {
4714
                // Ok. Please note that the event type 'site' still uses a course context.
4715
            } else {
4716
                // Some other type.
4717
                send_file_not_found();
4718
            }
4719
 
4720
            // If we get this far we can serve the file
4721
            $filename = array_pop($args);
4722
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4723
            if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {
4724
                send_file_not_found();
4725
            }
4726
 
4727
            \core\session\manager::write_close(); // Unlock session during file serving.
4728
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4729
 
4730
        } else {
4731
            send_file_not_found();
4732
        }
4733
 
4734
    // ========================================================================================================================
4735
    } else if ($component === 'user') {
4736
        if ($filearea === 'icon' and $context->contextlevel == CONTEXT_USER) {
4737
            if (count($args) == 1) {
4738
                $themename = theme_config::DEFAULT_THEME;
4739
                $filename = array_shift($args);
4740
            } else {
4741
                $themename = array_shift($args);
4742
                $filename = array_shift($args);
4743
            }
4744
 
4745
            // fix file name automatically
4746
            if ($filename !== 'f1' and $filename !== 'f2' and $filename !== 'f3') {
4747
                $filename = 'f1';
4748
            }
4749
 
4750
            if ((!empty($CFG->forcelogin) and !isloggedin()) ||
4751
                    (!empty($CFG->forceloginforprofileimage) && (!isloggedin() || isguestuser()))) {
4752
                // protect images if login required and not logged in;
4753
                // also if login is required for profile images and is not logged in or guest
4754
                // do not use require_login() because it is expensive and not suitable here anyway
4755
                $theme = theme_config::load($themename);
4756
                redirect($theme->image_url('u/'.$filename, 'moodle')); // intentionally not cached
4757
            }
4758
 
4759
            if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.png')) {
4760
                if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.jpg')) {
4761
                    if ($filename === 'f3') {
4762
                        // f3 512x512px was introduced in 2.3, there might be only the smaller version.
4763
                        if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.png')) {
4764
                            $file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.jpg');
4765
                        }
4766
                    }
4767
                }
4768
            }
4769
            if (!$file) {
4770
                // bad reference - try to prevent future retries as hard as possible!
4771
                if ($user = $DB->get_record('user', array('id'=>$context->instanceid), 'id, picture')) {
4772
                    if ($user->picture > 0) {
4773
                        $DB->set_field('user', 'picture', 0, array('id'=>$user->id));
4774
                    }
4775
                }
4776
                // no redirect here because it is not cached
4777
                $theme = theme_config::load($themename);
4778
                $imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle', null);
4779
                send_file($imagefile, basename($imagefile), 60*60*24*14);
4780
            }
4781
 
4782
            $options = $sendfileoptions;
4783
            if (empty($CFG->forcelogin) && empty($CFG->forceloginforprofileimage)) {
4784
                // Profile images should be cache-able by both browsers and proxies according
4785
                // to $CFG->forcelogin and $CFG->forceloginforprofileimage.
4786
                $options['cacheability'] = 'public';
4787
            }
4788
            send_stored_file($file, 60*60*24*365, 0, false, $options); // enable long caching, there are many images on each page
4789
 
4790
        } else if ($filearea === 'private' and $context->contextlevel == CONTEXT_USER) {
4791
            require_login();
4792
 
4793
            if (isguestuser()) {
4794
                send_file_not_found();
4795
            }
4796
 
4797
            if ($USER->id !== $context->instanceid) {
4798
                send_file_not_found();
4799
            }
4800
 
4801
            $filename = array_pop($args);
4802
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4803
            if (!$file = $fs->get_file($context->id, $component, $filearea, 0, $filepath, $filename) or $file->is_directory()) {
4804
                send_file_not_found();
4805
            }
4806
 
4807
            \core\session\manager::write_close(); // Unlock session during file serving.
4808
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4809
 
4810
        } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_USER) {
4811
 
4812
            if ($CFG->forcelogin) {
4813
                require_login();
4814
            }
4815
 
4816
            $userid = $context->instanceid;
4817
 
4818
            if (!empty($CFG->forceloginforprofiles)) {
4819
                require_once("{$CFG->dirroot}/user/lib.php");
4820
 
4821
                require_login();
4822
 
4823
                // Verify the current user is able to view the profile of the supplied user anywhere.
4824
                $user = core_user::get_user($userid);
4825
                if (!user_can_view_profile($user, null, $context)) {
4826
                    send_file_not_found();
4827
                }
4828
            }
4829
 
4830
            $filename = array_pop($args);
4831
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4832
            if (!$file = $fs->get_file($context->id, $component, $filearea, 0, $filepath, $filename) or $file->is_directory()) {
4833
                send_file_not_found();
4834
            }
4835
 
4836
            \core\session\manager::write_close(); // Unlock session during file serving.
4837
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4838
 
4839
        } else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_COURSE) {
4840
            $userid = (int)array_shift($args);
4841
            $usercontext = context_user::instance($userid);
4842
 
4843
            if ($CFG->forcelogin) {
4844
                require_login();
4845
            }
4846
 
4847
            if (!empty($CFG->forceloginforprofiles)) {
4848
                require_once("{$CFG->dirroot}/user/lib.php");
4849
 
4850
                require_login();
4851
 
4852
                // Verify the current user is able to view the profile of the supplied user in current course.
4853
                $user = core_user::get_user($userid);
4854
                if (!user_can_view_profile($user, $course, $usercontext)) {
4855
                    send_file_not_found();
4856
                }
4857
            }
4858
 
4859
            $filename = array_pop($args);
4860
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4861
            if (!$file = $fs->get_file($usercontext->id, 'user', 'profile', 0, $filepath, $filename) or $file->is_directory()) {
4862
                send_file_not_found();
4863
            }
4864
 
4865
            \core\session\manager::write_close(); // Unlock session during file serving.
4866
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4867
 
4868
        } else if ($filearea === 'backup' and $context->contextlevel == CONTEXT_USER) {
4869
            require_login();
4870
 
4871
            if (isguestuser()) {
4872
                send_file_not_found();
4873
            }
4874
            $userid = $context->instanceid;
4875
 
4876
            if ($USER->id != $userid) {
4877
                send_file_not_found();
4878
            }
4879
 
4880
            $filename = array_pop($args);
4881
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4882
            if (!$file = $fs->get_file($context->id, 'user', 'backup', 0, $filepath, $filename) or $file->is_directory()) {
4883
                send_file_not_found();
4884
            }
4885
 
4886
            \core\session\manager::write_close(); // Unlock session during file serving.
4887
            send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!
4888
 
4889
        } else {
4890
            send_file_not_found();
4891
        }
4892
 
4893
    // ========================================================================================================================
4894
    } else if ($component === 'coursecat') {
4895
        if ($context->contextlevel != CONTEXT_COURSECAT) {
4896
            send_file_not_found();
4897
        }
4898
 
4899
        if ($filearea === 'description') {
4900
            if ($CFG->forcelogin) {
4901
                // no login necessary - unless login forced everywhere
4902
                require_login();
4903
            }
4904
 
4905
            // Check if user can view this category.
4906
            if (!core_course_category::get($context->instanceid, IGNORE_MISSING)) {
4907
                send_file_not_found();
4908
            }
4909
 
4910
            $filename = array_pop($args);
4911
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4912
            if (!$file = $fs->get_file($context->id, 'coursecat', 'description', 0, $filepath, $filename) or $file->is_directory()) {
4913
                send_file_not_found();
4914
            }
4915
 
4916
            \core\session\manager::write_close(); // Unlock session during file serving.
4917
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4918
        } else {
4919
            send_file_not_found();
4920
        }
4921
 
4922
    // ========================================================================================================================
4923
    } else if ($component === 'course') {
4924
        if ($context->contextlevel != CONTEXT_COURSE) {
4925
            send_file_not_found();
4926
        }
4927
 
4928
        if ($filearea === 'summary' || $filearea === 'overviewfiles') {
4929
            if ($CFG->forcelogin) {
4930
                require_login();
4931
            }
4932
 
4933
            $filename = array_pop($args);
4934
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4935
            if (!$file = $fs->get_file($context->id, 'course', $filearea, 0, $filepath, $filename) or $file->is_directory()) {
4936
                send_file_not_found();
4937
            }
4938
 
4939
            \core\session\manager::write_close(); // Unlock session during file serving.
4940
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4941
 
4942
        } else if ($filearea === 'section') {
4943
            if ($CFG->forcelogin) {
4944
                require_login($course);
4945
            } else if ($course->id != SITEID) {
4946
                require_login($course);
4947
            }
4948
 
4949
            $sectionid = (int)array_shift($args);
4950
 
4951
            if (!$section = $DB->get_record('course_sections', array('id'=>$sectionid, 'course'=>$course->id))) {
4952
                send_file_not_found();
4953
            }
4954
 
4955
            $filename = array_pop($args);
4956
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
4957
            if (!$file = $fs->get_file($context->id, 'course', 'section', $sectionid, $filepath, $filename) or $file->is_directory()) {
4958
                send_file_not_found();
4959
            }
4960
 
4961
            \core\session\manager::write_close(); // Unlock session during file serving.
4962
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
4963
 
4964
        } else if ($filearea === 'generated') {
4965
            if ($CFG->forcelogin) {
4966
                require_login($course);
4967
            } else if ($course->id != SITEID) {
4968
                require_login($course);
4969
            }
4970
 
4971
            $svg = $OUTPUT->get_generated_svg_for_id($course->id);
4972
 
4973
            \core\session\manager::write_close(); // Unlock session during file serving.
4974
            send_file($svg, 'course.svg', 60 * 60, 0, true, $forcedownload);
4975
 
4976
        } else {
4977
            send_file_not_found();
4978
        }
4979
 
4980
    } else if ($component === 'cohort') {
4981
 
4982
        $cohortid = (int)array_shift($args);
4983
        $cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);
4984
        $cohortcontext = context::instance_by_id($cohort->contextid);
4985
 
4986
        // The context in the file URL must be either cohort context or context of the course underneath the cohort's context.
4987
        if ($context->id != $cohort->contextid &&
4988
            ($context->contextlevel != CONTEXT_COURSE || !in_array($cohort->contextid, $context->get_parent_context_ids()))) {
4989
            send_file_not_found();
4990
        }
4991
 
4992
        // User is able to access cohort if they have view cap on cohort level or
4993
        // the cohort is visible and they have view cap on course level.
4994
        $canview = has_capability('moodle/cohort:view', $cohortcontext) ||
4995
                ($cohort->visible && has_capability('moodle/cohort:view', $context));
4996
 
4997
        if ($filearea === 'description' && $canview) {
4998
            $filename = array_pop($args);
4999
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5000
            if (($file = $fs->get_file($cohortcontext->id, 'cohort', 'description', $cohort->id, $filepath, $filename))
5001
                    && !$file->is_directory()) {
5002
                \core\session\manager::write_close(); // Unlock session during file serving.
5003
                send_stored_file($file, 60 * 60, 0, $forcedownload, $sendfileoptions);
5004
            }
5005
        }
5006
 
5007
        send_file_not_found();
5008
 
5009
    } else if ($component === 'group') {
5010
        if ($context->contextlevel != CONTEXT_COURSE) {
5011
            send_file_not_found();
5012
        }
5013
 
5014
        require_course_login($course, true, null, false);
5015
 
5016
        $groupid = (int)array_shift($args);
5017
 
5018
        $group = $DB->get_record('groups', array('id'=>$groupid, 'courseid'=>$course->id), '*', MUST_EXIST);
5019
        if (($course->groupmodeforce and $course->groupmode == SEPARATEGROUPS) and !has_capability('moodle/site:accessallgroups', $context) and !groups_is_member($group->id, $USER->id)) {
5020
            // do not allow access to separate group info if not member or teacher
5021
            send_file_not_found();
5022
        }
5023
 
5024
        if ($filearea === 'description') {
5025
 
5026
            require_login($course);
5027
 
5028
            $filename = array_pop($args);
5029
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5030
            if (!$file = $fs->get_file($context->id, 'group', 'description', $group->id, $filepath, $filename) or $file->is_directory()) {
5031
                send_file_not_found();
5032
            }
5033
 
5034
            \core\session\manager::write_close(); // Unlock session during file serving.
5035
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5036
 
5037
        } else if ($filearea === 'icon') {
5038
            $filename = array_pop($args);
5039
 
5040
            if ($filename !== 'f1' and $filename !== 'f2') {
5041
                send_file_not_found();
5042
            }
5043
            if (!$file = $fs->get_file($context->id, 'group', 'icon', $group->id, '/', $filename.'.png')) {
5044
                if (!$file = $fs->get_file($context->id, 'group', 'icon', $group->id, '/', $filename.'.jpg')) {
5045
                    send_file_not_found();
5046
                }
5047
            }
5048
 
5049
            \core\session\manager::write_close(); // Unlock session during file serving.
5050
            send_stored_file($file, 60*60, 0, false, $sendfileoptions);
5051
 
5052
        } else if ($filearea === 'generated') {
5053
            if ($CFG->forcelogin) {
5054
                require_login($course);
5055
            } else if ($course->id != SITEID) {
5056
                require_login($course);
5057
            }
5058
 
5059
            $svg = $OUTPUT->get_generated_svg_for_id($group->id);
5060
 
5061
            \core\session\manager::write_close(); // Unlock session during file serving.
5062
            send_file($svg, 'group.svg', 60 * 60, 0, true, $forcedownload);
5063
 
5064
        } else {
5065
            send_file_not_found();
5066
        }
5067
 
5068
    } else if ($component === 'grouping') {
5069
        if ($context->contextlevel != CONTEXT_COURSE) {
5070
            send_file_not_found();
5071
        }
5072
 
5073
        require_login($course);
5074
 
5075
        $groupingid = (int)array_shift($args);
5076
 
5077
        // note: everybody has access to grouping desc images for now
5078
        if ($filearea === 'description') {
5079
 
5080
            $filename = array_pop($args);
5081
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5082
            if (!$file = $fs->get_file($context->id, 'grouping', 'description', $groupingid, $filepath, $filename) or $file->is_directory()) {
5083
                send_file_not_found();
5084
            }
5085
 
5086
            \core\session\manager::write_close(); // Unlock session during file serving.
5087
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5088
 
5089
        } else {
5090
            send_file_not_found();
5091
        }
5092
 
5093
    // ========================================================================================================================
5094
    } else if ($component === 'backup') {
5095
        if ($filearea === 'course' and $context->contextlevel == CONTEXT_COURSE) {
5096
            require_login($course);
5097
            require_capability('moodle/backup:downloadfile', $context);
5098
 
5099
            $filename = array_pop($args);
5100
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5101
            if (!$file = $fs->get_file($context->id, 'backup', 'course', 0, $filepath, $filename) or $file->is_directory()) {
5102
                send_file_not_found();
5103
            }
5104
 
5105
            \core\session\manager::write_close(); // Unlock session during file serving.
5106
            send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions);
5107
 
5108
        } else if ($filearea === 'section' and $context->contextlevel == CONTEXT_COURSE) {
5109
            require_login($course);
5110
            require_capability('moodle/backup:downloadfile', $context);
5111
 
5112
            $sectionid = (int)array_shift($args);
5113
 
5114
            $filename = array_pop($args);
5115
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5116
            if (!$file = $fs->get_file($context->id, 'backup', 'section', $sectionid, $filepath, $filename) or $file->is_directory()) {
5117
                send_file_not_found();
5118
            }
5119
 
5120
            \core\session\manager::write_close();
5121
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5122
 
5123
        } else if ($filearea === 'activity' and $context->contextlevel == CONTEXT_MODULE) {
5124
            require_login($course, false, $cm);
5125
            require_capability('moodle/backup:downloadfile', $context);
5126
 
5127
            $filename = array_pop($args);
5128
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5129
            if (!$file = $fs->get_file($context->id, 'backup', 'activity', 0, $filepath, $filename) or $file->is_directory()) {
5130
                send_file_not_found();
5131
            }
5132
 
5133
            \core\session\manager::write_close();
5134
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5135
 
5136
        } else if ($filearea === 'automated' and $context->contextlevel == CONTEXT_COURSE) {
5137
            // Backup files that were generated by the automated backup systems.
5138
 
5139
            require_login($course);
5140
            require_capability('moodle/backup:downloadfile', $context);
5141
            require_capability('moodle/restore:userinfo', $context);
5142
 
5143
            $filename = array_pop($args);
5144
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5145
            if (!$file = $fs->get_file($context->id, 'backup', 'automated', 0, $filepath, $filename) or $file->is_directory()) {
5146
                send_file_not_found();
5147
            }
5148
 
5149
            \core\session\manager::write_close(); // Unlock session during file serving.
5150
            send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions);
5151
 
5152
        } else {
5153
            send_file_not_found();
5154
        }
5155
 
5156
    // ========================================================================================================================
5157
    } else if ($component === 'question') {
5158
        require_once($CFG->libdir . '/questionlib.php');
5159
        question_pluginfile($course, $context, 'question', $filearea, $args, $forcedownload, $sendfileoptions);
5160
        send_file_not_found();
5161
 
5162
    // ========================================================================================================================
5163
    } else if ($component === 'grading') {
5164
        if ($filearea === 'description') {
5165
            // files embedded into the form definition description
5166
 
5167
            if ($context->contextlevel == CONTEXT_SYSTEM) {
5168
                require_login();
5169
 
5170
            } else if ($context->contextlevel >= CONTEXT_COURSE) {
5171
                require_login($course, false, $cm);
5172
 
5173
            } else {
5174
                send_file_not_found();
5175
            }
5176
 
5177
            $formid = (int)array_shift($args);
5178
 
5179
            $sql = "SELECT ga.id
5180
                FROM {grading_areas} ga
5181
                JOIN {grading_definitions} gd ON (gd.areaid = ga.id)
5182
                WHERE gd.id = ? AND ga.contextid = ?";
5183
            $areaid = $DB->get_field_sql($sql, array($formid, $context->id), IGNORE_MISSING);
5184
 
5185
            if (!$areaid) {
5186
                send_file_not_found();
5187
            }
5188
 
5189
            $fullpath = "/$context->id/$component/$filearea/$formid/".implode('/', $args);
5190
 
5191
            if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {
5192
                send_file_not_found();
5193
            }
5194
 
5195
            \core\session\manager::write_close(); // Unlock session during file serving.
5196
            send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);
5197
        }
5198
    } else if ($component === 'contentbank') {
5199
        if ($filearea != 'public' || isguestuser()) {
5200
            send_file_not_found();
5201
        }
5202
 
5203
        if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {
5204
            require_login();
5205
        } else if ($context->contextlevel == CONTEXT_COURSE) {
5206
            require_login($course);
5207
        } else {
5208
            send_file_not_found();
5209
        }
5210
 
5211
        $componentargs = fullclone($args);
5212
        $itemid = (int)array_shift($args);
5213
        $filename = array_pop($args);
5214
        $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5215
 
5216
        \core\session\manager::write_close(); // Unlock session during file serving.
5217
 
5218
        $contenttype = $DB->get_field('contentbank_content', 'contenttype', ['id' => $itemid]);
5219
        if (component_class_callback("\\{$contenttype}\\contenttype", 'pluginfile',
5220
                [$course, null, $context, $filearea, $componentargs, $forcedownload, $sendfileoptions], false) === false) {
5221
 
5222
            if (!$file = $fs->get_file($context->id, $component, $filearea, $itemid, $filepath, $filename) or
5223
 
5224
                $file->is_directory()) {
5225
                send_file_not_found();
5226
 
5227
            } else {
5228
                send_stored_file($file, 0, 0, true, $sendfileoptions); // Must force download - security!
5229
            }
5230
        }
5231
    } else if (strpos($component, 'mod_') === 0) {
5232
        $modname = substr($component, 4);
5233
        if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {
5234
            send_file_not_found();
5235
        }
5236
        require_once("$CFG->dirroot/mod/$modname/lib.php");
5237
 
5238
        if ($context->contextlevel == CONTEXT_MODULE) {
5239
            if ($cm->modname !== $modname) {
5240
                // somebody tries to gain illegal access, cm type must match the component!
5241
                send_file_not_found();
5242
            }
5243
        }
5244
 
5245
        if ($filearea === 'intro') {
5246
            if (!plugin_supports('mod', $modname, FEATURE_MOD_INTRO, true)) {
5247
                send_file_not_found();
5248
            }
5249
 
5250
            // Require login to the course first (without login to the module).
5251
            require_course_login($course, true);
5252
 
5253
            // Now check if module is available OR it is restricted but the intro is shown on the course page.
5254
            $cminfo = cm_info::create($cm);
5255
            if (!$cminfo->uservisible) {
5256
                if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {
5257
                    // Module intro is not visible on the course page and module is not available, show access error.
5258
                    require_course_login($course, true, $cminfo);
5259
                }
5260
            }
5261
 
5262
            // all users may access it
5263
            $filename = array_pop($args);
5264
            $filepath = $args ? '/'.implode('/', $args).'/' : '/';
5265
            if (!$file = $fs->get_file($context->id, 'mod_'.$modname, 'intro', 0, $filepath, $filename) or $file->is_directory()) {
5266
                send_file_not_found();
5267
            }
5268
 
5269
            // finally send the file
5270
            send_stored_file($file, null, 0, false, $sendfileoptions);
5271
        }
5272
 
5273
        $filefunction = $component.'_pluginfile';
5274
        $filefunctionold = $modname.'_pluginfile';
5275
        if (function_exists($filefunction)) {
5276
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5277
            $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5278
        } else if (function_exists($filefunctionold)) {
5279
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5280
            $filefunctionold($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5281
        }
5282
 
5283
        send_file_not_found();
5284
 
5285
    // ========================================================================================================================
5286
    } else if (strpos($component, 'block_') === 0) {
5287
        $blockname = substr($component, 6);
5288
        // note: no more class methods in blocks please, that is ....
5289
        if (!file_exists("$CFG->dirroot/blocks/$blockname/lib.php")) {
5290
            send_file_not_found();
5291
        }
5292
        require_once("$CFG->dirroot/blocks/$blockname/lib.php");
5293
 
5294
        if ($context->contextlevel == CONTEXT_BLOCK) {
5295
            $birecord = $DB->get_record('block_instances', array('id'=>$context->instanceid), '*',MUST_EXIST);
5296
            if ($birecord->blockname !== $blockname) {
5297
                // somebody tries to gain illegal access, cm type must match the component!
5298
                send_file_not_found();
5299
            }
5300
 
5301
            if ($context->get_course_context(false)) {
5302
                // If block is in course context, then check if user has capability to access course.
5303
                require_course_login($course);
5304
            } else if ($CFG->forcelogin) {
5305
                // If user is logged out, bp record will not be visible, even if the user would have access if logged in.
5306
                require_login();
5307
            }
5308
 
5309
            $bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));
5310
            // User can't access file, if block is hidden or doesn't have block:view capability
5311
            if (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {
5312
                 send_file_not_found();
5313
            }
5314
        } else {
5315
            $birecord = null;
5316
        }
5317
 
5318
        $filefunction = $component.'_pluginfile';
5319
        if (function_exists($filefunction)) {
5320
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5321
            $filefunction($course, $birecord, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5322
        }
5323
 
5324
        send_file_not_found();
5325
 
5326
    // ========================================================================================================================
5327
    } else if (strpos($component, '_') === false) {
5328
        // all core subsystems have to be specified above, no more guessing here!
5329
        send_file_not_found();
5330
 
5331
    } else {
5332
        // try to serve general plugin file in arbitrary context
5333
        $dir = core_component::get_component_directory($component);
5334
        if (!file_exists("$dir/lib.php")) {
5335
            send_file_not_found();
5336
        }
5337
        include_once("$dir/lib.php");
5338
 
5339
        $filefunction = $component.'_pluginfile';
5340
        if (function_exists($filefunction)) {
5341
            // if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"
5342
            $filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);
5343
        }
5344
 
5345
        send_file_not_found();
5346
    }
5347
 
5348
}