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