Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.10.x will end 8 November 2021 (12 months).
  • Bug fixes for security issues in 3.10.x will end 9 May 2022 (18 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

Differences Between: [Versions 310 and 311] [Versions 310 and 400] [Versions 310 and 401] [Versions 310 and 402] [Versions 310 and 403] [Versions 39 and 310]

   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   * Private imscp module utility functions
  19   *
  20   * @package mod_imscp
  21   * @copyright  2009 Petr Skoda  {@link http://skodak.org}
  22   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  23   */
  24  
  25  defined('MOODLE_INTERNAL') || die();
  26  
  27  require_once("$CFG->dirroot/mod/imscp/lib.php");
  28  require_once("$CFG->libdir/filelib.php");
  29  require_once("$CFG->libdir/resourcelib.php");
  30  
  31  /**
  32   * Print IMSCP content to page.
  33   *
  34   * @param stdClass $imscp module instance.
  35   * @param stdClass $cm course module.
  36   * @param stdClass $course record.
  37   */
  38  function imscp_print_content($imscp, $cm, $course) {
  39      global $PAGE, $CFG;
  40  
  41      $items = unserialize($imscp->structure);
  42      $first = reset($items);
  43      $context = context_module::instance($cm->id);
  44      $urlbase = "$CFG->wwwroot/pluginfile.php";
  45      $path = '/'.$context->id.'/mod_imscp/content/'.$imscp->revision.'/'.$first['href'];
  46      $firsturl = file_encode_url($urlbase, $path, false);
  47  
  48      echo '<div id="imscp_layout">';
  49      echo '<div id="imscp_toc">';
  50      echo '<div id="imscp_tree"><ul>';
  51      foreach ($items as $item) {
  52          echo imscp_htmllize_item($item, $imscp, $cm);
  53      }
  54      echo '</ul></div>';
  55      echo '<div id="imscp_nav" style="display:none">';
  56      echo '<button id="nav_skipprev">&lt;&lt;</button><button id="nav_prev">&lt;</button><button id="nav_up">^</button>';
  57      echo '<button id="nav_next">&gt;</button><button id="nav_skipnext">&gt;&gt;</button>';
  58      echo '</div>';
  59      echo '</div>';
  60      echo '</div>';
  61  
  62      $PAGE->requires->js_init_call('M.mod_imscp.init');
  63      return;
  64  }
  65  
  66  /**
  67   * Internal function - creates htmls structure suitable for YUI tree.
  68   */
  69  function imscp_htmllize_item($item, $imscp, $cm) {
  70      global $CFG;
  71  
  72      if ($item['href']) {
  73          if (preg_match('|^https?://|', $item['href'])) {
  74              $url = $item['href'];
  75          } else {
  76              $context = context_module::instance($cm->id);
  77              $urlbase = "$CFG->wwwroot/pluginfile.php";
  78              $path = '/'.$context->id.'/mod_imscp/content/'.$imscp->revision.'/'.$item['href'];
  79              $url = file_encode_url($urlbase, $path, false);
  80          }
  81          $result = "<li><a href=\"$url\">".$item['title'].'</a>';
  82      } else {
  83          $result = '<li>'.$item['title'];
  84      }
  85      if ($item['subitems']) {
  86          $result .= '<ul>';
  87          foreach ($item['subitems'] as $subitem) {
  88              $result .= imscp_htmllize_item($subitem, $imscp, $cm);
  89          }
  90          $result .= '</ul>';
  91      }
  92      $result .= '</li>';
  93  
  94      return $result;
  95  }
  96  
  97  /**
  98   * Parse an IMS content package's manifest file to determine its structure
  99   * @param object $imscp
 100   * @param object $context
 101   * @return array
 102   */
 103  function imscp_parse_structure($imscp, $context) {
 104      $fs = get_file_storage();
 105  
 106      if (!$manifestfile = $fs->get_file($context->id, 'mod_imscp', 'content', $imscp->revision, '/', 'imsmanifest.xml')) {
 107          return null;
 108      }
 109  
 110      return imscp_parse_manifestfile($manifestfile->get_content(), $imscp, $context);
 111  }
 112  
 113  /**
 114   * Parse the contents of a IMS package's manifest file.
 115   * @param string $manifestfilecontents the contents of the manifest file
 116   * @return array
 117   */
 118  function imscp_parse_manifestfile($manifestfilecontents, $imscp, $context) {
 119      $doc = new DOMDocument();
 120      $oldentities = libxml_disable_entity_loader(true);
 121      if (!$doc->loadXML($manifestfilecontents, LIBXML_NONET)) {
 122          return null;
 123      }
 124      libxml_disable_entity_loader($oldentities);
 125  
 126      // We put this fake URL as base in order to detect path changes caused by xml:base attributes.
 127      $doc->documentURI = 'http://grrr/';
 128  
 129      $xmlorganizations = $doc->getElementsByTagName('organizations');
 130      if (empty($xmlorganizations->length)) {
 131          return null;
 132      }
 133      $default = null;
 134      if ($xmlorganizations->item(0)->attributes->getNamedItem('default')) {
 135          $default = $xmlorganizations->item(0)->attributes->getNamedItem('default')->nodeValue;
 136      }
 137      $xmlorganization = $doc->getElementsByTagName('organization');
 138      if (empty($xmlorganization->length)) {
 139          return null;
 140      }
 141      $organization = null;
 142      foreach ($xmlorganization as $org) {
 143          if (is_null($organization)) {
 144              // Use first if default nor found.
 145              $organization = $org;
 146          }
 147          if (!$org->attributes->getNamedItem('identifier')) {
 148              continue;
 149          }
 150          if ($default === $org->attributes->getNamedItem('identifier')->nodeValue) {
 151              // Found default - use it.
 152              $organization = $org;
 153              break;
 154          }
 155      }
 156  
 157      // Load all resources.
 158      $resources = array();
 159  
 160      $xmlresources = $doc->getElementsByTagName('resource');
 161      foreach ($xmlresources as $res) {
 162          if (!$identifier = $res->attributes->getNamedItem('identifier')) {
 163              continue;
 164          }
 165          $identifier = $identifier->nodeValue;
 166          if ($xmlbase = $res->baseURI) {
 167              // Undo the fake URL, we are interested in relative links only.
 168              $xmlbase = str_replace('http://grrr/', '/', $xmlbase);
 169              $xmlbase = rtrim($xmlbase, '/').'/';
 170          } else {
 171              $xmlbase = '';
 172          }
 173          if (!$href = $res->attributes->getNamedItem('href')) {
 174              // If href not found look for <file href="help.htm"/>.
 175              $fileresources = $res->getElementsByTagName('file');
 176              foreach ($fileresources as $file) {
 177                  $href = $file->getAttribute('href');
 178              }
 179              if (pathinfo($href, PATHINFO_EXTENSION) == 'xml') {
 180                  $href = imscp_recursive_href($href, $imscp, $context);
 181              }
 182              if (empty($href)) {
 183                  continue;
 184              }
 185          } else {
 186              $href = $href->nodeValue;
 187          }
 188          if (strpos($href, 'http://') !== 0) {
 189              $href = $xmlbase.$href;
 190          }
 191          // Item href cleanup - Some packages are poorly done and use \ in urls.
 192          $href = ltrim(strtr($href, "\\", '/'), '/');
 193          $resources[$identifier] = $href;
 194      }
 195  
 196      $items = array();
 197      foreach ($organization->childNodes as $child) {
 198          if ($child->nodeName === 'item') {
 199              if (!$item = imscp_recursive_item($child, 0, $resources)) {
 200                  continue;
 201              }
 202              $items[] = $item;
 203          }
 204      }
 205  
 206      return $items;
 207  }
 208  
 209  function imscp_recursive_href($manifestfilename, $imscp, $context) {
 210      $fs = get_file_storage();
 211  
 212      $dirname = dirname($manifestfilename);
 213      $filename = basename($manifestfilename);
 214  
 215      if ($dirname !== '/') {
 216          $dirname = "/$dirname/";
 217      }
 218  
 219      if (!$manifestfile = $fs->get_file($context->id, 'mod_imscp', 'content', $imscp->revision, $dirname, $filename)) {
 220          return null;
 221      }
 222  
 223      $doc = new DOMDocument();
 224      $oldentities = libxml_disable_entity_loader(true);
 225      if (!$doc->loadXML($manifestfile->get_content(), LIBXML_NONET)) {
 226          return null;
 227      }
 228      libxml_disable_entity_loader($oldentities);
 229  
 230      $xmlresources = $doc->getElementsByTagName('resource');
 231      foreach ($xmlresources as $res) {
 232          if (!$href = $res->attributes->getNamedItem('href')) {
 233              $fileresources = $res->getElementsByTagName('file');
 234              foreach ($fileresources as $file) {
 235                  $href = $file->getAttribute('href');
 236                  if (pathinfo($href, PATHINFO_EXTENSION) == 'xml') {
 237                      $href = imscp_recursive_href($href, $imscp, $context);
 238                  }
 239  
 240                  if (pathinfo($href, PATHINFO_EXTENSION) == 'htm' || pathinfo($href, PATHINFO_EXTENSION) == 'html') {
 241                      return $href;
 242                  }
 243              }
 244          }
 245      }
 246  
 247      return $manifestfilename;
 248  }
 249  
 250  function imscp_recursive_item($xmlitem, $level, $resources) {
 251      $identifierref = '';
 252      if ($identifierref = $xmlitem->attributes->getNamedItem('identifierref')) {
 253          $identifierref = $identifierref->nodeValue;
 254      }
 255  
 256      $title = '?';
 257      $subitems = array();
 258  
 259      foreach ($xmlitem->childNodes as $child) {
 260          if ($child->nodeName === 'title') {
 261              $title = $child->textContent;
 262  
 263          } else if ($child->nodeName === 'item') {
 264              if ($subitem = imscp_recursive_item($child, $level + 1, $resources)) {
 265                  $subitems[] = $subitem;
 266              }
 267          }
 268      }
 269  
 270      return array('href'     => isset($resources[$identifierref]) ? $resources[$identifierref] : '',
 271                   'title'    => $title,
 272                   'level'    => $level,
 273                   'subitems' => $subitems,
 274                  );
 275  }
 276  
 277  /**
 278   * File browsing support class
 279   *
 280   * @copyright  2009 Petr Skoda  {@link http://skodak.org}
 281   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 282   */
 283  class imscp_file_info extends file_info {
 284      protected $course;
 285      protected $cm;
 286      protected $areas;
 287      protected $filearea;
 288  
 289      public function __construct($browser, $course, $cm, $context, $areas, $filearea) {
 290          parent::__construct($browser, $context);
 291          $this->course   = $course;
 292          $this->cm       = $cm;
 293          $this->areas    = $areas;
 294          $this->filearea = $filearea;
 295      }
 296  
 297      /**
 298       * Returns list of standard virtual file/directory identification.
 299       * The difference from stored_file parameters is that null values
 300       * are allowed in all fields
 301       * @return array with keys contextid, filearea, itemid, filepath and filename
 302       */
 303      public function get_params() {
 304          return array('contextid' => $this->context->id,
 305                       'component' => 'mod_imscp',
 306                       'filearea'  => $this->filearea,
 307                       'itemid'    => null,
 308                       'filepath'  => null,
 309                       'filename'  => null);
 310      }
 311  
 312      /**
 313       * Returns localised visible name.
 314       * @return string
 315       */
 316      public function get_visible_name() {
 317          return $this->areas[$this->filearea];
 318      }
 319  
 320      /**
 321       * Can I add new files or directories?
 322       * @return bool
 323       */
 324      public function is_writable() {
 325          return false;
 326      }
 327  
 328      /**
 329       * Is directory?
 330       * @return bool
 331       */
 332      public function is_directory() {
 333          return true;
 334      }
 335  
 336      /**
 337       * Returns list of children.
 338       * @return array of file_info instances
 339       */
 340      public function get_children() {
 341          return $this->get_filtered_children('*', false, true);
 342      }
 343  
 344      /**
 345       * Help function to return files matching extensions or their count
 346       *
 347       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 348       * @param bool|int $countonly if false returns the children, if an int returns just the
 349       *    count of children but stops counting when $countonly number of children is reached
 350       * @param bool $returnemptyfolders if true returns items that don't have matching files inside
 351       * @return array|int array of file_info instances or the count
 352       */
 353      private function get_filtered_children($extensions = '*', $countonly = false, $returnemptyfolders = false) {
 354          global $DB;
 355          $params = array('contextid' => $this->context->id,
 356              'component' => 'mod_imscp',
 357              'filearea' => $this->filearea);
 358          $sql = 'SELECT DISTINCT itemid
 359                      FROM {files}
 360                      WHERE contextid = :contextid
 361                      AND component = :component
 362                      AND filearea = :filearea';
 363          if (!$returnemptyfolders) {
 364              $sql .= ' AND filename <> :emptyfilename';
 365              $params['emptyfilename'] = '.';
 366          }
 367          list($sql2, $params2) = $this->build_search_files_sql($extensions);
 368          $sql .= ' '.$sql2;
 369          $params = array_merge($params, $params2);
 370          if ($countonly !== false) {
 371              $sql .= ' ORDER BY itemid';
 372          }
 373  
 374          $rs = $DB->get_recordset_sql($sql, $params);
 375          $children = array();
 376          foreach ($rs as $record) {
 377              if ($child = $this->browser->get_file_info($this->context, 'mod_imscp', $this->filearea, $record->itemid)) {
 378                  $children[] = $child;
 379                  if ($countonly !== false && count($children) >= $countonly) {
 380                      break;
 381                  }
 382              }
 383          }
 384          $rs->close();
 385          if ($countonly !== false) {
 386              return count($children);
 387          }
 388          return $children;
 389      }
 390  
 391      /**
 392       * Returns list of children which are either files matching the specified extensions
 393       * or folders that contain at least one such file.
 394       *
 395       * @param string|array $extensions, either '*' or array of lowercase extensions, i.e. array('.gif','.jpg')
 396       * @return array of file_info instances
 397       */
 398      public function get_non_empty_children($extensions = '*') {
 399          return $this->get_filtered_children($extensions, false);
 400      }
 401  
 402      /**
 403       * Returns the number of children which are either files matching the specified extensions
 404       * or folders containing at least one such file.
 405       *
 406       * @param string|array $extensions, for example '*' or array('.gif','.jpg')
 407       * @param int $limit stop counting after at least $limit non-empty children are found
 408       * @return int
 409       */
 410      public function count_non_empty_children($extensions = '*', $limit = 1) {
 411          return $this->get_filtered_children($extensions, $limit);
 412      }
 413  
 414      /**
 415       * Returns parent file_info instance
 416       * @return file_info or null for root
 417       */
 418      public function get_parent() {
 419          return $this->browser->get_file_info($this->context);
 420      }
 421  }