Rev 1 | Ir a la última revisión | Autoría | Comparar con el anterior | Ultima modificación | Ver Log |
<?php// This file is part of Moodle - http://moodle.org///// Moodle is free software: you can redistribute it and/or modify// it under the terms of the GNU General Public License as published by// the Free Software Foundation, either version 3 of the License, or// (at your option) any later version.//// Moodle is distributed in the hope that it will be useful,// but WITHOUT ANY WARRANTY; without even the implied warranty of// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the// GNU General Public License for more details.//// You should have received a copy of the GNU General Public License// along with Moodle. If not, see <http://www.gnu.org/licenses/>./*** Functions for file handling.** @package core_files* @copyright 1999 onwards Martin Dougiamas (http://dougiamas.com)* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/defined('MOODLE_INTERNAL') || die();/*** BYTESERVING_BOUNDARY - string unique string constant.*/define('BYTESERVING_BOUNDARY', 's1k2o3d4a5k6s7');/*** Do not process file merging when working with draft area files.*/define('IGNORE_FILE_MERGE', -1);/*** Unlimited area size constant*/define('FILE_AREA_MAX_BYTES_UNLIMITED', -1);/*** Capacity of the draft area bucket when using the leaking bucket technique to limit the draft upload rate.*/define('DRAFT_AREA_BUCKET_CAPACITY', 50);/*** Leaking rate of the draft area bucket when using the leaking bucket technique to limit the draft upload rate.*/define('DRAFT_AREA_BUCKET_LEAK', 0.2);require_once("$CFG->libdir/filestorage/file_exceptions.php");require_once("$CFG->libdir/filestorage/file_storage.php");require_once("$CFG->libdir/filestorage/zip_packer.php");require_once("$CFG->libdir/filebrowser/file_browser.php");/*** Encodes file serving url** @deprecated use moodle_url factory methods instead** @todo MDL-31071 deprecate this function* @global stdClass $CFG* @param string $urlbase* @param string $path /filearea/itemid/dir/dir/file.exe* @param bool $forcedownload* @param bool $https https url required* @return string encoded file url*/function file_encode_url($urlbase, $path, $forcedownload=false, $https=false) {global $CFG;//TODO: deprecate thisif ($CFG->slasharguments) {$parts = explode('/', $path);$parts = array_map('rawurlencode', $parts);$path = implode('/', $parts);$return = $urlbase.$path;if ($forcedownload) {$return .= '?forcedownload=1';}} else {$path = rawurlencode($path);$return = $urlbase.'?file='.$path;if ($forcedownload) {$return .= '&forcedownload=1';}}if ($https) {$return = str_replace('http://', 'https://', $return);}return $return;}/*** Detects if area contains subdirs,* this is intended for file areas that are attached to content* migrated from 1.x where subdirs were allowed everywhere.** @param context $context* @param string $component* @param string $filearea* @param string $itemid* @return bool*/function file_area_contains_subdirs(context $context, $component, $filearea, $itemid) {global $DB;if (!isset($itemid)) {// Not initialised yet.return false;}// Detect if any directories are already present, this is necessary for content upgraded from 1.x.$select = "contextid = :contextid AND component = :component AND filearea = :filearea AND itemid = :itemid AND filepath <> '/' AND filename = '.'";$params = array('contextid'=>$context->id, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid);return $DB->record_exists_select('files', $select, $params);}/*** Prepares 'editor' formslib element from data in database** The passed $data record must contain field foobar, foobarformat and optionally foobartrust. This* function then copies the embedded files into draft area (assigning itemids automatically),* creates the form element foobar_editor and rewrites the URLs so the embedded images can be* displayed.* In your mform definition, you must have an 'editor' element called foobar_editor. Then you call* your mform's set_data() supplying the object returned by this function.** @category files* @param stdClass $data database field that holds the html text with embedded media* @param string $field the name of the database field that holds the html text with embedded media* @param array $options editor options (like maxifiles, maxbytes etc.)* @param stdClass $context context of the editor* @param string $component* @param string $filearea file area name* @param int $itemid item id, required if item exists* @return stdClass modified data object*/function file_prepare_standard_editor($data, $field, array $options, $context=null, $component=null, $filearea=null, $itemid=null) {$options = (array)$options;if (!isset($options['trusttext'])) {$options['trusttext'] = false;}if (!isset($options['forcehttps'])) {$options['forcehttps'] = false;}if (!isset($options['subdirs'])) {$options['subdirs'] = false;}if (!isset($options['maxfiles'])) {$options['maxfiles'] = 0; // no files by default}if (!isset($options['noclean'])) {$options['noclean'] = false;}//sanity check for passed context. This function doesn't expect $option['context'] to be set//But this function is called before creating editor hence, this is one of the best places to check//if context is used properly. This check notify developer that they missed passing context to editor.if (isset($context) && !isset($options['context'])) {//if $context is not null then make sure $option['context'] is also set.debugging('Context for editor is not set in editoroptions. Hence editor will not respect editor filters', DEBUG_DEVELOPER);} else if (isset($options['context']) && isset($context)) {//If both are passed then they should be equal.if ($options['context']->id != $context->id) {$exceptionmsg = 'Editor context ['.$options['context']->id.'] is not equal to passed context ['.$context->id.']';throw new coding_exception($exceptionmsg);}}if (is_null($itemid) or is_null($context)) {$contextid = null;$itemid = null;if (!isset($data)) {$data = new stdClass();}if (!isset($data->{$field})) {$data->{$field} = '';}if (!isset($data->{$field.'format'})) {$data->{$field.'format'} = editors_get_preferred_format();}if (!$options['noclean']) {if ($data->{$field.'format'} != FORMAT_MARKDOWN) {$data->{$field} = clean_text($data->{$field}, $data->{$field . 'format'});}}} else {if ($options['trusttext']) {// noclean ignored if trusttext enabledif (!isset($data->{$field.'trust'})) {$data->{$field.'trust'} = 0;}$data = trusttext_pre_edit($data, $field, $context);} else {if (!$options['noclean']) {// We do not have a way to sanitise Markdown texts,// luckily editors for this format should not have XSS problems.if ($data->{$field.'format'} != FORMAT_MARKDOWN) {$data->{$field} = clean_text($data->{$field}, $data->{$field.'format'});}}}$contextid = $context->id;}if ($options['maxfiles'] != 0) {$draftid_editor = file_get_submitted_draft_itemid($field);$currenttext = file_prepare_draft_area($draftid_editor, $contextid, $component, $filearea, $itemid, $options, $data->{$field});$data->{$field.'_editor'} = array('text'=>$currenttext, 'format'=>$data->{$field.'format'}, 'itemid'=>$draftid_editor);} else {$data->{$field.'_editor'} = array('text'=>$data->{$field}, 'format'=>$data->{$field.'format'}, 'itemid'=>0);}return $data;}/*** Prepares the content of the 'editor' form element with embedded media files to be saved in database** This function moves files from draft area to the destination area and* encodes URLs to the draft files so they can be safely saved into DB. The* form has to contain the 'editor' element named foobar_editor, where 'foobar'* is the name of the database field to hold the wysiwyg editor content. The* editor data comes as an array with text, format and itemid properties. This* function automatically adds $data properties foobar, foobarformat and* foobartrust, where foobar has URL to embedded files encoded.** @category files* @param stdClass $data raw data submitted by the form* @param string $field name of the database field containing the html with embedded media files* @param array $options editor options (trusttext, subdirs, maxfiles, maxbytes etc.)* @param stdClass $context context, required for existing data* @param string $component file component* @param string $filearea file area name* @param int $itemid item id, required if item exists* @return stdClass modified data object*/function file_postupdate_standard_editor($data, $field, array $options, $context, $component=null, $filearea=null, $itemid=null) {$options = (array)$options;if (!isset($options['trusttext'])) {$options['trusttext'] = false;}if (!isset($options['forcehttps'])) {$options['forcehttps'] = false;}if (!isset($options['subdirs'])) {$options['subdirs'] = false;}if (!isset($options['maxfiles'])) {$options['maxfiles'] = 0; // no files by default}if (!isset($options['maxbytes'])) {$options['maxbytes'] = 0; // unlimited}if (!isset($options['removeorphaneddrafts'])) {$options['removeorphaneddrafts'] = false; // Don't remove orphaned draft files by default.}if ($options['trusttext']) {$data->{$field.'trust'} = trusttext_trusted($context);} else {$data->{$field.'trust'} = 0;}$editor = $data->{$field.'_editor'};if ($options['maxfiles'] == 0 or is_null($filearea) or is_null($itemid) or empty($editor['itemid'])) {$data->{$field} = $editor['text'];} else {// Clean the user drafts area of any files not referenced in the editor text.if ($options['removeorphaneddrafts']) {file_remove_editor_orphaned_files($editor);}$data->{$field} = file_save_draft_area_files($editor['itemid'], $context->id, $component, $filearea, $itemid, $options, $editor['text'], $options['forcehttps']);}$data->{$field.'format'} = $editor['format'];return $data;}/*** Saves text and files modified by Editor formslib element** @category files* @param stdClass $data $database entry field* @param string $field name of data field* @param array $options various options* @param stdClass $context context - must already exist* @param string $component* @param string $filearea file area name* @param int $itemid must already exist, usually means data is in db* @return stdClass modified data obejct*/function file_prepare_standard_filemanager($data, $field, array $options, $context=null, $component=null, $filearea=null, $itemid=null) {$options = (array)$options;if (!isset($options['subdirs'])) {$options['subdirs'] = false;}if (is_null($itemid) or is_null($context)) {$itemid = null;$contextid = null;} else {$contextid = $context->id;}$draftid_editor = file_get_submitted_draft_itemid($field.'_filemanager');file_prepare_draft_area($draftid_editor, $contextid, $component, $filearea, $itemid, $options);$data->{$field.'_filemanager'} = $draftid_editor;return $data;}/*** Saves files modified by File manager formslib element** @todo MDL-31073 review this function* @category files* @param stdClass $data $database entry field* @param string $field name of data field* @param array $options various options* @param stdClass $context context - must already exist* @param string $component* @param string $filearea file area name* @param int $itemid must already exist, usually means data is in db* @return stdClass modified data obejct*/function file_postupdate_standard_filemanager($data, $field, array $options, $context, $component, $filearea, $itemid) {$options = (array)$options;if (!isset($options['subdirs'])) {$options['subdirs'] = false;}if (!isset($options['maxfiles'])) {$options['maxfiles'] = -1; // unlimited}if (!isset($options['maxbytes'])) {$options['maxbytes'] = 0; // unlimited}if (empty($data->{$field.'_filemanager'})) {$data->$field = '';} else {file_save_draft_area_files($data->{$field.'_filemanager'}, $context->id, $component, $filearea, $itemid, $options);$fs = get_file_storage();if ($fs->get_area_files($context->id, $component, $filearea, $itemid)) {$data->$field = '1'; // TODO: this is an ugly hack (skodak)} else {$data->$field = '';}}return $data;}/*** Generate a draft itemid** @category files* @global moodle_database $DB* @global stdClass $USER* @return int a random but available draft itemid that can be used to create a new draft* file area.*/function file_get_unused_draft_itemid() {global $DB, $USER;if (isguestuser() or !isloggedin()) {// guests and not-logged-in users can not be allowed to upload anything!!!!!!throw new \moodle_exception('noguest');}$contextid = context_user::instance($USER->id)->id;$fs = get_file_storage();$draftitemid = rand(1, 999999999);while ($files = $fs->get_area_files($contextid, 'user', 'draft', $draftitemid)) {$draftitemid = rand(1, 999999999);}return $draftitemid;}/*** Initialise a draft file area from a real one by copying the files. A draft* area will be created if one does not already exist. Normally you should* get $draftitemid by calling file_get_submitted_draft_itemid('elementname');** @category files* @global stdClass $CFG* @global stdClass $USER* @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.* @param int $contextid This parameter and the next two identify the file area to copy files from.* @param string $component* @param string $filearea helps indentify the file area.* @param int $itemid helps identify the file area. Can be null if there are no files yet.* @param array $options text and file options ('subdirs'=>false, 'forcehttps'=>false)* @param string $text some html content that needs to have embedded links rewritten to point to the draft area.* @return string|null returns string if $text was passed in, the rewritten $text is returned. Otherwise NULL.*/function file_prepare_draft_area(&$draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null) {global $CFG, $USER;$options = (array)$options;if (!isset($options['subdirs'])) {$options['subdirs'] = false;}if (!isset($options['forcehttps'])) {$options['forcehttps'] = false;}$usercontext = context_user::instance($USER->id);$fs = get_file_storage();if (empty($draftitemid)) {// create a new area and copy existing files into$draftitemid = file_get_unused_draft_itemid();$file_record = array('contextid'=>$usercontext->id, 'component'=>'user', 'filearea'=>'draft', 'itemid'=>$draftitemid);if (!is_null($itemid) and $files = $fs->get_area_files($contextid, $component, $filearea, $itemid)) {foreach ($files as $file) {if ($file->is_directory() and $file->get_filepath() === '/') {// we need a way to mark the age of each draft area,// by not copying the root dir we force it to be created automatically with current timestampcontinue;}if (!$options['subdirs'] and ($file->is_directory() or $file->get_filepath() !== '/')) {continue;}$draftfile = $fs->create_file_from_storedfile($file_record, $file);// XXX: This is a hack for file manager (MDL-28666)// File manager needs to know the original file information before copying// to draft area, so we append these information in mdl_files.source field// {@link file_storage::search_references()}// {@link file_storage::search_references_count()}$sourcefield = $file->get_source();$newsourcefield = new stdClass;$newsourcefield->source = $sourcefield;$original = new stdClass;$original->contextid = $contextid;$original->component = $component;$original->filearea = $filearea;$original->itemid = $itemid;$original->filename = $file->get_filename();$original->filepath = $file->get_filepath();$newsourcefield->original = file_storage::pack_reference($original);$draftfile->set_source(serialize($newsourcefield));// End of file manager hack}}if (!is_null($text)) {// at this point there should not be any draftfile links yet,// because this is a new text from database that should still contain the @@pluginfile@@ links// this happens when developers forget to post process the text$text = str_replace("\"$CFG->wwwroot/draftfile.php", "\"$CFG->wwwroot/brokenfile.php#", $text);}} else {// nothing to do}if (is_null($text)) {return null;}// relink embedded files - editor can not handle @@PLUGINFILE@@ !return file_rewrite_pluginfile_urls($text, 'draftfile.php', $usercontext->id, 'user', 'draft', $draftitemid, $options);}/*** Convert encoded URLs in $text from the @@PLUGINFILE@@/... form to an actual URL.* Passing a new option reverse = true in the $options var will make the function to convert actual URLs in $text to encoded URLs* in the @@PLUGINFILE@@ form.** @param string $text The content that may contain ULRs in need of rewriting.* @param string $file The script that should be used to serve these files. pluginfile.php, draftfile.php, etc.* @param int $contextid This parameter and the next two identify the file area to use.* @param string $component* @param string $filearea helps identify the file area.* @param ?int $itemid helps identify the file area.* @param array $options* bool $options.forcehttps Force the user of https* bool $options.reverse Reverse the behaviour of the function* mixed $options.includetoken Use a token for authentication. True for current user, int value for other user id.* string The processed text.*/function file_rewrite_pluginfile_urls($text, $file, $contextid, $component, $filearea, $itemid, array $options=null) {global $CFG, $USER;$options = (array)$options;if (!isset($options['forcehttps'])) {$options['forcehttps'] = false;}$baseurl = "{$CFG->wwwroot}/{$file}";if (!empty($options['includetoken'])) {$userid = $options['includetoken'] === true ? $USER->id : $options['includetoken'];$token = get_user_key('core_files', $userid);$finalfile = basename($file);$tokenfile = "token{$finalfile}";$file = substr($file, 0, strlen($file) - strlen($finalfile)) . $tokenfile;$baseurl = "{$CFG->wwwroot}/{$file}";if (!$CFG->slasharguments) {$baseurl .= "?token={$token}&file=";} else {$baseurl .= "/{$token}";}}$baseurl .= "/{$contextid}/{$component}/{$filearea}/";if ($itemid !== null) {$baseurl .= "$itemid/";}if ($options['forcehttps']) {$baseurl = str_replace('http://', 'https://', $baseurl);}if (!empty($options['reverse'])) {return str_replace($baseurl, '@@PLUGINFILE@@/', $text ?? '');} else {return str_replace('@@PLUGINFILE@@/', $baseurl, $text ?? '');}}/*** Returns information about files in a draft area.** @global stdClass $CFG* @global stdClass $USER* @param int $draftitemid the draft area item id.* @param string $filepath path to the directory from which the information have to be retrieved.* @return array with the following entries:* 'filecount' => number of files in the draft area.* 'filesize' => total size of the files in the draft area.* 'foldercount' => number of folders in the draft area.* 'filesize_without_references' => total size of the area excluding file references.* (more information will be added as needed).*/function file_get_draft_area_info($draftitemid, $filepath = '/') {global $USER;$usercontext = context_user::instance($USER->id);return file_get_file_area_info($usercontext->id, 'user', 'draft', $draftitemid, $filepath);}/*** Returns information about files in an area.** @param int $contextid context id* @param string $component component* @param string $filearea file area name* @param int $itemid item id or all files if not specified* @param string $filepath path to the directory from which the information have to be retrieved.* @return array with the following entries:* 'filecount' => number of files in the area.* 'filesize' => total size of the files in the area.* 'foldercount' => number of folders in the area.* 'filesize_without_references' => total size of the area excluding file references.* @since Moodle 3.4*/function file_get_file_area_info($contextid, $component, $filearea, $itemid = 0, $filepath = '/') {$fs = get_file_storage();$results = array('filecount' => 0,'foldercount' => 0,'filesize' => 0,'filesize_without_references' => 0);$draftfiles = $fs->get_directory_files($contextid, $component, $filearea, $itemid, $filepath, true, true);foreach ($draftfiles as $file) {if ($file->is_directory()) {$results['foldercount'] += 1;} else {$results['filecount'] += 1;}$filesize = $file->get_filesize();$results['filesize'] += $filesize;if (!$file->is_external_file()) {$results['filesize_without_references'] += $filesize;}}return $results;}/*** Returns whether a draft area has exceeded/will exceed its size limit.** Please note that the unlimited value for $areamaxbytes is -1 {@link FILE_AREA_MAX_BYTES_UNLIMITED}, not 0.** @param int $draftitemid the draft area item id.* @param int $areamaxbytes the maximum size allowed in this draft area.* @param int $newfilesize the size that would be added to the current area.* @param bool $includereferences true to include the size of the references in the area size.* @return bool true if the area will/has exceeded its limit.* @since Moodle 2.4*/function file_is_draft_area_limit_reached($draftitemid, $areamaxbytes, $newfilesize = 0, $includereferences = false) {if ($areamaxbytes != FILE_AREA_MAX_BYTES_UNLIMITED) {$draftinfo = file_get_draft_area_info($draftitemid);$areasize = $draftinfo['filesize_without_references'];if ($includereferences) {$areasize = $draftinfo['filesize'];}if ($areasize + $newfilesize > $areamaxbytes) {return true;}}return false;}/*** Returns whether a user has reached their draft area upload rate.** @param int $userid The user id* @return bool*/function file_is_draft_areas_limit_reached(int $userid): bool {global $CFG;$capacity = $CFG->draft_area_bucket_capacity ?? DRAFT_AREA_BUCKET_CAPACITY;$leak = $CFG->draft_area_bucket_leak ?? DRAFT_AREA_BUCKET_LEAK;$since = time() - floor($capacity / $leak); // The items that were in the bucket before this time are already leaked by now.// We are going to be a bit generous to the user when using the leaky bucket// algorithm below. We are going to assume that the bucket is empty at $since.// We have to do an assumption here unless we really want to get ALL user's draft// items without any limit and put all of them in the leaking bucket.// I decided to favour performance over accuracy here.$fs = get_file_storage();$items = $fs->get_user_draft_items($userid, $since);$items = array_reverse($items); // So that the items are sorted based on time in the ascending direction.// We only need to store the time that each element in the bucket is going to leak. So $bucket is array of leaking times.$bucket = [];foreach ($items as $item) {$now = $item->timemodified;// First let's see if items can be dropped from the bucket as a result of leakage.while (!empty($bucket) && ($now >= $bucket[0])) {array_shift($bucket);}// Calculate the time that the new item we put into the bucket will be leaked from it, and store it into the bucket.if ($bucket) {$bucket[] = max($bucket[count($bucket) - 1], $now) + (1 / $leak);} else {$bucket[] = $now + (1 / $leak);}}// Recalculate the bucket's content based on the leakage until now.$now = time();while (!empty($bucket) && ($now >= $bucket[0])) {array_shift($bucket);}return count($bucket) >= $capacity;}/*** Get used space of files* @global moodle_database $DB* @global stdClass $USER* @return int total bytes*/function file_get_user_used_space() {global $DB, $USER;$usercontext = context_user::instance($USER->id);$sql = "SELECT SUM(files1.filesize) AS totalbytes FROM {files} files1JOIN (SELECT contenthash, filename, MAX(id) AS idFROM {files}WHERE contextid = ? AND component = ? AND filearea != ?GROUP BY contenthash, filename) files2 ON files1.id = files2.id";$params = array('contextid'=>$usercontext->id, 'component'=>'user', 'filearea'=>'draft');$record = $DB->get_record_sql($sql, $params);return (int)$record->totalbytes;}/*** Convert any string to a valid filepath* @todo review this function* @param string $str* @return string path*/function file_correct_filepath($str) { //TODO: what is this? (skodak) - No idea (Fred)if ($str == '/' or empty($str)) {return '/';} else {return '/'.trim($str, '/').'/';}}/*** Generate a folder tree of draft area of current USER recursively** @todo MDL-31073 use normal return value instead, this does not fit the rest of api here (skodak)* @param int $draftitemid* @param string $filepath* @param mixed $data*/function file_get_drafarea_folders($draftitemid, $filepath, &$data) {global $USER, $OUTPUT, $CFG;$data->children = array();$context = context_user::instance($USER->id);$fs = get_file_storage();if ($files = $fs->get_directory_files($context->id, 'user', 'draft', $draftitemid, $filepath, false)) {foreach ($files as $file) {if ($file->is_directory()) {$item = new stdClass();$item->sortorder = $file->get_sortorder();$item->filepath = $file->get_filepath();$foldername = explode('/', trim($item->filepath, '/'));$item->fullname = trim(array_pop($foldername), '/');$item->id = uniqid();file_get_drafarea_folders($draftitemid, $item->filepath, $item);$data->children[] = $item;} else {continue;}}}}/*** Listing all files (including folders) in current path (draft area)* used by file manager* @param int $draftitemid* @param string $filepath* @return stdClass*/function file_get_drafarea_files($draftitemid, $filepath = '/') {global $USER, $OUTPUT, $CFG;$context = context_user::instance($USER->id);$fs = get_file_storage();$data = new stdClass();$data->path = array();$data->path[] = array('name'=>get_string('files'), 'path'=>'/');// will be used to build breadcrumb$trail = '/';if ($filepath !== '/') {$filepath = file_correct_filepath($filepath);$parts = explode('/', $filepath);foreach ($parts as $part) {if ($part != '' && $part != null) {$trail .= ($part.'/');$data->path[] = array('name'=>$part, 'path'=>$trail);}}}$list = array();$maxlength = 12;if ($files = $fs->get_directory_files($context->id, 'user', 'draft', $draftitemid, $filepath, false)) {foreach ($files as $file) {$item = new stdClass();$item->filename = $file->get_filename();$item->filepath = $file->get_filepath();$item->fullname = trim($item->filename, '/');$filesize = $file->get_filesize();$item->size = $filesize ? $filesize : null;$item->filesize = $filesize ? display_size($filesize) : '';$item->sortorder = $file->get_sortorder();$item->author = $file->get_author();$item->license = $file->get_license();$item->datemodified = $file->get_timemodified();$item->datecreated = $file->get_timecreated();$item->isref = $file->is_external_file();if ($item->isref && $file->get_status() == 666) {$item->originalmissing = true;}// find the file this draft file was created from and count all references in local// system pointing to that file$source = @unserialize($file->get_source() ?? '');if (isset($source->original)) {$item->refcount = $fs->search_references_count($source->original);}if ($file->is_directory()) {$item->filesize = 0;$item->icon = $OUTPUT->image_url(file_folder_icon())->out(false);$item->type = 'folder';$foldername = explode('/', trim($item->filepath, '/'));$item->fullname = trim(array_pop($foldername), '/');$item->thumbnail = $OUTPUT->image_url(file_folder_icon())->out(false);} else {// do NOT use file browser here!$item->mimetype = get_mimetype_description($file);if (file_extension_in_typegroup($file->get_filename(), 'archive')) {$item->type = 'zip';} else {$item->type = 'file';}$itemurl = moodle_url::make_draftfile_url($draftitemid, $item->filepath, $item->filename);$item->url = $itemurl->out();$item->icon = $OUTPUT->image_url(file_file_icon($file))->out(false);$item->thumbnail = $OUTPUT->image_url(file_file_icon($file))->out(false);// The call to $file->get_imageinfo() fails with an exception if the file can't be read on the file system.// We still want to add such files to the list, so the owner can view and delete them if needed. So, we only call// get_imageinfo() on files that can be read, and we also spoof the file status based on whether it was found.// We'll use the same status types used by stored_file->get_status(), where 0 = OK. 1 = problem, as these will be// used by the widget to display a warning about the problem files.// The value of stored_file->get_status(), and the file record are unaffected by this. It's only superficially set.$item->status = $fs->get_file_system()->is_file_readable_remotely_by_storedfile($file) ? 0 : 1;if ($item->status == 0) {if ($imageinfo = $file->get_imageinfo()) {$item->realthumbnail = $itemurl->out(false, array('preview' => 'thumb','oid' => $file->get_timemodified()));$item->realicon = $itemurl->out(false, array('preview' => 'tinyicon', 'oid' => $file->get_timemodified()));$item->image_width = $imageinfo['width'];$item->image_height = $imageinfo['height'];}}}$list[] = $item;}}$data->itemid = $draftitemid;$data->list = $list;return $data;}/*** Returns all of the files in the draftarea.** @param int $draftitemid The draft item ID* @param string $filepath path for the uploaded files.* @return array An array of files associated with this draft item id.*/function file_get_all_files_in_draftarea(int $draftitemid, string $filepath = '/'): array {$files = [];$draftfiles = file_get_drafarea_files($draftitemid, $filepath);file_get_drafarea_folders($draftitemid, $filepath, $draftfiles);if (!empty($draftfiles)) {foreach ($draftfiles->list as $draftfile) {if ($draftfile->type == 'file') {$files[] = $draftfile;}}if (isset($draftfiles->children)) {foreach ($draftfiles->children as $draftfile) {$files = array_merge($files, file_get_all_files_in_draftarea($draftitemid, $draftfile->filepath));}}}return $files;}/*** Returns draft area itemid for a given element.** @category files* @param string $elname name of formlib editor element, or a hidden form field that stores the draft area item id, etc.* @return int the itemid, or 0 if there is not one yet.*/function file_get_submitted_draft_itemid($elname) {// this is a nasty hack, ideally all new elements should use arrays here or there should be a new parameterif (!isset($_REQUEST[$elname])) {return 0;}if (is_array($_REQUEST[$elname])) {$param = optional_param_array($elname, 0, PARAM_INT);if (!empty($param['itemid'])) {$param = $param['itemid'];} else {debugging('Missing itemid, maybe caused by unset maxfiles option', DEBUG_DEVELOPER);return false;}} else {$param = optional_param($elname, 0, PARAM_INT);}if ($param) {require_sesskey();}return $param;}/*** Restore the original source field from draft files** Do not use this function because it makes field files.source inconsistent* for draft area files. This function will be deprecated in 2.6** @param stored_file $storedfile This only works with draft files* @return stored_file*/function file_restore_source_field_from_draft_file($storedfile) {$source = @unserialize($storedfile->get_source() ?? '');if (!empty($source)) {if (is_object($source)) {$restoredsource = $source->source;$storedfile->set_source($restoredsource);} else {throw new moodle_exception('invalidsourcefield', 'error');}}return $storedfile;}/*** Removes those files from the user drafts filearea which are not referenced in the editor text.** @param array $editor The online text editor element from the submitted form data.*/function file_remove_editor_orphaned_files($editor) {global $CFG, $USER;// Find those draft files included in the text, and generate their hashes.$context = context_user::instance($USER->id);$baseurl = $CFG->wwwroot . '/draftfile.php/' . $context->id . '/user/draft/' . $editor['itemid'] . '/';$pattern = "/" . preg_quote($baseurl, '/') . "(.+?)[\?\"'<>\s:\\\\]/";preg_match_all($pattern, $editor['text'], $matches);$usedfilehashes = [];foreach ($matches[1] as $matchedfilename) {$matchedfilename = urldecode($matchedfilename);$usedfilehashes[] = \file_storage::get_pathname_hash($context->id, 'user', 'draft', $editor['itemid'], '/',$matchedfilename);}// Now, compare the hashes of all draft files, and remove those which don't match used files.$fs = get_file_storage();$files = $fs->get_area_files($context->id, 'user', 'draft', $editor['itemid'], 'id', false);foreach ($files as $file) {$tmphash = $file->get_pathnamehash();if (!in_array($tmphash, $usedfilehashes)) {$file->delete();}}}/*** Finds all draft areas used in a textarea and copies the files into the primary textarea. If a user copies and pastes* content from another draft area it's possible for a single textarea to reference multiple draft areas.** @category files* @param int $draftitemid the id of the primary draft area.* When set to -1 (probably, by a WebService) it won't process file merging, keeping the original state of the file area.* @param int $usercontextid the user's context id.* @param string $text some html content that needs to have files copied to the correct draft area.* @param bool $forcehttps force https urls.** @return string $text html content modified with new draft links*/function file_merge_draft_areas($draftitemid, $usercontextid, $text, $forcehttps = false) {if (is_null($text)) {return null;}// Do not merge files, leave it as it was.if ($draftitemid === IGNORE_FILE_MERGE) {return null;}$urls = extract_draft_file_urls_from_text($text, $forcehttps, $usercontextid, 'user', 'draft');// No draft areas to rewrite.if (empty($urls)) {return $text;}foreach ($urls as $url) {// Do not process the "home" draft area.if ($url['itemid'] == $draftitemid) {continue;}// Decode the filename.$filename = urldecode($url['filename']);// Copy the file.file_copy_file_to_file_area($url, $filename, $draftitemid);// Rewrite draft area.$text = file_replace_file_area_in_text($url, $draftitemid, $text, $forcehttps);}return $text;}/*** Rewrites a file area in arbitrary text.** @param array $file General information about the file.* @param int $newid The new file area itemid.* @param string $text The text to rewrite.* @param bool $forcehttps force https urls.* @return string The rewritten text.*/function file_replace_file_area_in_text($file, $newid, $text, $forcehttps = false) {global $CFG;$wwwroot = $CFG->wwwroot;if ($forcehttps) {$wwwroot = str_replace('http://', 'https://', $wwwroot);}$search = [$wwwroot,$file['urlbase'],$file['contextid'],$file['component'],$file['filearea'],$file['itemid'],$file['filename']];$replace = [$wwwroot,$file['urlbase'],$file['contextid'],$file['component'],$file['filearea'],$newid,$file['filename']];$text = str_ireplace( implode('/', $search), implode('/', $replace), $text);return $text;}/*** Copies a file from one file area to another.** @param array $file Information about the file to be copied.* @param string $filename The filename.* @param int $itemid The new file area.*/function file_copy_file_to_file_area($file, $filename, $itemid) {$fs = get_file_storage();// Load the current file in the old draft area.$fileinfo = array('component' => $file['component'],'filearea' => $file['filearea'],'itemid' => $file['itemid'],'contextid' => $file['contextid'],'filepath' => '/','filename' => $filename);$oldfile = $fs->get_file($fileinfo['contextid'], $fileinfo['component'], $fileinfo['filearea'],$fileinfo['itemid'], $fileinfo['filepath'], $fileinfo['filename']);$newfileinfo = array('component' => $file['component'],'filearea' => $file['filearea'],'itemid' => $itemid,'contextid' => $file['contextid'],'filepath' => '/','filename' => $filename);$newcontextid = $newfileinfo['contextid'];$newcomponent = $newfileinfo['component'];$newfilearea = $newfileinfo['filearea'];$newitemid = $newfileinfo['itemid'];$newfilepath = $newfileinfo['filepath'];$newfilename = $newfileinfo['filename'];// Check if the file exists.if (!$fs->file_exists($newcontextid, $newcomponent, $newfilearea, $newitemid, $newfilepath, $newfilename)) {$fs->create_file_from_storedfile($newfileinfo, $oldfile);}}/*** Saves files from a draft file area to a real one (merging the list of files).* Can rewrite URLs in some content at the same time if desired.** @category files* @global stdClass $USER* @param int $draftitemid the id of the draft area to use. Normally obtained* from file_get_submitted_draft_itemid('elementname') or similar.* When set to -1 (probably, by a WebService) it won't process file merging, keeping the original state of the file area.* @param int $contextid This parameter and the next two identify the file area to save to.* @param string $component* @param string $filearea indentifies the file area.* @param int $itemid helps identifies the file area.* @param array $options area options (subdirs=>false, maxfiles=-1, maxbytes=0)* @param string $text some html content that needs to have embedded links rewritten* to the @@PLUGINFILE@@ form for saving in the database.* @param bool $forcehttps force https urls.* @return string|null if $text was passed in, the rewritten $text is returned. Otherwise NULL.*/function file_save_draft_area_files($draftitemid, $contextid, $component, $filearea, $itemid, array $options=null, $text=null, $forcehttps=false) {global $USER;// Do not merge files, leave it as it was.if ($draftitemid === IGNORE_FILE_MERGE) {// Safely return $text, no need to rewrite pluginfile because this is mostly comming from an external client like the app.return $text;}if ($itemid === false) {// Catch a potentially dangerous coding error.throw new coding_exception('file_save_draft_area_files was called with $itemid false. ' ."This suggests a bug, because it would wipe all ($contextid, $component, $filearea) files.");}$usercontext = context_user::instance($USER->id);$fs = get_file_storage();$options = (array)$options;if (!isset($options['subdirs'])) {$options['subdirs'] = false;}if (!isset($options['maxfiles'])) {$options['maxfiles'] = -1; // unlimited}if (!isset($options['maxbytes']) || $options['maxbytes'] == USER_CAN_IGNORE_FILE_SIZE_LIMITS) {$options['maxbytes'] = 0; // unlimited}if (!isset($options['areamaxbytes'])) {$options['areamaxbytes'] = FILE_AREA_MAX_BYTES_UNLIMITED; // Unlimited.}$allowreferences = true;if (isset($options['return_types']) && !($options['return_types'] & (FILE_REFERENCE | FILE_CONTROLLED_LINK))) {// we assume that if $options['return_types'] is NOT specified, we DO allow references.// this is not exactly right. BUT there are many places in code where filemanager options// are not passed to file_save_draft_area_files()$allowreferences = false;}// Check if the user has copy-pasted from other draft areas. Those files will be located in different draft// areas and need to be copied into the current draft area.$text = file_merge_draft_areas($draftitemid, $usercontext->id, $text, $forcehttps);// Check if the draft area has exceeded the authorised limit. This should never happen as validation// should have taken place before, unless the user is doing something nauthly. If so, let's just not save// anything at all in the next area.if (file_is_draft_area_limit_reached($draftitemid, $options['areamaxbytes'])) {return null;}$draftfiles = $fs->get_area_files($usercontext->id, 'user', 'draft', $draftitemid, 'id');$oldfiles = $fs->get_area_files($contextid, $component, $filearea, $itemid, 'id');// One file in filearea means it is empty (it has only top-level directory '.').if (count($draftfiles) > 1 || count($oldfiles) > 1) {// we have to merge old and new files - we want to keep file ids for files that were not changed// we change time modified for all new and changed files, we keep time created as is$newhashes = array();$filecount = 0;$context = context::instance_by_id($contextid, MUST_EXIST);foreach ($draftfiles as $file) {if (!$options['subdirs'] && $file->get_filepath() !== '/') {continue;}if (!$allowreferences && $file->is_external_file()) {continue;}if (!$file->is_directory()) {// Check to see if this file was uploaded by someone who can ignore the file size limits.$fileusermaxbytes = get_user_max_upload_file_size($context, $options['maxbytes'], 0, 0, $file->get_userid());if ($fileusermaxbytes != USER_CAN_IGNORE_FILE_SIZE_LIMITS&& ($options['maxbytes'] and $options['maxbytes'] < $file->get_filesize())) {// Oversized file.continue;}if ($options['maxfiles'] != -1 and $options['maxfiles'] <= $filecount) {// more files - should not get here at allcontinue;}$filecount++;}$newhash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $file->get_filepath(), $file->get_filename());$newhashes[$newhash] = $file;}// Loop through oldfiles and decide which we need to delete and which to update.// After this cycle the array $newhashes will only contain the files that need to be added.foreach ($oldfiles as $oldfile) {$oldhash = $oldfile->get_pathnamehash();if (!isset($newhashes[$oldhash])) {// delete files not needed any more - deleted by user$oldfile->delete();continue;}$newfile = $newhashes[$oldhash];// Now we know that we have $oldfile and $newfile for the same path.// Let's check if we can update this file or we need to delete and create.if ($newfile->is_directory()) {// Directories are always ok to just update.} else if (($source = @unserialize($newfile->get_source() ?? '')) && isset($source->original)) {// File has the 'original' - we need to update the file (it may even have not been changed at all).$original = file_storage::unpack_reference($source->original);if ($original['filename'] !== $oldfile->get_filename() || $original['filepath'] !== $oldfile->get_filepath()) {// Very odd, original points to another file. Delete and create file.$oldfile->delete();continue;}} else {// The same file name but absence of 'original' means that file was deteled and uploaded again.// By deleting and creating new file we properly manage all existing references.$oldfile->delete();continue;}// status changed, we delete old file, and create a new oneif ($oldfile->get_status() != $newfile->get_status()) {// file was changed, use updated with new timemodified data$oldfile->delete();// This file will be added latercontinue;}// Updated authorif ($oldfile->get_author() != $newfile->get_author()) {$oldfile->set_author($newfile->get_author());}// Updated licenseif ($oldfile->get_license() != $newfile->get_license()) {$oldfile->set_license($newfile->get_license());}// Updated file source// Field files.source for draftarea files contains serialised object with source and original information.// We only store the source part of it for non-draft file area.$newsource = $newfile->get_source();if ($source = @unserialize($newfile->get_source() ?? '')) {$newsource = $source->source;}if ($oldfile->get_source() !== $newsource) {$oldfile->set_source($newsource);}// Updated sort orderif ($oldfile->get_sortorder() != $newfile->get_sortorder()) {$oldfile->set_sortorder($newfile->get_sortorder());}// Update file timemodifiedif ($oldfile->get_timemodified() != $newfile->get_timemodified()) {$oldfile->set_timemodified($newfile->get_timemodified());}// Replaced file contentif (!$oldfile->is_directory() &&($oldfile->get_contenthash() != $newfile->get_contenthash() ||$oldfile->get_filesize() != $newfile->get_filesize() ||$oldfile->get_referencefileid() != $newfile->get_referencefileid() ||$oldfile->get_userid() != $newfile->get_userid())) {$oldfile->replace_file_with($newfile);}// unchanged file or directory - we keep it as isunset($newhashes[$oldhash]);}// Add fresh file or the file which has changed status// the size and subdirectory tests are extra safety only, the UI should prevent itforeach ($newhashes as $file) {$file_record = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'timemodified'=>time());if ($source = @unserialize($file->get_source() ?? '')) {// Field files.source for draftarea files contains serialised object with source and original information.// We only store the source part of it for non-draft file area.$file_record['source'] = $source->source;}if ($file->is_external_file()) {$repoid = $file->get_repository_id();if (!empty($repoid)) {$context = context::instance_by_id($contextid, MUST_EXIST);$repo = repository::get_repository_by_id($repoid, $context);if (!empty($options)) {$repo->options = $options;}$file_record['repositoryid'] = $repoid;// This hook gives the repo a place to do some house cleaning, and update the $reference before it's saved// to the file store. E.g. transfer ownership of the file to a system account etc.$reference = $repo->reference_file_selected($file->get_reference(), $context, $component, $filearea, $itemid);$file_record['reference'] = $reference;}}$fs->create_file_from_storedfile($file_record, $file);}}// note: do not purge the draft area - we clean up areas later in cron,// the reason is that user might press submit twice and they would loose the files,// also sometimes we might want to use hacks that save files into two different areasif (is_null($text)) {return null;} else {return file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps);}}/*** Convert the draft file area URLs in some content to @@PLUGINFILE@@ tokens* ready to be saved in the database. Normally, this is done automatically by* {@link file_save_draft_area_files()}.** @category files* @param string $text the content to process.* @param int $draftitemid the draft file area the content was using.* @param bool $forcehttps whether the content contains https URLs. Default false.* @return string the processed content.*/function file_rewrite_urls_to_pluginfile($text, $draftitemid, $forcehttps = false) {global $CFG, $USER;$usercontext = context_user::instance($USER->id);$wwwroot = $CFG->wwwroot;if ($forcehttps) {$wwwroot = str_replace('http://', 'https://', $wwwroot);}// relink embedded files if text submitted - no absolute links allowed in database!$text = str_ireplace("$wwwroot/draftfile.php/$usercontext->id/user/draft/$draftitemid/", '@@PLUGINFILE@@/', $text);if (strpos($text, 'draftfile.php?file=') !== false) {$matches = array();preg_match_all("!$wwwroot/draftfile.php\?file=%2F{$usercontext->id}%2Fuser%2Fdraft%2F{$draftitemid}%2F[^'\",&<>|`\s:\\\\]+!iu", $text, $matches);if ($matches) {foreach ($matches[0] as $match) {$replace = str_ireplace('%2F', '/', $match);$text = str_replace($match, $replace, $text);}}$text = str_ireplace("$wwwroot/draftfile.php?file=/$usercontext->id/user/draft/$draftitemid/", '@@PLUGINFILE@@/', $text);}return $text;}/*** Set file sort order** @global moodle_database $DB* @param int $contextid the context id* @param string $component file component* @param string $filearea file area.* @param int $itemid itemid.* @param string $filepath file path.* @param string $filename file name.* @param int $sortorder the sort order of file.* @return bool*/function file_set_sortorder($contextid, $component, $filearea, $itemid, $filepath, $filename, $sortorder) {global $DB;$conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea, 'itemid'=>$itemid, 'filepath'=>$filepath, 'filename'=>$filename);if ($file_record = $DB->get_record('files', $conditions)) {$sortorder = (int)$sortorder;$file_record->sortorder = $sortorder;$DB->update_record('files', $file_record);return true;}return false;}/*** reset file sort order number to 0* @global moodle_database $DB* @param int $contextid the context id* @param string $component* @param string $filearea file area.* @param int|bool $itemid itemid.* @return bool*/function file_reset_sortorder($contextid, $component, $filearea, $itemid=false) {global $DB;$conditions = array('contextid'=>$contextid, 'component'=>$component, 'filearea'=>$filearea);if ($itemid !== false) {$conditions['itemid'] = $itemid;}$file_records = $DB->get_records('files', $conditions);foreach ($file_records as $file_record) {$file_record->sortorder = 0;$DB->update_record('files', $file_record);}return true;}/*** Returns description of upload error** @param int $errorcode found in $_FILES['filename.ext']['error']* @return string error description string, '' if ok*/function file_get_upload_error($errorcode) {switch ($errorcode) {case 0: // UPLOAD_ERR_OK - no error$errmessage = '';break;case 1: // UPLOAD_ERR_INI_SIZE$errmessage = get_string('uploadserverlimit');break;case 2: // UPLOAD_ERR_FORM_SIZE$errmessage = get_string('uploadformlimit');break;case 3: // UPLOAD_ERR_PARTIAL$errmessage = get_string('uploadpartialfile');break;case 4: // UPLOAD_ERR_NO_FILE$errmessage = get_string('uploadnofilefound');break;// Note: there is no error with a value of 5case 6: // UPLOAD_ERR_NO_TMP_DIR$errmessage = get_string('uploadnotempdir');break;case 7: // UPLOAD_ERR_CANT_WRITE$errmessage = get_string('uploadcantwrite');break;case 8: // UPLOAD_ERR_EXTENSION$errmessage = get_string('uploadextension');break;default:$errmessage = get_string('uploadproblem');}return $errmessage;}/*** Recursive function formating an array in POST parameter* @param array $arraydata - the array that we are going to format and add into &$data array* @param string $currentdata - a row of the final postdata array at instant T* when finish, it's assign to $data under this format: name[keyname][][]...[]='value'* @param array $data - the final data array containing all POST parameters : 1 row = 1 parameter*/function format_array_postdata_for_curlcall($arraydata, $currentdata, &$data) {foreach ($arraydata as $k=>$v) {$newcurrentdata = $currentdata;if (is_array($v)) { //the value is an array, call the function recursively$newcurrentdata = $newcurrentdata.'['.urlencode($k).']';format_array_postdata_for_curlcall($v, $newcurrentdata, $data);} else { //add the POST parameter to the $data array$data[] = $newcurrentdata.'['.urlencode($k).']='.urlencode($v);}}}/*** Transform a PHP array into POST parameter* (see the recursive function format_array_postdata_for_curlcall)* @param array $postdata* @return string containing all POST parameters (1 row = 1 POST parameter)*/function format_postdata_for_curlcall($postdata) {$data = array();foreach ($postdata as $k=>$v) {if (is_array($v)) {$currentdata = urlencode($k);format_array_postdata_for_curlcall($v, $currentdata, $data);} else {$data[] = urlencode($k).'='.urlencode($v ?? '');}}$convertedpostdata = implode('&', $data);return $convertedpostdata;}/*** Fetches content of file from Internet (using proxy if defined). Uses cURL extension if present.* Due to security concerns only downloads from http(s) sources are supported.** @category files* @param string $url file url starting with http(s)://* @param array $headers http headers, null if none. If set, should be an* associative array of header name => value pairs.* @param array $postdata array means use POST request with given parameters* @param bool $fullresponse return headers, responses, etc in a similar way snoopy does* (if false, just returns content)* @param int $timeout timeout for complete download process including all file transfer* (default 5 minutes)* @param int $connecttimeout timeout for connection to server; this is the timeout that* usually happens if the remote server is completely down (default 20 seconds);* may not work when using proxy* @param bool $skipcertverify If true, the peer's SSL certificate will not be checked.* Only use this when already in a trusted location.* @param string $tofile store the downloaded content to file instead of returning it.* @param bool $calctimeout false by default, true enables an extra head request to try and determine* filesize and appropriately larger timeout based on $CFG->curltimeoutkbitrate* @return stdClass|string|bool stdClass object if $fullresponse is true, false if request failed, true* if file downloaded into $tofile successfully or the file content as a string.*/function download_file_content($url, $headers=null, $postdata=null, $fullresponse=false, $timeout=300, $connecttimeout=20, $skipcertverify=false, $tofile=NULL, $calctimeout=false) {global $CFG;// Only http and https links supported.if (!preg_match('|^https?://|i', $url)) {if ($fullresponse) {$response = new stdClass();$response->status = 0;$response->headers = array();$response->response_code = 'Invalid protocol specified in url';$response->results = '';$response->error = 'Invalid protocol specified in url';return $response;} else {return false;}}$options = array();$headers2 = array();if (is_array($headers)) {foreach ($headers as $key => $value) {if (is_numeric($key)) {$headers2[] = $value;} else {$headers2[] = "$key: $value";}}}if ($skipcertverify) {$options['CURLOPT_SSL_VERIFYPEER'] = false;} else {$options['CURLOPT_SSL_VERIFYPEER'] = true;}$options['CURLOPT_CONNECTTIMEOUT'] = $connecttimeout;$options['CURLOPT_FOLLOWLOCATION'] = 1;$options['CURLOPT_MAXREDIRS'] = 5;// Use POST if requested.if (is_array($postdata)) {$postdata = format_postdata_for_curlcall($postdata);} else if (empty($postdata)) {$postdata = null;}// Optionally attempt to get more correct timeout by fetching the file size.if (!isset($CFG->curltimeoutkbitrate)) {// Use very slow rate of 56kbps as a timeout speed when not set.$bitrate = 56;} else {$bitrate = $CFG->curltimeoutkbitrate;}if ($calctimeout and !isset($postdata)) {$curl = new curl();$curl->setHeader($headers2);$curl->head($url, $postdata, $options);$info = $curl->get_info();$error_no = $curl->get_errno();if (!$error_no && $info['download_content_length'] > 0) {// No curl errors - adjust for large files only - take max timeout.$timeout = max($timeout, ceil($info['download_content_length'] * 8 / ($bitrate * 1024)));}}$curl = new curl();$curl->setHeader($headers2);$options['CURLOPT_RETURNTRANSFER'] = true;$options['CURLOPT_NOBODY'] = false;$options['CURLOPT_TIMEOUT'] = $timeout;if ($tofile) {$fh = fopen($tofile, 'w');if (!$fh) {if ($fullresponse) {$response = new stdClass();$response->status = 0;$response->headers = array();$response->response_code = 'Can not write to file';$response->results = false;$response->error = 'Can not write to file';return $response;} else {return false;}}$options['CURLOPT_FILE'] = $fh;}if (isset($postdata)) {$content = $curl->post($url, $postdata, $options);} else {$content = $curl->get($url, null, $options);}if ($tofile) {fclose($fh);@chmod($tofile, $CFG->filepermissions);}/*// Try to detect encoding problems.if ((curl_errno($ch) == 23 or curl_errno($ch) == 61) and defined('CURLOPT_ENCODING')) {curl_setopt($ch, CURLOPT_ENCODING, 'none');$result = curl_exec($ch);}*/$info = $curl->get_info();$error_no = $curl->get_errno();$rawheaders = $curl->get_raw_response();if ($error_no) {$error = $content;if (!$fullresponse) {debugging("cURL request for \"$url\" failed with: $error ($error_no)", DEBUG_ALL);return false;}$response = new stdClass();if ($error_no == 28) {$response->status = '-100'; // Mimic snoopy.} else {$response->status = '0';}$response->headers = array();$response->response_code = $error;$response->results = false;$response->error = $error;return $response;}if ($tofile) {$content = true;}if (empty($info['http_code'])) {// For security reasons we support only true http connections (Location: file:// exploit prevention).$response = new stdClass();$response->status = '0';$response->headers = array();$response->response_code = 'Unknown cURL error';$response->results = false; // do NOT change this, we really want to ignore the result!$response->error = 'Unknown cURL error';} else {$response = new stdClass();$response->status = (string)$info['http_code'];$response->headers = $rawheaders;$response->results = $content;$response->error = '';// There might be multiple headers on redirect, find the status of the last one.$firstline = true;foreach ($rawheaders as $line) {if ($firstline) {$response->response_code = $line;$firstline = false;}if (trim($line, "\r\n") === '') {$firstline = true;}}}if ($fullresponse) {return $response;}if ($info['http_code'] != 200) {debugging("cURL request for \"$url\" failed, HTTP response code: ".$response->response_code, DEBUG_ALL);return false;}return $response->results;}/*** Returns a list of information about file types based on extensions.** The following elements expected in value array for each extension:* 'type' - mimetype* 'icon' - location of the icon file. If value is FILENAME, then either pix/f/FILENAME.gif* or pix/f/FILENAME.png must be present in moodle and contain 16x16 filetype icon;* also files with bigger sizes under names* FILENAME-24, FILENAME-32, FILENAME-64, FILENAME-128, FILENAME-256 are recommended.* 'groups' (optional) - array of filetype groups this filetype extension is part of;* commonly used in moodle the following groups:* - web_image - image that can be included as <img> in HTML* - image - image that we can parse using GD to find it's dimensions, also used for portfolio format* - optimised_image - image that will be processed and optimised* - video - file that can be imported as video in text editor* - audio - file that can be imported as audio in text editor* - archive - we can extract files from this archive* - spreadsheet - used for portfolio format* - document - used for portfolio format* - presentation - used for portfolio format* 'string' (optional) - the name of the string from lang/en/mimetypes.php that displays* human-readable description for this filetype;* Function {@link get_mimetype_description()} first looks at the presence of string for* particular mimetype (value of 'type'), if not found looks for string specified in 'string'* attribute, if not found returns the value of 'type';* 'defaulticon' (boolean, optional) - used by function {@link file_mimetype_icon()} to find* an icon for mimetype. If an entry with 'defaulticon' is not found for a particular mimetype,* this function will return first found icon; Especially usefull for types such as 'text/plain'** @category files* @return array List of information about file types based on extensions.* Associative array of extension (lower-case) to associative array* from 'element name' to data. Current element names are 'type' and 'icon'.* Unknown types should use the 'xxx' entry which includes defaults.*/function &get_mimetypes_array() {// Get types from the core_filetypes function, which includes caching.return core_filetypes::get_types();}/*** Determine a file's MIME type based on the given filename using the function mimeinfo.** This function retrieves a file's MIME type for a file that will be sent to the user.* This should only be used for file-sending purposes just like in send_stored_file, send_file, and send_temp_file.* Should the file's MIME type cannot be determined by mimeinfo, it will return 'application/octet-stream' as a default* MIME type which should tell the browser "I don't know what type of file this is, so just download it.".** @param string $filename The file's filename.* @return string The file's MIME type or 'application/octet-stream' if it cannot be determined.*/function get_mimetype_for_sending($filename = '') {// Guess the file's MIME type using mimeinfo.$mimetype = mimeinfo('type', $filename);// Use octet-stream as fallback if MIME type cannot be determined by mimeinfo.if (!$mimetype || $mimetype === 'document/unknown') {$mimetype = 'application/octet-stream';}return $mimetype;}/*** Obtains information about a filetype based on its extension. Will* use a default if no information is present about that particular* extension.** @category files* @param string $element Desired information (usually 'icon'* for icon filename or 'type' for MIME type. Can also be* 'icon24', ...32, 48, 64, 72, 80, 96, 128, 256)* @param string $filename Filename we're looking up* @return string Requested piece of information from array*/function mimeinfo($element, $filename) {global $CFG;$mimeinfo = & get_mimetypes_array();static $iconpostfixes = array(256=>'-256', 128=>'-128', 96=>'-96', 80=>'-80', 72=>'-72', 64=>'-64', 48=>'-48', 32=>'-32', 24=>'-24', 16=>'');$filetype = strtolower(pathinfo($filename ?? '', PATHINFO_EXTENSION));if (empty($filetype)) {$filetype = 'xxx'; // file without extension}if (preg_match('/^icon(\d*)$/', $element, $iconsizematch)) {$iconsize = max(array(16, (int)$iconsizematch[1]));$filenames = array($mimeinfo['xxx']['icon']);if ($filetype != 'xxx' && isset($mimeinfo[$filetype]['icon'])) {array_unshift($filenames, $mimeinfo[$filetype]['icon']);}// find the file with the closest size, first search for specific icon then for default iconforeach ($filenames as $filename) {foreach ($iconpostfixes as $size => $postfix) {$fullname = $CFG->dirroot.'/pix/f/'.$filename.$postfix;if ($iconsize >= $size &&(file_exists($fullname.'.svg') || file_exists($fullname.'.png') || file_exists($fullname.'.gif'))) {return $filename.$postfix;}}}} else if (isset($mimeinfo[$filetype][$element])) {return $mimeinfo[$filetype][$element];} else if (isset($mimeinfo['xxx'][$element])) {return $mimeinfo['xxx'][$element]; // By default} else {return null;}}/*** Obtains information about a filetype based on the MIME type rather than* the other way around.** @category files* @param string $element Desired information ('extension', 'icon', etc.)* @param string $mimetype MIME type we're looking up* @return string Requested piece of information from array*/function mimeinfo_from_type($element, $mimetype) {/* array of cached mimetype->extension associations */static $cached = array();$mimeinfo = & get_mimetypes_array();if (!array_key_exists($mimetype, $cached)) {$cached[$mimetype] = null;foreach($mimeinfo as $filetype => $values) {if ($values['type'] == $mimetype) {if ($cached[$mimetype] === null) {$cached[$mimetype] = '.'.$filetype;}if (!empty($values['defaulticon'])) {$cached[$mimetype] = '.'.$filetype;break;}}}if (empty($cached[$mimetype])) {$cached[$mimetype] = '.xxx';}}if ($element === 'extension') {return $cached[$mimetype];} else {return mimeinfo($element, $cached[$mimetype]);}}/*** Return the relative icon path for a given file** Usage:* <code>* // $file - instance of stored_file or file_info* $icon = $OUTPUT->image_url(file_file_icon($file))->out();* echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($file)));* </code>* or* <code>* echo $OUTPUT->pix_icon(file_file_icon($file), get_mimetype_description($file));* </code>** @param stored_file|file_info|stdClass|array $file (in case of object attributes $file->filename* and $file->mimetype are expected)* @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.* @return string*/function file_file_icon($file, $unused = null) {if ($unused !== null) {debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);}if (!is_object($file)) {$file = (object)$file;}if (isset($file->filename)) {$filename = $file->filename;} else if (method_exists($file, 'get_filename')) {$filename = $file->get_filename();} else if (method_exists($file, 'get_visible_name')) {$filename = $file->get_visible_name();} else {$filename = '';}if (isset($file->mimetype)) {$mimetype = $file->mimetype;} else if (method_exists($file, 'get_mimetype')) {$mimetype = $file->get_mimetype();} else {$mimetype = '';}$mimetypes = &get_mimetypes_array();if ($filename) {$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));if ($extension && !empty($mimetypes[$extension])) {// if file name has known extension, return icon for this extensionreturn file_extension_icon($filename);}}return file_mimetype_icon($mimetype);}/*** Return the relative icon path for a folder image.** Usage:* <code>* $icon = $OUTPUT->image_url(file_folder_icon())->out();* echo html_writer::empty_tag('img', array('src' => $icon));* </code>* or* <code>* echo $OUTPUT->pix_icon(file_folder_icon(), '');* </code>** @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.* @return string*/function file_folder_icon($unused = null) {global $CFG;if ($unused !== null) {debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);}return 'f/folder';}/*** Returns the relative icon path for a given mime type** This function should be used in conjunction with $OUTPUT->image_url to produce* a return the full path to an icon.** <code>* $mimetype = 'image/jpg';* $icon = $OUTPUT->image_url(file_mimetype_icon($mimetype))->out();* echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => get_mimetype_description($mimetype)));* </code>** @category files* @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered* to conform with that.* @param string $mimetype The mimetype to fetch an icon for* @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.* @return string The relative path to the icon*/function file_mimetype_icon($mimetype, $unused = null) {return 'f/'.mimeinfo_from_type('icon', $mimetype);}/*** Returns the relative icon path for a given file name** This function should be used in conjunction with $OUTPUT->image_url to produce* a return the full path to an icon.** <code>* $filename = '.jpg';* $icon = $OUTPUT->image_url(file_extension_icon($filename))->out();* echo html_writer::empty_tag('img', array('src' => $icon, 'alt' => '...'));* </code>** @todo MDL-31074 When an $OUTPUT->icon method is available this function should be altered* to conform with that.* @todo MDL-31074 Implement $size* @category files* @param string $filename The filename to get the icon for* @param mixed $unused This parameter has been deprecated since 4.3 and should not be used anymore.* @return string*/function file_extension_icon($filename, $unused = null) {if ($unused !== null) {debugging('Deprecated argument passed to ' . __FUNCTION__, DEBUG_DEVELOPER);}return 'f/'.mimeinfo('icon', $filename);}/*** Obtains descriptions for file types (e.g. 'Microsoft Word document') from the* mimetypes.php language file.** @param mixed $obj - instance of stored_file or file_info or array/stdClass with field* 'filename' and 'mimetype', or just a string with mimetype (though it is recommended to* have filename); In case of array/stdClass the field 'mimetype' is optional.* @param bool $capitalise If true, capitalises first character of result* @return string Text description*/function get_mimetype_description($obj, $capitalise=false) {$filename = $mimetype = '';if (is_object($obj) && method_exists($obj, 'get_filename') && method_exists($obj, 'get_mimetype')) {// this is an instance of stored_file$mimetype = $obj->get_mimetype();$filename = $obj->get_filename();} else if (is_object($obj) && method_exists($obj, 'get_visible_name') && method_exists($obj, 'get_mimetype')) {// this is an instance of file_info$mimetype = $obj->get_mimetype();$filename = $obj->get_visible_name();} else if (is_array($obj) || is_object ($obj)) {$obj = (array)$obj;if (!empty($obj['filename'])) {$filename = $obj['filename'];}if (!empty($obj['mimetype'])) {$mimetype = $obj['mimetype'];}} else {$mimetype = $obj;}$mimetypefromext = mimeinfo('type', $filename);if (empty($mimetype) || $mimetypefromext !== 'document/unknown') {// if file has a known extension, overwrite the specified mimetype$mimetype = $mimetypefromext;}$extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));if (empty($extension)) {$mimetypestr = mimeinfo_from_type('string', $mimetype);$extension = str_replace('.', '', mimeinfo_from_type('extension', $mimetype));} else {$mimetypestr = mimeinfo('string', $filename);}$chunks = explode('/', $mimetype, 2);$chunks[] = '';$attr = array('mimetype' => $mimetype,'ext' => $extension,'mimetype1' => $chunks[0],'mimetype2' => $chunks[1],);$a = array();foreach ($attr as $key => $value) {$a[$key] = $value;$a[strtoupper($key)] = strtoupper($value);$a[ucfirst($key)] = ucfirst($value);}// MIME types may include + symbol but this is not permitted in string ids.$safemimetype = str_replace('+', '_', $mimetype ?? '');$safemimetypestr = str_replace('+', '_', $mimetypestr ?? '');$customdescription = mimeinfo('customdescription', $filename);if ($customdescription) {// Call format_string on the custom description so that multilang// filter can be used (if enabled on system context). We use system// context because it is possible that the page context might not have// been defined yet.$result = format_string($customdescription, true,array('context' => context_system::instance()));} else if (get_string_manager()->string_exists($safemimetype, 'mimetypes')) {$result = get_string($safemimetype, 'mimetypes', (object)$a);} else if (get_string_manager()->string_exists($safemimetypestr, 'mimetypes')) {$result = get_string($safemimetypestr, 'mimetypes', (object)$a);} else if (get_string_manager()->string_exists('default', 'mimetypes')) {$result = get_string('default', 'mimetypes', (object)$a);} else {$result = $mimetype;}if ($capitalise) {$result=ucfirst($result);}return $result;}/*** Returns array of elements of type $element in type group(s)** @param string $element name of the element we are interested in, usually 'type' or 'extension'* @param string|array $groups one group or array of groups/extensions/mimetypes* @return array*/function file_get_typegroup($element, $groups) {static $cached = array();// Turn groups into a list.if (!is_array($groups)) {$groups = preg_split('/[\s,;:"\']+/', $groups, -1, PREG_SPLIT_NO_EMPTY);}if (!array_key_exists($element, $cached)) {$cached[$element] = array();}$result = array();foreach ($groups as $group) {if (!array_key_exists($group, $cached[$element])) {// retrieive and cache all elements of type $element for group $group$mimeinfo = & get_mimetypes_array();$cached[$element][$group] = array();foreach ($mimeinfo as $extension => $value) {$value['extension'] = '.'.$extension;if (empty($value[$element])) {continue;}if (($group === '.'.$extension || $group === $value['type'] ||(!empty($value['groups']) && in_array($group, $value['groups']))) &&!in_array($value[$element], $cached[$element][$group])) {$cached[$element][$group][] = $value[$element];}}}$result = array_merge($result, $cached[$element][$group]);}return array_values(array_unique($result));}/*** Checks if file with name $filename has one of the extensions in groups $groups** @see get_mimetypes_array()* @param string $filename name of the file to check* @param string|array $groups one group or array of groups to check* @param bool $checktype if true and extension check fails, find the mimetype and check if* file mimetype is in mimetypes in groups $groups* @return bool*/function file_extension_in_typegroup($filename, $groups, $checktype = false) {$extension = pathinfo($filename, PATHINFO_EXTENSION);if (!empty($extension) && in_array('.'.strtolower($extension), file_get_typegroup('extension', $groups))) {return true;}return $checktype && file_mimetype_in_typegroup(mimeinfo('type', $filename), $groups);}/*** Checks if mimetype $mimetype belongs to one of the groups $groups** @see get_mimetypes_array()* @param string $mimetype* @param string|array $groups one group or array of groups to check* @return bool*/function file_mimetype_in_typegroup($mimetype, $groups) {return !empty($mimetype) && in_array($mimetype, file_get_typegroup('type', $groups));}/*** Requested file is not found or not accessible, does not return, terminates script** @global stdClass $CFG* @global stdClass $COURSE*/function send_file_not_found() {global $CFG, $COURSE;// Allow cross-origin requests only for Web Services.// This allow to receive requests done by Web Workers or webapps in different domains.if (WS_SERVER) {header('Access-Control-Allow-Origin: *');}send_header_404();throw new \moodle_exception('filenotfound', 'error',$CFG->wwwroot.'/course/view.php?id='.$COURSE->id); // This is not displayed on IIS?}/*** Helper function to send correct 404 for server.*/function send_header_404() {if (substr(php_sapi_name(), 0, 3) == 'cgi') {header("Status: 404 Not Found");} else {header('HTTP/1.0 404 not found');}}/*** The readfile function can fail when files are larger than 2GB (even on 64-bit* platforms). This wrapper uses readfile for small files and custom code for* large ones.** @param string $path Path to file* @param int $filesize Size of file (if left out, will get it automatically)* @return int|bool Size read (will always be $filesize) or false if failed*/function readfile_allow_large($path, $filesize = -1) {// Automatically get size if not specified.if ($filesize === -1) {$filesize = filesize($path);}if ($filesize <= 2147483647) {// If the file is up to 2^31 - 1, send it normally using readfile.return readfile($path);} else {// For large files, read and output in 64KB chunks.$handle = fopen($path, 'r');if ($handle === false) {return false;}$left = $filesize;while ($left > 0) {$size = min($left, 65536);$buffer = fread($handle, $size);if ($buffer === false) {return false;}echo $buffer;$left -= $size;}return $filesize;}}/*** Enhanced readfile() with optional acceleration.* @param string|stored_file $file* @param string $mimetype* @param bool $accelerate* @return void*/function readfile_accel($file, $mimetype, $accelerate) {global $CFG;if ($mimetype === 'text/plain') {// there is no encoding specified in text files, we need something consistentheader('Content-Type: text/plain; charset=utf-8');} else {header('Content-Type: '.$mimetype);}$lastmodified = is_object($file) ? $file->get_timemodified() : filemtime($file);header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');if (is_object($file)) {header('Etag: "' . $file->get_contenthash() . '"');if (isset($_SERVER['HTTP_IF_NONE_MATCH']) and trim($_SERVER['HTTP_IF_NONE_MATCH'], '"') === $file->get_contenthash()) {header('HTTP/1.1 304 Not Modified');return;}}// if etag present for stored file rely on it exclusivelyif (!empty($_SERVER['HTTP_IF_MODIFIED_SINCE']) and (empty($_SERVER['HTTP_IF_NONE_MATCH']) or !is_object($file))) {// get unixtime of request header; clip extra junk off first$since = strtotime(preg_replace('/;.*$/', '', $_SERVER["HTTP_IF_MODIFIED_SINCE"]));if ($since && $since >= $lastmodified) {header('HTTP/1.1 304 Not Modified');return;}}if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {header('Accept-Ranges: bytes');} else {header('Accept-Ranges: none');}if ($accelerate) {if (is_object($file)) {$fs = get_file_storage();if ($fs->supports_xsendfile()) {if ($fs->xsendfile_file($file)) {return;}}} else {if (!empty($CFG->xsendfile)) {require_once("$CFG->libdir/xsendfilelib.php");if (xsendfile($file)) {return;}}}}$filesize = is_object($file) ? $file->get_filesize() : filesize($file);header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');if ($accelerate and empty($CFG->disablebyteserving) and $mimetype !== 'text/plain') {if (!empty($_SERVER['HTTP_RANGE']) and strpos($_SERVER['HTTP_RANGE'],'bytes=') !== FALSE) {// byteserving stuff - for acrobat reader and download accelerators// see: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35// inspired by: http://www.coneural.org/florian/papers/04_byteserving.php$ranges = false;if (preg_match_all('/(\d*)-(\d*)/', $_SERVER['HTTP_RANGE'], $ranges, PREG_SET_ORDER)) {foreach ($ranges as $key=>$value) {if ($ranges[$key][1] == '') {//suffix case$ranges[$key][1] = $filesize - $ranges[$key][2];$ranges[$key][2] = $filesize - 1;} else if ($ranges[$key][2] == '' || $ranges[$key][2] > $filesize - 1) {//fix range length$ranges[$key][2] = $filesize - 1;}if ($ranges[$key][2] != '' && $ranges[$key][2] < $ranges[$key][1]) {//invalid byte-range ==> ignore header$ranges = false;break;}//prepare multipart header$ranges[$key][0] = "\r\n--".BYTESERVING_BOUNDARY."\r\nContent-Type: $mimetype\r\n";$ranges[$key][0] .= "Content-Range: bytes {$ranges[$key][1]}-{$ranges[$key][2]}/$filesize\r\n\r\n";}} else {$ranges = false;}if ($ranges) {if (is_object($file)) {$handle = $file->get_content_file_handle();if ($handle === false) {throw new file_exception('storedfilecannotreadfile', $file->get_filename());}} else {$handle = fopen($file, 'rb');if ($handle === false) {throw new file_exception('cannotopenfile', $file);}}byteserving_send_file($handle, $mimetype, $ranges, $filesize);}}}header('Content-Length: ' . $filesize);if (!empty($_SERVER['REQUEST_METHOD']) and $_SERVER['REQUEST_METHOD'] === 'HEAD') {exit;}while (ob_get_level()) {$handlerstack = ob_list_handlers();$activehandler = array_pop($handlerstack);if ($activehandler === 'default output handler') {// We do not expect any content in the buffer when we are serving files.$buffercontents = ob_get_clean();if ($buffercontents !== '') {error_log('Non-empty default output handler buffer detected while serving the file ' . $file);}} else {// Some handlers such as zlib output compression may have file signature buffered - flush it.ob_end_flush();}}// send the whole file contentif (is_object($file)) {$file->readfile();} else {if (readfile_allow_large($file, $filesize) === false) {throw new file_exception('cannotopenfile', $file);}}}/*** Similar to readfile_accel() but designed for strings.* @param string $string* @param string $mimetype* @param bool $accelerate Ignored* @return void*/function readstring_accel($string, $mimetype, $accelerate = false) {global $CFG;if ($mimetype === 'text/plain') {// there is no encoding specified in text files, we need something consistentheader('Content-Type: text/plain; charset=utf-8');} else {header('Content-Type: '.$mimetype);}header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');header('Accept-Ranges: none');header('Content-Length: '.strlen($string));echo $string;}/*** Handles the sending of temporary file to user, download is forced.* File is deleted after abort or successful sending, does not return, script terminated** @param string $path path to file, preferably from moodledata/temp/something; or content of file itself* @param string $filename proposed file name when saving file* @param bool $pathisstring If the path is string*/function send_temp_file($path, $filename, $pathisstring=false) {global $CFG;// Guess the file's MIME type.$mimetype = get_mimetype_for_sending($filename);// close session - not needed anymore\core\session\manager::write_close();if (!$pathisstring) {if (!file_exists($path)) {send_header_404();throw new \moodle_exception('filenotfound', 'error', $CFG->wwwroot.'/');}// executed after normal finish or abortcore_shutdown_manager::register_function('send_temp_file_finished', array($path));}// if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popupif (core_useragent::is_ie() || core_useragent::is_edge()) {$filename = urlencode($filename);}// If this file was requested from a form, then mark download as complete.\core_form\util::form_download_complete();header('Content-Disposition: attachment; filename="'.$filename.'"');if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.header('Cache-Control: private, max-age=10, no-transform');header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');header('Pragma: ');} else { //normal http - prevent caching at all costheader('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0, no-transform');header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');header('Pragma: no-cache');}// send the contents - we can not accelerate this because the file will be deleted asapif ($pathisstring) {readstring_accel($path, $mimetype);} else {readfile_accel($path, $mimetype, false);@unlink($path);}die; //no more chars to output}/*** Internal callback function used by send_temp_file()** @param string $path*/function send_temp_file_finished($path) {if (file_exists($path)) {@unlink($path);}}/*** Serve content which is not meant to be cached.** This is only intended to be used for volatile public files, for instance* when development is enabled, or when caching is not required on a public resource.** @param string $content Raw content.* @param string $filename The file name.* @return void*/function send_content_uncached($content, $filename) {$mimetype = mimeinfo('type', $filename);$charset = strpos($mimetype, 'text/') === 0 ? '; charset=utf-8' : '';header('Content-Disposition: inline; filename="' . $filename . '"');header('Last-Modified: ' . gmdate('D, d M Y H:i:s', time()) . ' GMT');header('Expires: ' . gmdate('D, d M Y H:i:s', time() + 2) . ' GMT');header('Pragma: ');header('Accept-Ranges: none');header('Content-Type: ' . $mimetype . $charset);header('Content-Length: ' . strlen($content));echo $content;die();}/*** Safely save content to a certain path.** This function tries hard to be atomic by first copying the content* to a separate file, and then moving the file across. It also prevents* the user to abort a request to prevent half-safed files.** This function is intended to be used when saving some content to cache like* $CFG->localcachedir. If you're not caching a file you should use the File API.** @param string $content The file content.* @param string $destination The absolute path of the final file.* @return void*/function file_safe_save_content($content, $destination) {global $CFG;clearstatcache();if (!file_exists(dirname($destination))) {@mkdir(dirname($destination), $CFG->directorypermissions, true);}// Prevent serving of incomplete file from concurrent request,// the rename() should be more atomic than fwrite().ignore_user_abort(true);if ($fp = fopen($destination . '.tmp', 'xb')) {fwrite($fp, $content);fclose($fp);rename($destination . '.tmp', $destination);@chmod($destination, $CFG->filepermissions);@unlink($destination . '.tmp'); // Just in case anything fails.}ignore_user_abort(false);if (connection_aborted()) {die();}}/*** Handles the sending of file data to the user's browser, including support for* byteranges etc.** @category files* @param string|stored_file $path Path of file on disk (including real filename),* or actual content of file as string,* or stored_file object* @param string $filename Filename to send* @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)* @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only* @param bool $pathisstring If true (default false), $path is the content to send and not the pathname.* Forced to false when $path is a stored_file object.* @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin* @param string $mimetype Include to specify the MIME type; leave blank to have it guess the type from $filename* @param bool $dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.* if this is passed as true, ignore_user_abort is called. if you don't want your processing to continue on cancel,* you must detect this case when control is returned using connection_aborted. Please not that session is closed* and should not be reopened.* @param array $options An array of options, currently accepts:* - (string) cacheability: public, or private.* - (string|null) immutable* - (bool) dontforcesvgdownload: true if force download should be disabled on SVGs.* Note: This overrides a security feature, so should only be applied to "trusted" content* (eg module content that is created using an XSS risk flagged capability, such as SCORM).* @return null script execution stopped unless $dontdie is true*/function send_file($path, $filename, $lifetime = null , $filter=0, $pathisstring=false, $forcedownload=false, $mimetype='',$dontdie=false, array $options = array()) {global $CFG, $COURSE;if ($dontdie) {ignore_user_abort(true);}if ($lifetime === 'default' or is_null($lifetime)) {$lifetime = $CFG->filelifetime;}if (is_object($path)) {$pathisstring = false;}\core\session\manager::write_close(); // Unlock session during file serving.// Use given MIME type if specified, otherwise guess it.if (!$mimetype || $mimetype === 'document/unknown') {$mimetype = get_mimetype_for_sending($filename);}// if user is using IE, urlencode the filename so that multibyte file name will show up correctly on popupif (core_useragent::is_ie() || core_useragent::is_edge()) {$filename = rawurlencode($filename);}// Make sure we force download of SVG files, unless the module explicitly allows them (eg within SCORM content).// This is for security reasons (https://digi.ninja/blog/svg_xss.php).if (file_is_svg_image_from_mimetype($mimetype) && empty($options['dontforcesvgdownload'])) {$forcedownload = true;}if ($forcedownload) {header('Content-Disposition: attachment; filename="'.$filename.'"');// If this file was requested from a form, then mark download as complete.\core_form\util::form_download_complete();} else if ($mimetype !== 'application/x-shockwave-flash') {// If this is an swf don't pass content-disposition with filename as this makes the flash player treat the file// as an upload and enforces security that may prevent the file from being loaded.header('Content-Disposition: inline; filename="'.$filename.'"');}if ($lifetime > 0) {$immutable = '';if (!empty($options['immutable'])) {$immutable = ', immutable';// Overwrite lifetime accordingly:// 90 days only - based on Moodle point release cadence being every 3 months.$lifetimemin = 60 * 60 * 24 * 90;$lifetime = max($lifetime, $lifetimemin);}$cacheability = ' public,';if (!empty($options['cacheability']) && ($options['cacheability'] === 'public')) {// This file must be cache-able by both browsers and proxies.$cacheability = ' public,';} else if (!empty($options['cacheability']) && ($options['cacheability'] === 'private')) {// This file must be cache-able only by browsers.$cacheability = ' private,';} else if (isloggedin() and !isguestuser()) {// By default, under the conditions above, this file must be cache-able only by browsers.$cacheability = ' private,';}$nobyteserving = false;header('Cache-Control:'.$cacheability.' max-age='.$lifetime.', no-transform'.$immutable);header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');header('Pragma: ');} else { // Do not cache files in proxies and browsers$nobyteserving = true;if (is_https()) { // HTTPS sites - watch out for IE! KB812935 and KB316431.header('Cache-Control: private, max-age=10, no-transform');header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');header('Pragma: ');} else { //normal http - prevent caching at all costheader('Cache-Control: private, must-revalidate, pre-check=0, post-check=0, max-age=0, no-transform');header('Expires: '. gmdate('D, d M Y H:i:s', 0) .' GMT');header('Pragma: no-cache');}}if (empty($filter)) {// send the contentsif ($pathisstring) {readstring_accel($path, $mimetype);} else {readfile_accel($path, $mimetype, !$dontdie);}} else {// Try to put the file through filtersif ($mimetype == 'text/html' || $mimetype == 'application/xhtml+xml' || file_is_svg_image_from_mimetype($mimetype)) {$options = new stdClass();$options->noclean = true;$options->nocache = true; // temporary workaround for MDL-5136if (is_object($path)) {$text = $path->get_content();} else if ($pathisstring) {$text = $path;} else {$text = implode('', file($path));}$output = format_text($text, FORMAT_HTML, $options, $COURSE->id);readstring_accel($output, $mimetype);} else if (($mimetype == 'text/plain') and ($filter == 1)) {// only filter text if filter all files is selected$options = new stdClass();$options->newlines = false;$options->noclean = true;if (is_object($path)) {$text = htmlentities($path->get_content(), ENT_QUOTES, 'UTF-8');} else if ($pathisstring) {$text = htmlentities($path, ENT_QUOTES, 'UTF-8');} else {$text = htmlentities(implode('', file($path)), ENT_QUOTES, 'UTF-8');}$output = '<pre>'. format_text($text, FORMAT_MOODLE, $options, $COURSE->id) .'</pre>';readstring_accel($output, $mimetype);} else {// send the contentsif ($pathisstring) {readstring_accel($path, $mimetype);} else {readfile_accel($path, $mimetype, !$dontdie);}}}if ($dontdie) {return;}die; //no more chars to output!!!}/*** Handles the sending of file data to the user's browser, including support for* byteranges etc.** The $options parameter supports the following keys:* (string|null) preview - send the preview of the file (e.g. "thumb" for a thumbnail)* (string|null) filename - overrides the implicit filename* (bool) dontdie - return control to caller afterwards. this is not recommended and only used for cleanup tasks.* if this is passed as true, ignore_user_abort is called. if you don't want your processing to continue on cancel,* you must detect this case when control is returned using connection_aborted. Please not that session is closed* and should not be reopened* (string|null) cacheability - force the cacheability setting of the HTTP response, "private" or "public",* when $lifetime is greater than 0. Cacheability defaults to "private" when logged in as other than guest; otherwise,* defaults to "public".* (string|null) immutable - set the immutable cache setting in the HTTP response, when served under HTTPS.* Note: it's up to the consumer to set it properly i.e. when serving a "versioned" URL.** @category files* @param stored_file $storedfile local file object* @param int $lifetime Number of seconds before the file should expire from caches (null means $CFG->filelifetime)* @param int $filter 0 (default)=no filtering, 1=all files, 2=html files only* @param bool $forcedownload If true (default false), forces download of file rather than view in browser/plugin* @param array $options additional options affecting the file serving* @return null script execution stopped unless $options['dontdie'] is true*/function send_stored_file($storedfile, $lifetime=null, $filter=0, $forcedownload=false, array $options=array()) {global $CFG, $COURSE;static $recursion = 0;if (empty($options['filename'])) {$filename = null;} else {$filename = $options['filename'];}if (empty($options['dontdie'])) {$dontdie = false;} else {$dontdie = true;}if ($lifetime === 'default' or is_null($lifetime)) {$lifetime = $CFG->filelifetime;}if (!empty($options['preview'])) {// replace the file with its preview$fs = get_file_storage();$previewfile = $fs->get_file_preview($storedfile, $options['preview']);if (!$previewfile) {// Unable to create a preview of the file, send its default mime icon instead.$fileicon = file_file_icon($storedfile);send_file($CFG->dirroot.'/pix/'.$fileicon.'.svg', basename($fileicon).'.svg');} else {// preview images have fixed cache lifetime and they ignore forced download// (they are generated by GD and therefore they are considered reasonably safe).$storedfile = $previewfile;$lifetime = DAYSECS;$filter = 0;$forcedownload = false;}}// handle external resourceif ($storedfile && $storedfile->is_external_file() && !isset($options['sendcachedexternalfile'])) {// Have we been here before?$recursion++;if ($recursion > 10) {throw new coding_exception('Recursive file serving detected');}$storedfile->send_file($lifetime, $filter, $forcedownload, $options);die;}if (!$storedfile || $storedfile->is_directory()) {// Nothing to serve.if ($dontdie) {return;}die;}$filename = is_null($filename) ? $storedfile->get_filename() : $filename;// Use given MIME type if specified.$mimetype = $storedfile->get_mimetype();// Allow cross-origin requests only for Web Services.// This allow to receive requests done by Web Workers or webapps in different domains.if (WS_SERVER) {header('Access-Control-Allow-Origin: *');}send_file($storedfile, $filename, $lifetime, $filter, false, $forcedownload, $mimetype, $dontdie, $options);}/*** Recursively delete the file or folder with path $location. That is,* if it is a file delete it. If it is a folder, delete all its content* then delete it. If $location does not exist to start, that is not* considered an error.** @param string $location the path to remove.* @return bool*/function fulldelete($location) {if (empty($location)) {// extra safety against wrong paramreturn false;}if (is_dir($location)) {if (!$currdir = opendir($location)) {return false;}while (false !== ($file = readdir($currdir))) {if ($file <> ".." && $file <> ".") {$fullfile = $location."/".$file;if (is_dir($fullfile)) {if (!fulldelete($fullfile)) {return false;}} else {if (!unlink($fullfile)) {return false;}}}}closedir($currdir);if (! rmdir($location)) {return false;}} else if (file_exists($location)) {if (!unlink($location)) {return false;}}return true;}/*** Send requested byterange of file.** @param resource $handle A file handle* @param string $mimetype The mimetype for the output* @param array $ranges An array of ranges to send* @param string $filesize The size of the content if only one range is used*/function byteserving_send_file($handle, $mimetype, $ranges, $filesize) {// better turn off any kind of compression and bufferingini_set('zlib.output_compression', 'Off');$chunksize = 1*(1024*1024); // 1MB chunks - must be less than 2MB!if ($handle === false) {die;}if (count($ranges) == 1) { //only one range requested$length = $ranges[0][2] - $ranges[0][1] + 1;header('HTTP/1.1 206 Partial content');header('Content-Length: '.$length);header('Content-Range: bytes '.$ranges[0][1].'-'.$ranges[0][2].'/'.$filesize);header('Content-Type: '.$mimetype);while(@ob_get_level()) {if (!@ob_end_flush()) {break;}}fseek($handle, $ranges[0][1]);while (!feof($handle) && $length > 0) {core_php_time_limit::raise(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk$buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));echo $buffer;flush();$length -= strlen($buffer);}fclose($handle);die;} else { // multiple ranges requested - not tested much$totallength = 0;foreach($ranges as $range) {$totallength += strlen($range[0]) + $range[2] - $range[1] + 1;}$totallength += strlen("\r\n--".BYTESERVING_BOUNDARY."--\r\n");header('HTTP/1.1 206 Partial content');header('Content-Length: '.$totallength);header('Content-Type: multipart/byteranges; boundary='.BYTESERVING_BOUNDARY);while(@ob_get_level()) {if (!@ob_end_flush()) {break;}}foreach($ranges as $range) {$length = $range[2] - $range[1] + 1;echo $range[0];fseek($handle, $range[1]);while (!feof($handle) && $length > 0) {core_php_time_limit::raise(60*60); //reset time limit to 60 min - should be enough for 1 MB chunk$buffer = fread($handle, ($chunksize < $length ? $chunksize : $length));echo $buffer;flush();$length -= strlen($buffer);}}echo "\r\n--".BYTESERVING_BOUNDARY."--\r\n";fclose($handle);die;}}/*** Tells whether the filename is executable.** @link http://php.net/manual/en/function.is-executable.php* @link https://bugs.php.net/bug.php?id=41062* @param string $filename Path to the file.* @return bool True if the filename exists and is executable; otherwise, false.*/function file_is_executable($filename) {if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {if (is_executable($filename)) {return true;} else {$fileext = strrchr($filename, '.');// If we have an extension we can check if it is listed as executable.if ($fileext && file_exists($filename) && !is_dir($filename)) {$winpathext = strtolower(getenv('PATHEXT'));$winpathexts = explode(';', $winpathext);return in_array(strtolower($fileext), $winpathexts);}return false;}} else {return is_executable($filename);}}/*** Overwrite an existing file in a draft area.** @param stored_file $newfile the new file with the new content and meta-data* @param stored_file $existingfile the file that will be overwritten* @throws moodle_exception* @since Moodle 3.2*/function file_overwrite_existing_draftfile(stored_file $newfile, stored_file $existingfile) {if ($existingfile->get_component() != 'user' or $existingfile->get_filearea() != 'draft') {throw new coding_exception('The file to overwrite is not in a draft area.');}$fs = get_file_storage();// Remember original file source field.$source = @unserialize($existingfile->get_source() ?? '');// Remember the original sortorder.$sortorder = $existingfile->get_sortorder();if ($newfile->is_external_file()) {// New file is a reference. Check that existing file does not have any other files referencing to itif (isset($source->original) && $fs->search_references_count($source->original)) {throw new moodle_exception('errordoublereference', 'repository');}}// Delete existing file to release filename.$newfilerecord = array('contextid' => $existingfile->get_contextid(),'component' => 'user','filearea' => 'draft','itemid' => $existingfile->get_itemid(),'timemodified' => time());$existingfile->delete();// Create new file.$newfile = $fs->create_file_from_storedfile($newfilerecord, $newfile);// Preserve original file location (stored in source field) for handling references.if (isset($source->original)) {if (!($newfilesource = @unserialize($newfile->get_source() ?? ''))) {$newfilesource = new stdClass();}$newfilesource->original = $source->original;$newfile->set_source(serialize($newfilesource));}$newfile->set_sortorder($sortorder);}/*** Add files from a draft area into a final area.** Most of the time you do not want to use this. It is intended to be used* by asynchronous services which cannot direcly manipulate a final* area through a draft area. Instead they add files to a new draft* area and merge that new draft into the final area when ready.** @param int $draftitemid the id of the draft area to use.* @param int $contextid this parameter and the next two identify the file area to save to.* @param string $component component name* @param string $filearea indentifies the file area* @param int $itemid identifies the item id or false for all items in the file area* @param array $options area options (subdirs=false, maxfiles=-1, maxbytes=0, areamaxbytes=FILE_AREA_MAX_BYTES_UNLIMITED)* @see file_save_draft_area_files* @since Moodle 3.2*/function file_merge_files_from_draft_area_into_filearea($draftitemid, $contextid, $component, $filearea, $itemid,array $options = null) {// We use 0 here so file_prepare_draft_area creates a new one, finaldraftid will be updated with the new draft id.$finaldraftid = 0;file_prepare_draft_area($finaldraftid, $contextid, $component, $filearea, $itemid, $options);file_merge_draft_area_into_draft_area($draftitemid, $finaldraftid);file_save_draft_area_files($finaldraftid, $contextid, $component, $filearea, $itemid, $options);}/*** Merge files from two draftarea areas.** This does not handle conflict resolution, files in the destination area which appear* to be more recent will be kept disregarding the intended ones.** @param int $getfromdraftid the id of the draft area where are the files to merge.* @param int $mergeintodraftid the id of the draft area where new files will be merged.* @throws coding_exception* @since Moodle 3.2*/function file_merge_draft_area_into_draft_area($getfromdraftid, $mergeintodraftid) {global $USER;$fs = get_file_storage();$contextid = context_user::instance($USER->id)->id;if (!$filestomerge = $fs->get_area_files($contextid, 'user', 'draft', $getfromdraftid)) {throw new coding_exception('Nothing to merge or area does not belong to current user');}$currentfiles = $fs->get_area_files($contextid, 'user', 'draft', $mergeintodraftid);// Get hashes of the files to merge.$newhashes = array();foreach ($filestomerge as $filetomerge) {$filepath = $filetomerge->get_filepath();$filename = $filetomerge->get_filename();$newhash = $fs->get_pathname_hash($contextid, 'user', 'draft', $mergeintodraftid, $filepath, $filename);$newhashes[$newhash] = $filetomerge;}// Calculate wich files must be added.foreach ($currentfiles as $file) {$filehash = $file->get_pathnamehash();// One file to be merged already exists.if (isset($newhashes[$filehash])) {$updatedfile = $newhashes[$filehash];// Avoid race conditions.if ($file->get_timemodified() > $updatedfile->get_timemodified()) {// The existing file is more recent, do not copy the suposedly "new" one.unset($newhashes[$filehash]);continue;}// Update existing file (not only content, meta-data too).file_overwrite_existing_draftfile($updatedfile, $file);unset($newhashes[$filehash]);}}foreach ($newhashes as $newfile) {$newfilerecord = array('contextid' => $contextid,'component' => 'user','filearea' => 'draft','itemid' => $mergeintodraftid,'timemodified' => time());$fs->create_file_from_storedfile($newfilerecord, $newfile);}}/*** Attempt to determine whether the specified mime-type is an SVG image or not.** @param string $mimetype Mime-type* @return bool True if it is an SVG file*/function file_is_svg_image_from_mimetype(string $mimetype): bool {return preg_match('|^image/svg|', $mimetype);}/*** Returns the moodle proxy configuration as a formatted url** @return string the string to use for proxy settings.*/function get_moodle_proxy_url() {global $CFG;$proxy = '';if (empty($CFG->proxytype)) {return $proxy;}if (empty($CFG->proxyhost)) {return $proxy;}if ($CFG->proxytype === 'SOCKS5') {// If it is a SOCKS proxy, append the protocol info.$protocol = 'socks5://';} else {$protocol = '';}$proxy = $CFG->proxyhost;if (!empty($CFG->proxyport)) {$proxy .= ':'. $CFG->proxyport;}if (!empty($CFG->proxyuser) && !empty($CFG->proxypassword)) {$proxy = $protocol . $CFG->proxyuser . ':' . $CFG->proxypassword . '@' . $proxy;}return $proxy;}/*** RESTful cURL class** This is a wrapper class for curl, it is quite easy to use:* <code>* $c = new curl;* // enable cache* $c = new curl(array('cache'=>true));* // enable cookie* $c = new curl(array('cookie'=>true));* // enable proxy* $c = new curl(array('proxy'=>true));** // HTTP GET Method* $html = $c->get('http://example.com');* // HTTP POST Method* $html = $c->post('http://example.com/', array('q'=>'words', 'name'=>'moodle'));* // HTTP PUT Method* $html = $c->put('http://example.com/', array('file'=>'/var/www/test.txt');* </code>** @package core_files* @category files* @copyright Dongsheng Cai <dongsheng@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU Public License*/class curl {/** @var curl_cache|false Caches http request contents */public $cache = false;/** @var bool Uses proxy, null means automatic based on URL */public $proxy = null;/** @var string library version */public $version = '0.4 dev';/** @var array http's response */public $response = array();/** @var array Raw response headers, needed for BC in download_file_content(). */public $rawresponse = array();/** @var array http header */public $header = array();/** @var array cURL information */public $info;/** @var string error */public $error;/** @var int error code */public $errno;/** @var bool Perform redirects at PHP level instead of relying on native cURL functionality. Always true now. */public $emulateredirects = null;/** @var array cURL options */private $options;/** @var string Proxy host */private $proxy_host = '';/** @var string Proxy auth */private $proxy_auth = '';/** @var string Proxy type */private $proxy_type = '';/** @var bool Debug mode on */private $debug = false;/** @var bool|string Path to cookie file */private $cookie = false;/** @var bool tracks multiple headers in response - redirect detection */private $responsefinished = false;/** @var ?\core\files\curl_security_helper security helper class, responsible for checking host/ports against allowed/blocked entries.*/private $securityhelper;/** @var bool ignoresecurity a flag which can be supplied to the constructor, allowing security to be bypassed. */private $ignoresecurity;/** @var array $mockresponses For unit testing only - return the head of this list instead of making the next request. */private static $mockresponses = [];/** @var array temporary params value if the value is not belongs to class stored_file. */public $_tmp_file_post_params = [];/*** Curl constructor.** Allowed settings are:* proxy: (bool) use proxy server, null means autodetect non-local from url* debug: (bool) use debug output* cookie: (string) path to cookie file, false if none* cache: (bool) use cache* module_cache: (string) type of cache* securityhelper: (\core\files\curl_security_helper_base) helper object providing URL checking for requests.* ignoresecurity: (bool) set true to override and ignore the security helper when making requests.** @param array $settings*/public function __construct($settings = array()) {global $CFG;if (!function_exists('curl_init')) {$this->error = 'cURL module must be enabled!';trigger_error($this->error, E_USER_ERROR);return;}// All settings of this class should be init here.$this->resetopt();if (!empty($settings['debug'])) {$this->debug = true;}if (!empty($settings['cookie'])) {if($settings['cookie'] === true) {$this->cookie = $CFG->dataroot.'/curl_cookie.txt';} else {$this->cookie = $settings['cookie'];}}if (!empty($settings['cache'])) {if (class_exists('curl_cache')) {if (!empty($settings['module_cache'])) {$this->cache = new curl_cache($settings['module_cache']);} else {$this->cache = new curl_cache('misc');}}}if (!empty($CFG->proxyhost)) {if (empty($CFG->proxyport)) {$this->proxy_host = $CFG->proxyhost;} else {$this->proxy_host = $CFG->proxyhost.':'.$CFG->proxyport;}if (!empty($CFG->proxyuser) and !empty($CFG->proxypassword)) {$this->proxy_auth = $CFG->proxyuser.':'.$CFG->proxypassword;$this->setopt(array('proxyauth'=> CURLAUTH_BASIC | CURLAUTH_NTLM,'proxyuserpwd'=>$this->proxy_auth));}if (!empty($CFG->proxytype)) {if ($CFG->proxytype == 'SOCKS5') {$this->proxy_type = CURLPROXY_SOCKS5;} else {$this->proxy_type = CURLPROXY_HTTP;$this->setopt(['httpproxytunnel' => false,]);if (defined('CURLOPT_SUPPRESS_CONNECT_HEADERS')) {$this->setopt(['suppress_connect_headers' => true,]);}}$this->setopt(array('proxytype'=>$this->proxy_type));}if (isset($settings['proxy'])) {$this->proxy = $settings['proxy'];}} else {$this->proxy = false;}// All redirects are performed at PHP level now and each one is checked against blocked URLs rules. We do not// want to let cURL naively follow the redirect chain and visit every URL for security reasons. Even when the// caller explicitly wants to ignore the security checks, we would need to fall back to the original// implementation and use emulated redirects if open_basedir is in effect to avoid the PHP warning// "CURLOPT_FOLLOWLOCATION cannot be activated when in safe_mode or an open_basedir". So it is better to simply// ignore this property and always handle redirects at this PHP wrapper level and not inside the native cURL.$this->emulateredirects = true;// Curl security setup. Allow injection of a security helper, but if not found, default to the core helper.if (isset($settings['securityhelper']) && $settings['securityhelper'] instanceof \core\files\curl_security_helper_base) {$this->set_security($settings['securityhelper']);} else {$this->set_security(new \core\files\curl_security_helper());}$this->ignoresecurity = isset($settings['ignoresecurity']) ? $settings['ignoresecurity'] : false;}/*** Resets the CURL options that have already been set*/public function resetopt() {$this->options = array();$this->options['CURLOPT_USERAGENT'] = \core_useragent::get_moodlebot_useragent();// True to include the header in the output$this->options['CURLOPT_HEADER'] = 0;// True to Exclude the body from the output$this->options['CURLOPT_NOBODY'] = 0;// Redirect ny default.$this->options['CURLOPT_FOLLOWLOCATION'] = 1;$this->options['CURLOPT_MAXREDIRS'] = 10;$this->options['CURLOPT_ENCODING'] = '';// TRUE to return the transfer as a string of the return// value of curl_exec() instead of outputting it out directly.$this->options['CURLOPT_RETURNTRANSFER'] = 1;$this->options['CURLOPT_SSL_VERIFYPEER'] = 0;$this->options['CURLOPT_SSL_VERIFYHOST'] = 2;$this->options['CURLOPT_CONNECTTIMEOUT'] = 30;if ($cacert = self::get_cacert()) {$this->options['CURLOPT_CAINFO'] = $cacert;}}/*** Get the location of ca certificates.* @return string absolute file path or empty if default used*/public static function get_cacert() {global $CFG;// Bundle in dataroot always wins.if (is_readable("$CFG->dataroot/moodleorgca.crt")) {return realpath("$CFG->dataroot/moodleorgca.crt");}// Next comes the default from php.ini$cacert = ini_get('curl.cainfo');if (!empty($cacert) and is_readable($cacert)) {return realpath($cacert);}// Windows PHP does not have any certs, we need to use something.if ($CFG->ostype === 'WINDOWS') {if (is_readable("$CFG->libdir/cacert.pem")) {return realpath("$CFG->libdir/cacert.pem");}}// Use default, this should work fine on all properly configured *nix systems.return null;}/*** Reset Cookie*/public function resetcookie() {if (!empty($this->cookie)) {if (is_file($this->cookie)) {$fp = fopen($this->cookie, 'w');if (!empty($fp)) {fwrite($fp, '');fclose($fp);}}}}/*** Set curl options.** Do not use the curl constants to define the options, pass a string* corresponding to that constant. Ie. to set CURLOPT_MAXREDIRS, pass* array('CURLOPT_MAXREDIRS' => 10) or array('maxredirs' => 10) to this method.** @param array $options If array is null, this function will reset the options to default value.* @return void* @throws coding_exception If an option uses constant value instead of option name.*/public function setopt($options = array()) {if (is_array($options)) {foreach ($options as $name => $val) {if (!is_string($name)) {throw new coding_exception('Curl options should be defined using strings, not constant values.');}if (stripos($name, 'CURLOPT_') === false) {// Only prefix with CURLOPT_ if the option doesn't contain CURLINFO_,// which is a valid prefix for at least one option CURLINFO_HEADER_OUT.if (stripos($name, 'CURLINFO_') === false) {$name = strtoupper('CURLOPT_'.$name);}} else {$name = strtoupper($name);}$this->options[$name] = $val;}}}/*** Reset http method*/public function cleanopt() {unset($this->options['CURLOPT_HTTPGET']);unset($this->options['CURLOPT_POST']);unset($this->options['CURLOPT_POSTFIELDS']);unset($this->options['CURLOPT_PUT']);unset($this->options['CURLOPT_INFILE']);unset($this->options['CURLOPT_INFILESIZE']);unset($this->options['CURLOPT_CUSTOMREQUEST']);unset($this->options['CURLOPT_FILE']);}/*** Resets the HTTP Request headers (to prepare for the new request)*/public function resetHeader() {$this->header = array();}/*** Set HTTP Request Header** @param array|string $header*/public function setHeader($header) {if (is_array($header)) {foreach ($header as $v) {$this->setHeader($v);}} else {// Remove newlines, they are not allowed in headers.$newvalue = preg_replace('/[\r\n]/', '', $header);if (!in_array($newvalue, $this->header)) {$this->header[] = $newvalue;}}}/*** Get HTTP Response Headers* @return array of arrays*/public function getResponse() {return $this->response;}/*** Get raw HTTP Response Headers* @return array of strings*/public function get_raw_response() {return $this->rawresponse;}/*** private callback function* Formatting HTTP Response Header** We only keep the last headers returned. For example during a redirect the* redirect headers will not appear in {@link self::getResponse()}, if you need* to use those headers, refer to {@link self::get_raw_response()}.** @param resource $ch Apparently not used* @param string $header* @return int The strlen of the header*/private function formatHeader($ch, $header) {$this->rawresponse[] = $header;if (trim($header, "\r\n") === '') {// This must be the last header.$this->responsefinished = true;}if (strlen($header) > 2) {if ($this->responsefinished) {// We still have headers after the supposedly last header, we must be// in a redirect so let's empty the response to keep the last headers.$this->responsefinished = false;$this->response = array();}$parts = explode(" ", rtrim($header, "\r\n"), 2);$key = rtrim($parts[0], ':');$value = isset($parts[1]) ? $parts[1] : null;if (!empty($this->response[$key])) {if (is_array($this->response[$key])) {$this->response[$key][] = $value;} else {$tmp = $this->response[$key];$this->response[$key] = array();$this->response[$key][] = $tmp;$this->response[$key][] = $value;}} else {$this->response[$key] = $value;}}return strlen($header);}/*** Set options for individual curl instance** @param resource|CurlHandle $curl A curl handle* @param array $options* @return resource The curl handle*/private function apply_opt($curl, $options) {// Clean up$this->cleanopt();// set cookieif (!empty($this->cookie) || !empty($options['cookie'])) {$this->setopt(array('cookiejar'=>$this->cookie,'cookiefile'=>$this->cookie));}// Bypass proxy if required.if ($this->proxy === null) {if (!empty($this->options['CURLOPT_URL']) and is_proxybypass($this->options['CURLOPT_URL'])) {$proxy = false;} else {$proxy = true;}} else {$proxy = (bool)$this->proxy;}// Set proxy.if ($proxy) {$options['CURLOPT_PROXY'] = $this->proxy_host;} else {unset($this->options['CURLOPT_PROXY']);}$this->setopt($options);// Reset before set options.curl_setopt($curl, CURLOPT_HEADERFUNCTION, array(&$this,'formatHeader'));// Setting the User-Agent based on options provided.$useragent = '';if (!empty($options['CURLOPT_USERAGENT'])) {$useragent = $options['CURLOPT_USERAGENT'];} else if (!empty($this->options['CURLOPT_USERAGENT'])) {$useragent = $this->options['CURLOPT_USERAGENT'];} else {$useragent = \core_useragent::get_moodlebot_useragent();}// Set headers.if (empty($this->header)) {$this->setHeader(array('User-Agent: ' . $useragent,'Connection: keep-alive'));} else if (!in_array('User-Agent: ' . $useragent, $this->header)) {// Remove old User-Agent if one existed.// We have to partial search since we don't know what the original User-Agent is.if ($match = preg_grep('/User-Agent.*/', $this->header)) {$key = array_keys($match)[0];unset($this->header[$key]);}$this->setHeader(array('User-Agent: ' . $useragent));}curl_setopt($curl, CURLOPT_HTTPHEADER, $this->header);if ($this->debug) {echo '<h1>Options</h1>';var_dump($this->options);echo '<h1>Header</h1>';var_dump($this->header);}// Do not allow infinite redirects.if (!isset($this->options['CURLOPT_MAXREDIRS'])) {$this->options['CURLOPT_MAXREDIRS'] = 0;} else if ($this->options['CURLOPT_MAXREDIRS'] > 100) {$this->options['CURLOPT_MAXREDIRS'] = 100;} else {$this->options['CURLOPT_MAXREDIRS'] = (int)$this->options['CURLOPT_MAXREDIRS'];}// Make sure we always know if redirects expected.if (!isset($this->options['CURLOPT_FOLLOWLOCATION'])) {$this->options['CURLOPT_FOLLOWLOCATION'] = 0;}// Limit the protocols to HTTP and HTTPS.if (defined('CURLOPT_PROTOCOLS')) {$this->options['CURLOPT_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);$this->options['CURLOPT_REDIR_PROTOCOLS'] = (CURLPROTO_HTTP | CURLPROTO_HTTPS);}// Set options.foreach($this->options as $name => $val) {if ($name === 'CURLOPT_FOLLOWLOCATION') {// All the redirects are emulated at PHP level.curl_setopt($curl, CURLOPT_FOLLOWLOCATION, 0);continue;}$name = constant($name);curl_setopt($curl, $name, $val);}return $curl;}/*** Download multiple files in parallel** Calls {@link multi()} with specific download headers** <code>* $c = new curl();* $file1 = fopen('a', 'wb');* $file2 = fopen('b', 'wb');* $c->download(array(* array('url'=>'http://localhost/', 'file'=>$file1),* array('url'=>'http://localhost/20/', 'file'=>$file2)* ));* fclose($file1);* fclose($file2);* </code>** or** <code>* $c = new curl();* $c->download(array(* array('url'=>'http://localhost/', 'filepath'=>'/tmp/file1.tmp'),* array('url'=>'http://localhost/20/', 'filepath'=>'/tmp/file2.tmp')* ));* </code>** @param array $requests An array of files to request {* url => url to download the file [required]* file => file handler, or* filepath => file path* }* If 'file' and 'filepath' parameters are both specified in one request, the* open file handle in the 'file' parameter will take precedence and 'filepath'* will be ignored.** @param array $options An array of options to set* @return array An array of results*/public function download($requests, $options = array()) {$options['RETURNTRANSFER'] = false;return $this->multi($requests, $options);}/*** Returns the current curl security helper.** @return \core\files\curl_security_helper instance.*/public function get_security() {return $this->securityhelper;}/*** Sets the curl security helper.** @param \core\files\curl_security_helper $securityobject instance/subclass of the base curl_security_helper class.* @return bool true if the security helper could be set, false otherwise.*/public function set_security($securityobject) {if ($securityobject instanceof \core\files\curl_security_helper) {$this->securityhelper = $securityobject;return true;}return false;}/*** Multi HTTP Requests* This function could run multi-requests in parallel.** @param array $requests An array of files to request* @param array $options An array of options to set* @return array An array of results*/protected function multi($requests, $options = array()) {$count = count($requests);$handles = array();$results = array();$main = curl_multi_init();for ($i = 0; $i < $count; $i++) {if (!empty($requests[$i]['filepath']) and empty($requests[$i]['file'])) {// open file$requests[$i]['file'] = fopen($requests[$i]['filepath'], 'w');$requests[$i]['auto-handle'] = true;}foreach($requests[$i] as $n=>$v) {$options[$n] = $v;}$handles[$i] = curl_init($requests[$i]['url']);$this->apply_opt($handles[$i], $options);curl_multi_add_handle($main, $handles[$i]);}$running = 0;do {curl_multi_exec($main, $running);} while($running > 0);for ($i = 0; $i < $count; $i++) {if (!empty($options['CURLOPT_RETURNTRANSFER'])) {$results[] = true;} else {$results[] = curl_multi_getcontent($handles[$i]);}curl_multi_remove_handle($main, $handles[$i]);}curl_multi_close($main);for ($i = 0; $i < $count; $i++) {if (!empty($requests[$i]['filepath']) and !empty($requests[$i]['auto-handle'])) {// close file handler if file is opened in this functionfclose($requests[$i]['file']);}}return $results;}/*** Helper function to reset the request state vars.** @return void.*/protected function reset_request_state_vars() {$this->info = array();$this->error = '';$this->errno = 0;$this->response = array();$this->rawresponse = array();$this->responsefinished = false;}/*** For use only in unit tests - we can pre-set the next curl response.* This is useful for unit testing APIs that call external systems.* @param string $response*/public static function mock_response($response) {if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {array_push(self::$mockresponses, $response);} else {throw new coding_exception('mock_response function is only available for unit tests.');}}/*** check_securityhelper_blocklist.* Checks whether the given URL is blocked by checking both plugin's security helpers* and core curl security helper or any curl security helper that passed to curl class constructor.* If ignoresecurity is set to true, skip checking and consider the url is not blocked.* This augments all installed plugin's security helpers if there is any.** @param string $url the url to check.* @return ?string - an error message if URL is blocked or null if URL is not blocked.*/protected function check_securityhelper_blocklist(string $url): ?string {// If curl security is not enabled, do not proceed.if ($this->ignoresecurity) {return null;}// Augment all installed plugin's security helpers if there is any.// The plugin's function has to be defined as plugintype_pluginname_curl_security_helper in pluginname/lib.php.$plugintypes = get_plugins_with_function('curl_security_helper');// If any of the security helper's function returns true, treat as URL is blocked.foreach ($plugintypes as $plugins) {foreach ($plugins as $pluginfunction) {// Get curl security helper object from plugin lib.php.$pluginsecurityhelper = $pluginfunction();if ($pluginsecurityhelper instanceof \core\files\curl_security_helper_base) {if ($pluginsecurityhelper->url_is_blocked($url)) {$this->error = $pluginsecurityhelper->get_blocked_url_string();return $this->error;}}}}// Check if the URL is blocked in core curl_security_helper or// curl security helper that passed to curl class constructor.if ($this->securityhelper->url_is_blocked($url)) {$this->error = $this->securityhelper->get_blocked_url_string();return $this->error;}return null;}/*** Single HTTP Request** @param string $url The URL to request* @param array $options* @return string*/protected function request($url, $options = array()) {// Reset here so that the data is valid when result returned from cache, or if we return due to a blocked URL hit.$this->reset_request_state_vars();if ((defined('PHPUNIT_TEST') && PHPUNIT_TEST)) {$mockresponse = array_pop(self::$mockresponses);if ($mockresponse !== null) {$this->info = [ 'http_code' => 200 ];return $mockresponse;}}if (empty($this->emulateredirects)) {// Just in case someone had tried to explicitly disable emulated redirects in legacy code.debugging('Attempting to disable emulated redirects has no effect any more!', DEBUG_DEVELOPER);}$urlisblocked = $this->check_securityhelper_blocklist($url);if (!is_null($urlisblocked)) {$this->trigger_url_blocked_event($url, $urlisblocked);return $urlisblocked;}// Set the URL as a curl option.$this->setopt(array('CURLOPT_URL' => $url));// Create curl instance.$curl = curl_init();$this->apply_opt($curl, $options);if ($this->cache && $ret = $this->cache->get($this->options)) {return $ret;}$ret = curl_exec($curl);$this->info = curl_getinfo($curl);$this->error = curl_error($curl);$this->errno = curl_errno($curl);// Note: $this->response and $this->rawresponse are filled by $hits->formatHeader callback.if (intval($this->info['redirect_count']) > 0) {// For security reasons we do not allow the cURL handle to follow redirects on its own.// See setting CURLOPT_FOLLOWLOCATION in {@see self::apply_opt()} method.throw new coding_exception('Internal cURL handle should never follow redirects on its own!','Reported number of redirects: ' . $this->info['redirect_count']);}if ($this->options['CURLOPT_FOLLOWLOCATION'] && $this->info['http_code'] != 200) {$redirects = 0;while($redirects <= $this->options['CURLOPT_MAXREDIRS']) {if ($this->info['http_code'] == 301) {// Moved Permanently - repeat the same request on new URL.} else if ($this->info['http_code'] == 302) {// Found - the standard redirect - repeat the same request on new URL.} else if ($this->info['http_code'] == 303) {// 303 See Other - repeat only if GET, do not bother with POSTs.if (empty($this->options['CURLOPT_HTTPGET'])) {break;}} else if ($this->info['http_code'] == 307) {// Temporary Redirect - must repeat using the same request type.} else if ($this->info['http_code'] == 308) {// Permanent Redirect - must repeat using the same request type.} else {// Some other http code means do not retry!break;}$redirects++;$currenturl = $redirecturl ?? $url;$redirecturl = null;if (isset($this->info['redirect_url'])) {if (preg_match('|^https?://|i', $this->info['redirect_url'])) {$redirecturl = $this->info['redirect_url'];} else {// Emulate CURLOPT_REDIR_PROTOCOLS behaviour which we have set to (CURLPROTO_HTTP | CURLPROTO_HTTPS) only.$this->errno = CURLE_UNSUPPORTED_PROTOCOL;$this->error = 'Redirect to a URL with unsuported protocol: ' . $this->info['redirect_url'];curl_close($curl);return $this->error;}}if (!$redirecturl) {foreach ($this->response as $k => $v) {if (strtolower($k) === 'location') {$redirecturl = $v;break;}}if (preg_match('|^https?://|i', $redirecturl)) {// Great, this is the correct location format!} else if ($redirecturl) {$current = curl_getinfo($curl, CURLINFO_EFFECTIVE_URL);if (strpos($redirecturl, '/') === 0) {// Relative to server root - just guess.$pos = strpos('/', $current, 8);if ($pos === false) {$redirecturl = $current.$redirecturl;} else {$redirecturl = substr($current, 0, $pos).$redirecturl;}} else {// Relative to current script.$redirecturl = dirname($current).'/'.$redirecturl;}}}$urlisblocked = $this->check_securityhelper_blocklist($redirecturl);if (!is_null($urlisblocked)) {$this->reset_request_state_vars();curl_close($curl);$this->trigger_url_blocked_event($redirecturl, $urlisblocked, true);return $urlisblocked;}// If the response body is written to a seekable stream resource, reset the stream pointer to avoid// appending multiple response bodies to the same resource.if (!empty($this->options['CURLOPT_FILE'])) {$streammetadata = stream_get_meta_data($this->options['CURLOPT_FILE']);if ($streammetadata['seekable']) {ftruncate($this->options['CURLOPT_FILE'], 0);rewind($this->options['CURLOPT_FILE']);}}curl_setopt($curl, CURLOPT_URL, $redirecturl);// If CURLOPT_UNRESTRICTED_AUTH is empty/false, don't send credentials to other hosts.// Ref: https://curl.se/libcurl/c/CURLOPT_UNRESTRICTED_AUTH.html.$isdifferenthost = parse_url($currenturl)['host'] !== parse_url($redirecturl)['host'];$sendauthentication = !empty($this->options['CURLOPT_UNRESTRICTED_AUTH']);if ($isdifferenthost && !$sendauthentication) {curl_setopt($curl, CURLOPT_HTTPAUTH, null);curl_setopt($curl, CURLOPT_USERPWD, null);// Check whether the CURLOPT_HTTPHEADER is specified.if (!empty($this->options['CURLOPT_HTTPHEADER'])) {// Remove the "Authorization:" header, if any.$headerredirect = array_filter($this->options['CURLOPT_HTTPHEADER'],fn($header) => strpos($header, 'Authorization:') === false);curl_setopt($curl, CURLOPT_HTTPHEADER, $headerredirect);}}$ret = curl_exec($curl);$this->info = curl_getinfo($curl);$this->error = curl_error($curl);$this->errno = curl_errno($curl);$this->info['redirect_count'] = $redirects;if ($this->info['http_code'] === 200) {// Finally this is what we wanted.break;}if ($this->errno != CURLE_OK) {// Something wrong is going on.break;}}if ($redirects > $this->options['CURLOPT_MAXREDIRS']) {$this->errno = CURLE_TOO_MANY_REDIRECTS;$this->error = 'Maximum ('.$this->options['CURLOPT_MAXREDIRS'].') redirects followed';}}if ($this->cache) {$this->cache->set($this->options, $ret);}if ($this->debug) {echo '<h1>Return Data</h1>';var_dump($ret);echo '<h1>Info</h1>';var_dump($this->info);echo '<h1>Error</h1>';var_dump($this->error);}curl_close($curl);if (empty($this->error)) {return $ret;} else {return $this->error;// exception is not ajax friendly//throw new moodle_exception($this->error, 'curl');}}/*** Trigger url_blocked event** @param string $url The URL to request* @param string $reason Reason for blocking* @param bool $redirect true if it was a redirect*/private function trigger_url_blocked_event($url, $reason, $redirect = false): void {$params = ['url' => $url,'reason' => $reason,'redirect' => $redirect,];$event = core\event\url_blocked::create(['other' => $params]);$event->trigger();}/*** HTTP HEAD method** @see request()** @param string $url* @param array $options* @return string*/public function head($url, $options = array()) {$options['CURLOPT_HTTPGET'] = 0;$options['CURLOPT_HEADER'] = 1;$options['CURLOPT_NOBODY'] = 1;return $this->request($url, $options);}/*** HTTP PATCH method** @param string $url* @param array|string $params* @param array $options* @return string*/public function patch($url, $params = '', $options = array()) {$options['CURLOPT_CUSTOMREQUEST'] = 'PATCH';if (is_array($params)) {$this->_tmp_file_post_params = array();foreach ($params as $key => $value) {if ($value instanceof stored_file) {$value->add_to_curl_request($this, $key);} else {$this->_tmp_file_post_params[$key] = $value;}}$options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params;unset($this->_tmp_file_post_params);} else {// The variable $params is the raw post data.$options['CURLOPT_POSTFIELDS'] = $params;}return $this->request($url, $options);}/*** HTTP POST method** @param string $url* @param array|string $params* @param array $options* @return string*/public function post($url, $params = '', $options = array()) {$options['CURLOPT_POST'] = 1;if (is_array($params)) {$this->_tmp_file_post_params = array();foreach ($params as $key => $value) {if ($value instanceof stored_file) {$value->add_to_curl_request($this, $key);} else {$this->_tmp_file_post_params[$key] = $value;}}$options['CURLOPT_POSTFIELDS'] = $this->_tmp_file_post_params;unset($this->_tmp_file_post_params);} else {// $params is the raw post data$options['CURLOPT_POSTFIELDS'] = $params;}return $this->request($url, $options);}/*** HTTP GET method** @param string $url* @param ?array $params* @param array $options* @return string*/public function get($url, $params = array(), $options = array()) {$options['CURLOPT_HTTPGET'] = 1;if (!empty($params)) {$url .= (stripos($url, '?') !== false) ? '&' : '?';$url .= http_build_query($params, '', '&');}return $this->request($url, $options);}/*** Downloads one file and writes it to the specified file handler** <code>* $c = new curl();* $file = fopen('savepath', 'w');* $result = $c->download_one('http://localhost/', null,* array('file' => $file, 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));* fclose($file);* $download_info = $c->get_info();* if ($result === true) {* // file downloaded successfully* } else {* $error_text = $result;* $error_code = $c->get_errno();* }* </code>** <code>* $c = new curl();* $result = $c->download_one('http://localhost/', null,* array('filepath' => 'savepath', 'timeout' => 5, 'followlocation' => true, 'maxredirs' => 3));* // ... see above, no need to close handle and remove file if unsuccessful* </code>** @param string $url* @param array|null $params key-value pairs to be added to $url as query string* @param array $options request options. Must include either 'file' or 'filepath'* @return bool|string true on success or error string on failure*/public function download_one($url, $params, $options = array()) {$options['CURLOPT_HTTPGET'] = 1;if (!empty($params)) {$url .= (stripos($url, '?') !== false) ? '&' : '?';$url .= http_build_query($params, '', '&');}if (!empty($options['filepath']) && empty($options['file'])) {// open fileif (!($options['file'] = fopen($options['filepath'], 'w'))) {$this->errno = 100;return get_string('cannotwritefile', 'error', $options['filepath']);}$filepath = $options['filepath'];}unset($options['filepath']);$result = $this->request($url, $options);if (isset($filepath)) {fclose($options['file']);if ($result !== true) {unlink($filepath);}}return $result;}/*** HTTP PUT method** @param string $url* @param array $params* @param array $options* @return ?string*/public function put($url, $params = array(), $options = array()) {$file = '';$fp = false;if (isset($params['file'])) {$file = $params['file'];if (is_file($file)) {$fp = fopen($file, 'r');$size = filesize($file);$options['CURLOPT_PUT'] = 1;$options['CURLOPT_INFILESIZE'] = $size;$options['CURLOPT_INFILE'] = $fp;} else {return null;}if (!isset($this->options['CURLOPT_USERPWD'])) {$this->setopt(array('CURLOPT_USERPWD' => 'anonymous: noreply@moodle.org'));}} else {$options['CURLOPT_CUSTOMREQUEST'] = 'PUT';$options['CURLOPT_POSTFIELDS'] = $params;}$ret = $this->request($url, $options);if ($fp !== false) {fclose($fp);}return $ret;}/*** HTTP DELETE method** @param string $url* @param array $param* @param array $options* @return string*/public function delete($url, $param = array(), $options = array()) {$options['CURLOPT_CUSTOMREQUEST'] = 'DELETE';if (!isset($options['CURLOPT_USERPWD'])) {$options['CURLOPT_USERPWD'] = 'anonymous: noreply@moodle.org';}$ret = $this->request($url, $options);return $ret;}/*** HTTP TRACE method** @param string $url* @param array $options* @return string*/public function trace($url, $options = array()) {$options['CURLOPT_CUSTOMREQUEST'] = 'TRACE';$ret = $this->request($url, $options);return $ret;}/*** HTTP OPTIONS method** @param string $url* @param array $options* @return string*/public function options($url, $options = array()) {$options['CURLOPT_CUSTOMREQUEST'] = 'OPTIONS';$ret = $this->request($url, $options);return $ret;}/*** Get curl information** @return array*/public function get_info() {return $this->info;}/*** Get curl error code** @return int*/public function get_errno() {return $this->errno;}/*** When using a proxy, an additional HTTP response code may appear at* the start of the header. For example, when using https over a proxy* there may be 'HTTP/1.0 200 Connection Established'. Other codes are* also possible and some may come with their own headers.** If using the return value containing all headers, this function can be* called to remove unwanted doubles.** Note that it is not possible to distinguish this situation from valid* data unless you know the actual response part (below the headers)* will not be included in this string, or else will not 'look like' HTTP* headers. As a result it is not safe to call this function for general* data.** @param string $input Input HTTP response* @return string HTTP response with additional headers stripped if any*/public static function strip_double_headers($input) {// I have tried to make this regular expression as specific as possible// to avoid any case where it does weird stuff if you happen to put// HTTP/1.1 200 at the start of any line in your RSS file. This should// also make it faster because it can abandon regex processing as soon// as it hits something that doesn't look like an http header. The// header definition is taken from RFC 822, except I didn't support// folding which is never used in practice.$crlf = "\r\n";return preg_replace(// HTTP version and status code (ignore value of code).'~^HTTP/[1-9](\.[0-9])?.*' . $crlf .// Header name: character between 33 and 126 decimal, except colon.// Colon. Header value: any character except \r and \n. CRLF.'(?:[\x21-\x39\x3b-\x7e]+:[^' . $crlf . ']+' . $crlf . ')*' .// Headers are terminated by another CRLF (blank line).$crlf .// Second HTTP status code, this time must be 200.'(HTTP/[1-9](\.[0-9])? 200)~', '$2', $input);}}/*** This class is used by cURL class, use case:** <code>* $CFG->repositorycacheexpire = 120;* $CFG->curlcache = 120;** $c = new curl(array('cache'=>true), 'module_cache'=>'repository');* $ret = $c->get('http://www.google.com');* </code>** @package core_files* @copyright Dongsheng Cai <dongsheng@moodle.com>* @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later*/class curl_cache {/** @var string Path to cache directory */public $dir = '';/** @var int the repositorycacheexpire config value. */private $ttl;/*** Constructor** @global stdClass $CFG* @param string $module which module is using curl_cache*/public function __construct($module = 'repository') {global $CFG;if (!empty($module)) {$this->dir = $CFG->cachedir.'/'.$module.'/';} else {$this->dir = $CFG->cachedir.'/misc/';}if (!file_exists($this->dir)) {mkdir($this->dir, $CFG->directorypermissions, true);}if ($module == 'repository') {if (empty($CFG->repositorycacheexpire)) {$CFG->repositorycacheexpire = 120;}$this->ttl = $CFG->repositorycacheexpire;} else {if (empty($CFG->curlcache)) {$CFG->curlcache = 120;}$this->ttl = $CFG->curlcache;}}/*** Get cached value** @global stdClass $CFG* @global stdClass $USER* @param mixed $param* @return bool|string*/public function get($param) {global $CFG, $USER;$this->cleanup($this->ttl);$filename = 'u'.$USER->id.'_'.md5(serialize($param));if(file_exists($this->dir.$filename)) {$lasttime = filemtime($this->dir.$filename);if (time()-$lasttime > $this->ttl) {return false;} else {$fp = fopen($this->dir.$filename, 'r');$size = filesize($this->dir.$filename);$content = fread($fp, $size);return unserialize($content);}}return false;}/*** Set cache value** @global object $CFG* @global object $USER* @param mixed $param* @param mixed $val*/public function set($param, $val) {global $CFG, $USER;$filename = 'u'.$USER->id.'_'.md5(serialize($param));$fp = fopen($this->dir.$filename, 'w');fwrite($fp, serialize($val));fclose($fp);@chmod($this->dir.$filename, $CFG->filepermissions);}/*** Remove cache files** @param int $expire The number of seconds before expiry*/public function cleanup($expire) {if ($dir = opendir($this->dir)) {while (false !== ($file = readdir($dir))) {if(!is_dir($file) && $file != '.' && $file != '..') {$lasttime = @filemtime($this->dir.$file);if (time() - $lasttime > $expire) {@unlink($this->dir.$file);}}}closedir($dir);}}/*** delete current user's cache file** @global object $CFG* @global object $USER*/public function refresh() {global $CFG, $USER;if ($dir = opendir($this->dir)) {while (false !== ($file = readdir($dir))) {if (!is_dir($file) && $file != '.' && $file != '..') {if (strpos($file, 'u'.$USER->id.'_') !== false) {@unlink($this->dir.$file);}}}}}}/*** This function delegates file serving to individual plugins** @param string $relativepath* @param bool $forcedownload* @param null|string $preview the preview mode, defaults to serving the original file* @param boolean $offline If offline is requested - don't serve a redirect to an external file, return a file suitable for viewing* offline (e.g. mobile app).* @param bool $embed Whether this file will be served embed into an iframe.* @todo MDL-31088 file serving improments*/function file_pluginfile($relativepath, $forcedownload, $preview = null, $offline = false, $embed = false) {global $DB, $CFG, $USER, $OUTPUT;// relative path must start with '/'if (!$relativepath) {throw new \moodle_exception('invalidargorconf');} else if ($relativepath[0] != '/') {throw new \moodle_exception('pathdoesnotstartslash');}// extract relative path components$args = explode('/', ltrim($relativepath, '/'));if (count($args) < 3) { // always at least context, component and fileareathrow new \moodle_exception('invalidarguments');}$contextid = (int)array_shift($args);$component = clean_param(array_shift($args), PARAM_COMPONENT);$filearea = clean_param(array_shift($args), PARAM_AREA);list($context, $course, $cm) = get_context_info_array($contextid);$fs = get_file_storage();$sendfileoptions = ['preview' => $preview, 'offline' => $offline, 'embed' => $embed];// ========================================================================================================================if ($component === 'blog') {// Blog file servingif ($context->contextlevel != CONTEXT_SYSTEM) {send_file_not_found();}if ($filearea !== 'attachment' and $filearea !== 'post') {send_file_not_found();}if (empty($CFG->enableblogs)) {throw new \moodle_exception('siteblogdisable', 'blog');}$entryid = (int)array_shift($args);if (!$entry = $DB->get_record('post', array('module'=>'blog', 'id'=>$entryid))) {send_file_not_found();}if ($CFG->bloglevel < BLOG_GLOBAL_LEVEL) {require_login();if (isguestuser()) {throw new \moodle_exception('noguest');}if ($CFG->bloglevel == BLOG_USER_LEVEL) {if ($USER->id != $entry->userid) {send_file_not_found();}}}if ($entry->publishstate === 'public') {if ($CFG->forcelogin) {require_login();}} else if ($entry->publishstate === 'site') {require_login();//ok} else if ($entry->publishstate === 'draft') {require_login();if ($USER->id != $entry->userid) {send_file_not_found();}}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, $component, $filearea, $entryid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}send_stored_file($file, 10*60, 0, true, $sendfileoptions); // download MUST be forced - security!// ========================================================================================================================} else if ($component === 'grade') {require_once($CFG->libdir . '/grade/constants.php');if (($filearea === 'outcome' or $filearea === 'scale') and $context->contextlevel == CONTEXT_SYSTEM) {// Global gradebook filesif ($CFG->forcelogin) {require_login();}$fullpath = "/$context->id/$component/$filearea/".implode('/', $args);if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea == GRADE_FEEDBACK_FILEAREA || $filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {if ($context->contextlevel != CONTEXT_MODULE) {send_file_not_found();}require_login($course, false);$gradeid = (int) array_shift($args);$filename = array_pop($args);if ($filearea == GRADE_HISTORY_FEEDBACK_FILEAREA) {$grade = $DB->get_record('grade_grades_history', ['id' => $gradeid]);} else {$grade = $DB->get_record('grade_grades', ['id' => $gradeid]);}if (!$grade) {send_file_not_found();}$iscurrentuser = $USER->id == $grade->userid;if (!$iscurrentuser) {$coursecontext = context_course::instance($course->id);if (!has_capability('moodle/grade:viewall', $coursecontext)) {send_file_not_found();}}$fullpath = "/$context->id/$component/$filearea/$gradeid/$filename";if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'tag') {if ($filearea === 'description' and $context->contextlevel == CONTEXT_SYSTEM) {// All tag descriptions are going to be public but we still need to respect forceloginif ($CFG->forcelogin) {require_login();}$fullpath = "/$context->id/tag/description/".implode('/', $args);if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, true, $sendfileoptions);} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'badges') {require_once($CFG->libdir . '/badgeslib.php');$badgeid = (int)array_shift($args);$badge = new badge($badgeid);$filename = array_pop($args);if ($filearea === 'badgeimage') {if ($filename !== 'f1' && $filename !== 'f2' && $filename !== 'f3') {send_file_not_found();}if (!$file = $fs->get_file($context->id, 'badges', 'badgeimage', $badge->id, '/', $filename.'.png')) {send_file_not_found();}\core\session\manager::write_close();send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'userbadge' and $context->contextlevel == CONTEXT_USER) {if (!$file = $fs->get_file($context->id, 'badges', 'userbadge', $badge->id, '/', $filename.'.png')) {send_file_not_found();}\core\session\manager::write_close();send_stored_file($file, 60*60, 0, true, $sendfileoptions);}// ========================================================================================================================} else if ($component === 'calendar') {if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_SYSTEM) {// All events here are public the one requirement is that we respect forceloginif ($CFG->forcelogin) {require_login();}// Get the event if from the args array$eventid = array_shift($args);// Load the event from the databaseif (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'eventtype'=>'site'))) {send_file_not_found();}// Get the file and serve if successful$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_USER) {// Must be logged in, if they are not then they obviously can't be this userrequire_login();// Don't want guests here, potentially saves a DB callif (isguestuser()) {send_file_not_found();}// Get the event if from the args array$eventid = array_shift($args);// Load the event from the database - user id must matchif (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'userid'=>$USER->id, 'eventtype'=>'user'))) {send_file_not_found();}// Get the file and serve if successful$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, true, $sendfileoptions);} else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSECAT) {if ($CFG->forcelogin) {require_login();}// Get category, this will also validate access.$category = core_course_category::get($context->instanceid);// Get the event ID from the args array, load event.$eventid = array_shift($args);$event = $DB->get_record('event', ['id' => (int) $eventid,'eventtype' => 'category','categoryid' => $category->id,]);if (!$event) {send_file_not_found();}// Retrieve file from storage, and serve.$filename = array_pop($args);$filepath = $args ? '/' . implode('/', $args) .'/' : '/';$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename);if (!$file || $file->is_directory()) {send_file_not_found();}// Unlock session during file serving.\core\session\manager::write_close();send_stored_file($file, HOURSECS, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'event_description' and $context->contextlevel == CONTEXT_COURSE) {// Respect forcelogin and require login unless this is the site.... it probably// should NEVER be the siteif ($CFG->forcelogin || $course->id != SITEID) {require_login($course);}// Must be able to at least view the course. This does not apply to the front page.if ($course->id != SITEID && (!is_enrolled($context)) && (!is_viewing($context))) {//TODO: hmm, do we really want to block guests here?send_file_not_found();}// Get the event id$eventid = array_shift($args);// Load the event from the database we need to check whether it is// a) valid course event// b) a group event// Group events use the course context (there is no group context)if (!$event = $DB->get_record('event', array('id'=>(int)$eventid, 'courseid'=>$course->id))) {send_file_not_found();}// If its a group event require either membership of view all groups capabilityif ($event->eventtype === 'group') {if (!has_capability('moodle/site:accessallgroups', $context) && !groups_is_member($event->groupid, $USER->id)) {send_file_not_found();}} else if ($event->eventtype === 'course' || $event->eventtype === 'site') {// Ok. Please note that the event type 'site' still uses a course context.} else {// Some other type.send_file_not_found();}// If we get this far we can serve the file$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, $component, $filearea, $eventid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'user') {if ($filearea === 'icon' and $context->contextlevel == CONTEXT_USER) {if (count($args) == 1) {$themename = theme_config::DEFAULT_THEME;$filename = array_shift($args);} else {$themename = array_shift($args);$filename = array_shift($args);}// fix file name automaticallyif ($filename !== 'f1' and $filename !== 'f2' and $filename !== 'f3') {$filename = 'f1';}if ((!empty($CFG->forcelogin) and !isloggedin()) ||(!empty($CFG->forceloginforprofileimage) && (!isloggedin() || isguestuser()))) {// protect images if login required and not logged in;// also if login is required for profile images and is not logged in or guest// do not use require_login() because it is expensive and not suitable here anyway$theme = theme_config::load($themename);redirect($theme->image_url('u/'.$filename, 'moodle')); // intentionally not cached}if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.png')) {if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', $filename.'.jpg')) {if ($filename === 'f3') {// f3 512x512px was introduced in 2.3, there might be only the smaller version.if (!$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.png')) {$file = $fs->get_file($context->id, 'user', 'icon', 0, '/', 'f1.jpg');}}}}if (!$file) {// bad reference - try to prevent future retries as hard as possible!if ($user = $DB->get_record('user', array('id'=>$context->instanceid), 'id, picture')) {if ($user->picture > 0) {$DB->set_field('user', 'picture', 0, array('id'=>$user->id));}}// no redirect here because it is not cached$theme = theme_config::load($themename);$imagefile = $theme->resolve_image_location('u/'.$filename, 'moodle', null);send_file($imagefile, basename($imagefile), 60*60*24*14);}$options = $sendfileoptions;if (empty($CFG->forcelogin) && empty($CFG->forceloginforprofileimage)) {// Profile images should be cache-able by both browsers and proxies according// to $CFG->forcelogin and $CFG->forceloginforprofileimage.$options['cacheability'] = 'public';}send_stored_file($file, 60*60*24*365, 0, false, $options); // enable long caching, there are many images on each page} else if ($filearea === 'private' and $context->contextlevel == CONTEXT_USER) {require_login();if (isguestuser()) {send_file_not_found();}if ($USER->id !== $context->instanceid) {send_file_not_found();}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, $component, $filearea, 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!} else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_USER) {if ($CFG->forcelogin) {require_login();}$userid = $context->instanceid;if (!empty($CFG->forceloginforprofiles)) {require_once("{$CFG->dirroot}/user/lib.php");require_login();// Verify the current user is able to view the profile of the supplied user anywhere.$user = core_user::get_user($userid);if (!user_can_view_profile($user, null, $context)) {send_file_not_found();}}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, $component, $filearea, 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!} else if ($filearea === 'profile' and $context->contextlevel == CONTEXT_COURSE) {$userid = (int)array_shift($args);$usercontext = context_user::instance($userid);if ($CFG->forcelogin) {require_login();}if (!empty($CFG->forceloginforprofiles)) {require_once("{$CFG->dirroot}/user/lib.php");require_login();// Verify the current user is able to view the profile of the supplied user in current course.$user = core_user::get_user($userid);if (!user_can_view_profile($user, $course, $usercontext)) {send_file_not_found();}}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($usercontext->id, 'user', 'profile', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!} else if ($filearea === 'backup' and $context->contextlevel == CONTEXT_USER) {require_login();if (isguestuser()) {send_file_not_found();}$userid = $context->instanceid;if ($USER->id != $userid) {send_file_not_found();}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'user', 'backup', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, true, $sendfileoptions); // must force download - security!} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'coursecat') {if ($context->contextlevel != CONTEXT_COURSECAT) {send_file_not_found();}if ($filearea === 'description') {if ($CFG->forcelogin) {// no login necessary - unless login forced everywhererequire_login();}// Check if user can view this category.if (!core_course_category::get($context->instanceid, IGNORE_MISSING)) {send_file_not_found();}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'coursecat', 'description', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'course') {if ($context->contextlevel != CONTEXT_COURSE) {send_file_not_found();}if ($filearea === 'summary' || $filearea === 'overviewfiles') {if ($CFG->forcelogin) {require_login();}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'course', $filearea, 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'section') {if ($CFG->forcelogin) {require_login($course);} else if ($course->id != SITEID) {require_login($course);}$sectionid = (int)array_shift($args);if (!$section = $DB->get_record('course_sections', array('id'=>$sectionid, 'course'=>$course->id))) {send_file_not_found();}$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'course', 'section', $sectionid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'generated') {if ($CFG->forcelogin) {require_login($course);} else if ($course->id != SITEID) {require_login($course);}$svg = $OUTPUT->get_generated_svg_for_id($course->id);\core\session\manager::write_close(); // Unlock session during file serving.send_file($svg, 'course.svg', 60 * 60, 0, true, $forcedownload);} else {send_file_not_found();}} else if ($component === 'cohort') {$cohortid = (int)array_shift($args);$cohort = $DB->get_record('cohort', array('id' => $cohortid), '*', MUST_EXIST);$cohortcontext = context::instance_by_id($cohort->contextid);// The context in the file URL must be either cohort context or context of the course underneath the cohort's context.if ($context->id != $cohort->contextid &&($context->contextlevel != CONTEXT_COURSE || !in_array($cohort->contextid, $context->get_parent_context_ids()))) {send_file_not_found();}// User is able to access cohort if they have view cap on cohort level or// the cohort is visible and they have view cap on course level.$canview = has_capability('moodle/cohort:view', $cohortcontext) ||($cohort->visible && has_capability('moodle/cohort:view', $context));if ($filearea === 'description' && $canview) {$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (($file = $fs->get_file($cohortcontext->id, 'cohort', 'description', $cohort->id, $filepath, $filename))&& !$file->is_directory()) {\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60 * 60, 0, $forcedownload, $sendfileoptions);}}send_file_not_found();} else if ($component === 'group') {if ($context->contextlevel != CONTEXT_COURSE) {send_file_not_found();}require_course_login($course, true, null, false);$groupid = (int)array_shift($args);$group = $DB->get_record('groups', array('id'=>$groupid, 'courseid'=>$course->id), '*', MUST_EXIST);if (($course->groupmodeforce and $course->groupmode == SEPARATEGROUPS) and !has_capability('moodle/site:accessallgroups', $context) and !groups_is_member($group->id, $USER->id)) {// do not allow access to separate group info if not member or teachersend_file_not_found();}if ($filearea === 'description') {require_login($course);$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'group', 'description', $group->id, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'icon') {$filename = array_pop($args);if ($filename !== 'f1' and $filename !== 'f2') {send_file_not_found();}if (!$file = $fs->get_file($context->id, 'group', 'icon', $group->id, '/', $filename.'.png')) {if (!$file = $fs->get_file($context->id, 'group', 'icon', $group->id, '/', $filename.'.jpg')) {send_file_not_found();}}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, false, $sendfileoptions);} else if ($filearea === 'generated') {if ($CFG->forcelogin) {require_login($course);} else if ($course->id != SITEID) {require_login($course);}$svg = $OUTPUT->get_generated_svg_for_id($group->id);\core\session\manager::write_close(); // Unlock session during file serving.send_file($svg, 'group.svg', 60 * 60, 0, true, $forcedownload);} else {send_file_not_found();}} else if ($component === 'grouping') {if ($context->contextlevel != CONTEXT_COURSE) {send_file_not_found();}require_login($course);$groupingid = (int)array_shift($args);// note: everybody has access to grouping desc images for nowif ($filearea === 'description') {$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'grouping', 'description', $groupingid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'backup') {if ($filearea === 'course' and $context->contextlevel == CONTEXT_COURSE) {require_login($course);require_capability('moodle/backup:downloadfile', $context);$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'backup', 'course', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'section' and $context->contextlevel == CONTEXT_COURSE) {require_login($course);require_capability('moodle/backup:downloadfile', $context);$sectionid = (int)array_shift($args);$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'backup', 'section', $sectionid, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close();send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'activity' and $context->contextlevel == CONTEXT_MODULE) {require_login($course, false, $cm);require_capability('moodle/backup:downloadfile', $context);$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'backup', 'activity', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close();send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);} else if ($filearea === 'automated' and $context->contextlevel == CONTEXT_COURSE) {// Backup files that were generated by the automated backup systems.require_login($course);require_capability('moodle/backup:downloadfile', $context);require_capability('moodle/restore:userinfo', $context);$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'backup', 'automated', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 0, 0, $forcedownload, $sendfileoptions);} else {send_file_not_found();}// ========================================================================================================================} else if ($component === 'question') {require_once($CFG->libdir . '/questionlib.php');question_pluginfile($course, $context, 'question', $filearea, $args, $forcedownload, $sendfileoptions);send_file_not_found();// ========================================================================================================================} else if ($component === 'grading') {if ($filearea === 'description') {// files embedded into the form definition descriptionif ($context->contextlevel == CONTEXT_SYSTEM) {require_login();} else if ($context->contextlevel >= CONTEXT_COURSE) {require_login($course, false, $cm);} else {send_file_not_found();}$formid = (int)array_shift($args);$sql = "SELECT ga.idFROM {grading_areas} gaJOIN {grading_definitions} gd ON (gd.areaid = ga.id)WHERE gd.id = ? AND ga.contextid = ?";$areaid = $DB->get_field_sql($sql, array($formid, $context->id), IGNORE_MISSING);if (!$areaid) {send_file_not_found();}$fullpath = "/$context->id/$component/$filearea/$formid/".implode('/', $args);if (!$file = $fs->get_file_by_hash(sha1($fullpath)) or $file->is_directory()) {send_file_not_found();}\core\session\manager::write_close(); // Unlock session during file serving.send_stored_file($file, 60*60, 0, $forcedownload, $sendfileoptions);}} else if ($component === 'contentbank') {if ($filearea != 'public' || isguestuser()) {send_file_not_found();}if ($context->contextlevel == CONTEXT_SYSTEM || $context->contextlevel == CONTEXT_COURSECAT) {require_login();} else if ($context->contextlevel == CONTEXT_COURSE) {require_login($course);} else {send_file_not_found();}$componentargs = fullclone($args);$itemid = (int)array_shift($args);$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';\core\session\manager::write_close(); // Unlock session during file serving.$contenttype = $DB->get_field('contentbank_content', 'contenttype', ['id' => $itemid]);if (component_class_callback("\\{$contenttype}\\contenttype", 'pluginfile',[$course, null, $context, $filearea, $componentargs, $forcedownload, $sendfileoptions], false) === false) {if (!$file = $fs->get_file($context->id, $component, $filearea, $itemid, $filepath, $filename) or$file->is_directory()) {send_file_not_found();} else {send_stored_file($file, 0, 0, true, $sendfileoptions); // Must force download - security!}}} else if (strpos($component, 'mod_') === 0) {$modname = substr($component, 4);if (!file_exists("$CFG->dirroot/mod/$modname/lib.php")) {send_file_not_found();}require_once("$CFG->dirroot/mod/$modname/lib.php");if ($context->contextlevel == CONTEXT_MODULE) {if ($cm->modname !== $modname) {// somebody tries to gain illegal access, cm type must match the component!send_file_not_found();}}if ($filearea === 'intro') {if (!plugin_supports('mod', $modname, FEATURE_MOD_INTRO, true)) {send_file_not_found();}// Require login to the course first (without login to the module).require_course_login($course, true);// Now check if module is available OR it is restricted but the intro is shown on the course page.$cminfo = cm_info::create($cm);if (!$cminfo->uservisible) {if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) {// Module intro is not visible on the course page and module is not available, show access error.require_course_login($course, true, $cminfo);}}// all users may access it$filename = array_pop($args);$filepath = $args ? '/'.implode('/', $args).'/' : '/';if (!$file = $fs->get_file($context->id, 'mod_'.$modname, 'intro', 0, $filepath, $filename) or $file->is_directory()) {send_file_not_found();}// finally send the filesend_stored_file($file, null, 0, false, $sendfileoptions);}$filefunction = $component.'_pluginfile';$filefunctionold = $modname.'_pluginfile';if (function_exists($filefunction)) {// if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"$filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);} else if (function_exists($filefunctionold)) {// if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"$filefunctionold($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);}send_file_not_found();// ========================================================================================================================} else if (strpos($component, 'block_') === 0) {$blockname = substr($component, 6);// note: no more class methods in blocks please, that is ....if (!file_exists("$CFG->dirroot/blocks/$blockname/lib.php")) {send_file_not_found();}require_once("$CFG->dirroot/blocks/$blockname/lib.php");if ($context->contextlevel == CONTEXT_BLOCK) {$birecord = $DB->get_record('block_instances', array('id'=>$context->instanceid), '*',MUST_EXIST);if ($birecord->blockname !== $blockname) {// somebody tries to gain illegal access, cm type must match the component!send_file_not_found();}if ($context->get_course_context(false)) {// If block is in course context, then check if user has capability to access course.require_course_login($course);} else if ($CFG->forcelogin) {// If user is logged out, bp record will not be visible, even if the user would have access if logged in.require_login();}$bprecord = $DB->get_record('block_positions', array('contextid' => $context->id, 'blockinstanceid' => $context->instanceid));// User can't access file, if block is hidden or doesn't have block:view capabilityif (($bprecord && !$bprecord->visible) || !has_capability('moodle/block:view', $context)) {send_file_not_found();}} else {$birecord = null;}$filefunction = $component.'_pluginfile';if (function_exists($filefunction)) {// if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"$filefunction($course, $birecord, $context, $filearea, $args, $forcedownload, $sendfileoptions);}send_file_not_found();// ========================================================================================================================} else if (strpos($component, '_') === false) {// all core subsystems have to be specified above, no more guessing here!send_file_not_found();} else {// try to serve general plugin file in arbitrary context$dir = core_component::get_component_directory($component);if (!file_exists("$dir/lib.php")) {send_file_not_found();}include_once("$dir/lib.php");$filefunction = $component.'_pluginfile';if (function_exists($filefunction)) {// if the function exists, it must send the file and terminate. Whatever it returns leads to "not found"$filefunction($course, $cm, $context, $filearea, $args, $forcedownload, $sendfileoptions);}send_file_not_found();}}