[ Index ]

PHP Cross Reference of moodle-2.8

title

Body

[close]

/lib/ -> filelib.php (source)

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


Generated: Fri Nov 28 20:29:05 2014 Cross-referenced by PHPXref 0.7.1