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.
   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   * @package    backup-convert
  19   * @subpackage cc-library
  20   * @copyright  2011 Darko Miletic <dmiletic@moodlerooms.com>
  21   * @license    http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  22   */
  23  
  24  require_once ('xmlbase.php');
  25  
  26  /**
  27   *
  28   * Various helper utils
  29   * @author Darko Miletic dmiletic@moodlerooms.com
  30   *
  31   */
  32  abstract class cc_helpers {
  33  
  34      /**
  35       * Checks extension of the supplied filename
  36       *
  37       * @param string $filename
  38       */
  39      public static function is_html($filename) {
  40          $extension = strtolower(pathinfo($filename, PATHINFO_EXTENSION));
  41          return in_array($extension, array('htm', 'html'));
  42      }
  43  
  44      /**
  45       * Generates unique identifier
  46       * @param string $prefix
  47       * @param string $suffix
  48       * @return string
  49       */
  50      public static function uuidgen($prefix = '', $suffix = '', $uppercase = true) {
  51          $uuid = trim(sprintf('%s%04x%04x%s', $prefix, mt_rand(0, 65535), mt_rand(0, 65535), $suffix));
  52          $result = $uppercase ? strtoupper($uuid) : strtolower($uuid);
  53          return $result;
  54      }
  55  
  56      /**
  57       * Creates new folder with random name
  58       * @param string $where
  59       * @param string $prefix
  60       * @param string $suffix
  61       * @return mixed - directory short name or false in case of failure
  62       */
  63      public static function randomdir($where, $prefix = '', $suffix = '') {
  64          global $CFG;
  65  
  66          $dirname    = false;
  67          $randomname = self::uuidgen($prefix, $suffix, false);
  68          $newdirname = $where.DIRECTORY_SEPARATOR.$randomname;
  69          if (mkdir($newdirname)) {
  70              chmod($newdirname, $CFG->directorypermissions);
  71              $dirname = $randomname;
  72          }
  73          return $dirname;
  74      }
  75  
  76      public static function build_query($attributes, $search) {
  77          $result = '';
  78          foreach ($attributes as $attribute) {
  79              if ($result != '') {
  80                  $result .= ' | ';
  81              }
  82              $result .= "//*[starts-with(@{$attribute},'{$search}')]/@{$attribute}";
  83          }
  84          return $result;
  85      }
  86  
  87      public static function process_embedded_files(&$doc, $attributes, $search, $customslash = null) {
  88          $result = array();
  89          $query = self::build_query($attributes, $search);
  90          $list = $doc->nodeList($query);
  91          foreach ($list as $filelink) {
  92              $rvalue = str_replace($search, '', $filelink->nodeValue);
  93              if (!empty($customslash)) {
  94                  $rvalue = str_replace($customslash, '/', $rvalue);
  95              }
  96              $result[] = rawurldecode($rvalue);
  97          }
  98          return $result;
  99      }
 100  
 101      /**
 102       *
 103       * Get list of embedded files
 104       * @param string $html
 105       * @return multitype:mixed
 106       */
 107      public static function embedded_files($html) {
 108          $result = array();
 109          $doc = new XMLGenericDocument();
 110          $doc->doc->validateOnParse = false;
 111          $doc->doc->strictErrorChecking = false;
 112          if (!empty($html) && $doc->loadHTML($html)) {
 113              $attributes = array('src', 'href');
 114              $result1 = self::process_embedded_files($doc, $attributes, '@@PLUGINFILE@@');
 115              $result2 = self::process_embedded_files($doc, $attributes, '$@FILEPHP@$', '$@SLASH@$');
 116              $result = array_merge($result1, $result2);
 117          }
 118          return $result;
 119      }
 120  
 121      public static function embedded_mapping($packageroot, $contextid = null) {
 122          $main_file = $packageroot . DIRECTORY_SEPARATOR . 'files.xml';
 123          $mfile = new XMLGenericDocument();
 124          if (!$mfile->load($main_file)) {
 125              return false;
 126          }
 127          $query = "/files/file[filename!='.']";
 128          if (!empty($contextid)) {
 129              $query .= "[contextid='{$contextid}']";
 130          }
 131          $files = $mfile->nodeList($query);
 132          $depfiles = array();
 133          foreach ($files as $node) {
 134              $mainfile   = intval($mfile->nodeValue('sortorder', $node));
 135              $filename   = $mfile->nodeValue('filename', $node);
 136              $filepath   = $mfile->nodeValue('filepath', $node);
 137              $source     = $mfile->nodeValue('source', $node);
 138              $author     = $mfile->nodeValue('author', $node);
 139              $license    = $mfile->nodeValue('license', $node);
 140              $hashedname = $mfile->nodeValue('contenthash', $node);
 141              $hashpart   = substr($hashedname, 0, 2);
 142              $location   = 'files'.DIRECTORY_SEPARATOR.$hashpart.DIRECTORY_SEPARATOR.$hashedname;
 143              $type       = $mfile->nodeValue('mimetype', $node);
 144              $depfiles[$filepath.$filename] = array( $location,
 145                                                      ($mainfile == 1),
 146                                                      strtolower(str_replace(' ', '_', $filename)),
 147                                                      $type,
 148                                                      $source,
 149                                                      $author,
 150                                                      $license,
 151                                                      strtolower(str_replace(' ', '_', $filepath)));
 152          }
 153  
 154          return $depfiles;
 155      }
 156  
 157      public static function add_files(cc_i_manifest &$manifest, $packageroot, $outdir, $allinone = true) {
 158          global $CFG;
 159  
 160          if (pkg_static_resources::instance()->finished) {
 161              return;
 162          }
 163          $files = cc_helpers::embedded_mapping($packageroot);
 164          $rdir = $allinone ? new cc_resource_location($outdir) : null;
 165          foreach ($files as $virtual => $values) {
 166              $clean_filename = $values[2];
 167              if (!$allinone) {
 168                  $rdir = new cc_resource_location($outdir);
 169              }
 170              $rtp = $rdir->fullpath().$values[7].$clean_filename;
 171              //Are there any relative virtual directories?
 172              //let us try to recreate them
 173              $justdir = $rdir->fullpath(false).$values[7];
 174              if (!file_exists($justdir)) {
 175                  if (!mkdir($justdir, $CFG->directorypermissions, true)) {
 176                      throw new RuntimeException('Unable to create directories!');
 177                  }
 178              }
 179  
 180              $source = $packageroot.DIRECTORY_SEPARATOR.$values[0];
 181              if (!copy($source, $rtp)) {
 182                  throw new RuntimeException('Unable to copy files!');
 183              }
 184              $resource = new cc_resource($rdir->rootdir(),
 185                                          $values[7].$clean_filename,
 186                                          $rdir->dirname(false));
 187              $res = $manifest->add_resource($resource, null, cc_version11::webcontent);
 188              pkg_static_resources::instance()->add($virtual,
 189                                                    $res[0],
 190                                                    $rdir->dirname(false).$values[7].$clean_filename,
 191                                                    $values[1],
 192                                                    $resource);
 193          }
 194  
 195          pkg_static_resources::instance()->finished = true;
 196      }
 197  
 198      /**
 199       *
 200       * Excerpt from IMS CC 1.1 overview :
 201       * No spaces in filenames, directory and file references should
 202       * employ all lowercase or all uppercase - no mixed case
 203       *
 204       * @param cc_i_manifest $manifest
 205       * @param string $packageroot
 206       * @param integer $contextid
 207       * @param string $outdir
 208       * @param boolean $allinone
 209       * @throws RuntimeException
 210       */
 211      public static function handle_static_content(cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $allinone = true) {
 212          self::add_files($manifest, $packageroot, $outdir, $allinone);
 213          return pkg_static_resources::instance()->get_values();
 214      }
 215  
 216      public static function handle_resource_content(cc_i_manifest &$manifest, $packageroot, $contextid, $outdir, $allinone = true) {
 217          $result = array();
 218          self::add_files($manifest, $packageroot, $outdir, $allinone);
 219          $files = self::embedded_mapping($packageroot, $contextid);
 220          $rootnode = null;
 221          $rootvals = null;
 222          $depfiles = array();
 223          $depres = array();
 224          $flocation = null;
 225          foreach ($files as $virtual => $values) {
 226              $vals = pkg_static_resources::instance()->get_identifier($virtual);
 227              $resource = $vals[3];
 228              $identifier = $resource->identifier;
 229              $flocation = $vals[1];
 230              if ($values[1]) {
 231                  $rootnode = $resource;
 232                  $rootvals = $flocation;
 233                  continue;
 234              }
 235  
 236              $depres[] = $identifier;
 237              $depfiles[] = $vals[1];
 238              $result[$virtual] = array($identifier, $flocation, false);
 239          }
 240  
 241          if (!empty($rootnode)) {
 242              $rootnode->files = array_merge($rootnode->files, $depfiles);
 243              $result[$virtual] = array($rootnode->identifier, $rootvals, true);
 244          }
 245  
 246          return $result;
 247      }
 248  
 249      public static function process_linked_files($content, cc_i_manifest &$manifest, $packageroot,
 250                                                  $contextid, $outdir, $webcontent = false) {
 251          // Detect all embedded files
 252          // locate their physical counterparts in moodle 2 backup
 253          // copy all files in the cc package stripping any spaces and using only lowercase letters
 254          // add those files as resources of the type webcontent to the manifest
 255          // replace the links to the resource using $IMS-CC-FILEBASE$ and their new locations
 256          // cc_resource has array of files and array of dependencies
 257          // most likely we would need to add all files as independent resources and than
 258          // attach them all as dependencies to the forum tag.
 259          $lfiles = self::embedded_files($content);
 260          $text = $content;
 261          $deps = array();
 262          if (!empty($lfiles)) {
 263              $files = self::handle_static_content($manifest,
 264                                                   $packageroot,
 265                                                   $contextid,
 266                                                   $outdir);
 267              $replaceprefix = $webcontent ? '' : '$IMS-CC-FILEBASE$';
 268              foreach ($lfiles as $lfile) {
 269                  if (isset($files[$lfile])) {
 270                      $filename = str_replace('%2F', '/', rawurlencode($lfile));
 271                      $content = str_replace('@@PLUGINFILE@@'.$filename,
 272                                             $replaceprefix.'../'.$files[$lfile][1],
 273                                             $content);
 274                      // For the legacy stuff.
 275                      $content = str_replace('$@FILEPHP@$'.str_replace('/', '$@SLASH@$', $filename),
 276                                             $replaceprefix.'../'.$files[$lfile][1],
 277                                             $content);
 278                      $deps[] = $files[$lfile][0];
 279                  }
 280              }
 281              $text = $content;
 282          }
 283          return array($text, $deps);
 284      }
 285  
 286      public static function relative_location($originpath, $linkingpath) {
 287          return false;
 288      }
 289  
 290  }
 291  
 292  
 293  final class cc_resource_location {
 294      /**
 295       *
 296       * Root directory
 297       * @var string
 298       */
 299      private $rootdir = null;
 300      /**
 301       *
 302       * new directory
 303       * @var string
 304       */
 305      private $dir = null;
 306      /**
 307       *
 308       * Full precalculated path
 309       * @var string
 310       */
 311      private $fullpath = null;
 312  
 313      /**
 314       *
 315       * ctor
 316       * @param string $rootdir - path to the containing directory
 317       * @throws InvalidArgumentException
 318       * @throws RuntimeException
 319       */
 320      public function __construct($rootdir) {
 321          $rdir = realpath($rootdir);
 322          if (empty($rdir)) {
 323              throw new InvalidArgumentException('Invalid path!');
 324          }
 325          $dir = cc_helpers::randomdir($rdir, 'i_');
 326          if ($dir === false) {
 327              throw new RuntimeException('Unable to create directory!');
 328          }
 329          $this->rootdir  = $rdir;
 330          $this->dir      = $dir;
 331          $this->fullpath = $rdir.DIRECTORY_SEPARATOR.$dir;
 332      }
 333  
 334      /**
 335       *
 336       * Newly created directory
 337       * @return string
 338       */
 339      public function dirname($endseparator=false) {
 340          return $this->dir.($endseparator ? '/' : '');
 341      }
 342  
 343      /**
 344       *
 345       * Full path to the new directory
 346       * @return string
 347       */
 348      public function fullpath($endseparator=false) {
 349          return $this->fullpath.($endseparator ? DIRECTORY_SEPARATOR : '');
 350      }
 351  
 352      /**
 353       * Returns containing dir
 354       * @return string
 355       */
 356      public function rootdir($endseparator=false) {
 357          return $this->rootdir.($endseparator ? DIRECTORY_SEPARATOR : '');
 358      }
 359  }
 360  
 361  class pkg_static_resources {
 362  
 363      /**
 364       * @var array
 365       */
 366      private $values = array();
 367  
 368      /**
 369       * @var boolean
 370       */
 371      public $finished = false;
 372  
 373      /**
 374       * @var pkg_static_resources
 375       */
 376      private static $instance = null;
 377  
 378      private function __clone() {
 379      }
 380  
 381      private function __construct() {
 382      }
 383  
 384      /**
 385       * @return pkg_static_resources
 386       */
 387      public static function instance() {
 388          if (empty(self::$instance)) {
 389              $c = __CLASS__;
 390              self::$instance = new $c();
 391          }
 392          return self::$instance;
 393      }
 394  
 395      /**
 396       *
 397       * add new element
 398       * @param string $identifier
 399       * @param string $file
 400       * @param boolean $main
 401       */
 402      public function add($key, $identifier, $file, $main, $node = null) {
 403          $this->values[$key] = array($identifier, $file, $main, $node);
 404      }
 405  
 406      /**
 407       * @return array
 408       */
 409      public function get_values() {
 410          return $this->values;
 411      }
 412  
 413      public function get_identifier($location) {
 414          return isset($this->values[$location]) ? $this->values[$location] : false;
 415      }
 416  
 417      public function reset() {
 418          $this->values   = array();
 419          $this->finished = false;
 420      }
 421  }
 422  
 423  
 424  class pkg_resource_dependencies {
 425      /**
 426       * @var array
 427       */
 428      private $values = array();
 429  
 430      /**
 431       * @var pkg_resource_dependencies
 432       */
 433      private static $instance = null;
 434  
 435      private function __clone() {
 436      }
 437      private function __construct() {
 438      }
 439  
 440      /**
 441       * @return pkg_resource_dependencies
 442       */
 443      public static function instance() {
 444          if (empty(self::$instance)) {
 445              $c = __CLASS__;
 446              self::$instance = new $c();
 447          }
 448          return self::$instance;
 449      }
 450  
 451      /**
 452       * @param array $deps
 453       */
 454      public function add(array $deps) {
 455          $this->values = array_merge($this->values, $deps);
 456      }
 457  
 458      public function reset() {
 459          $this->values = array();
 460      }
 461  
 462      /**
 463       * @return array
 464       */
 465      public function get_deps() {
 466          return $this->values;
 467      }
 468  }