Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

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

   1  <?php
   2  
   3  namespace Moodle;
   4  
   5  use stdClass;
   6  
   7  class H5peditor {
   8  
   9    private static $hasWYSIWYGEditor = array(
  10      'H5P.CoursePresentation',
  11      'H5P.InteractiveVideo',
  12      'H5P.DragQuestion'
  13    );
  14  
  15    public static $styles = array(
  16      'libs/darkroom.css',
  17      'styles/css/h5p-hub-client.css',
  18      'styles/css/fonts.css',
  19      'styles/css/application.css',
  20      'styles/css/libs/zebra_datepicker.min.css'
  21    );
  22    public static $scripts = array(
  23      'scripts/h5p-hub-client.js',
  24      'scripts/h5peditor.js',
  25      'scripts/h5peditor-semantic-structure.js',
  26      'scripts/h5peditor-editor.js',
  27      'scripts/h5peditor-library-selector.js',
  28      'scripts/h5peditor-fullscreen-bar.js',
  29      'scripts/h5peditor-form.js',
  30      'scripts/h5peditor-text.js',
  31      'scripts/h5peditor-html.js',
  32      'scripts/h5peditor-number.js',
  33      'scripts/h5peditor-textarea.js',
  34      'scripts/h5peditor-file-uploader.js',
  35      'scripts/h5peditor-file.js',
  36      'scripts/h5peditor-image.js',
  37      'scripts/h5peditor-image-popup.js',
  38      'scripts/h5peditor-av.js',
  39      'scripts/h5peditor-group.js',
  40      'scripts/h5peditor-boolean.js',
  41      'scripts/h5peditor-list.js',
  42      'scripts/h5peditor-list-editor.js',
  43      'scripts/h5peditor-library.js',
  44      'scripts/h5peditor-library-list-cache.js',
  45      'scripts/h5peditor-select.js',
  46      'scripts/h5peditor-selector-hub.js',
  47      'scripts/h5peditor-selector-legacy.js',
  48      'scripts/h5peditor-dimensions.js',
  49      'scripts/h5peditor-coordinates.js',
  50      'scripts/h5peditor-none.js',
  51      'scripts/h5peditor-metadata.js',
  52      'scripts/h5peditor-metadata-author-widget.js',
  53      'scripts/h5peditor-metadata-changelog-widget.js',
  54      'scripts/h5peditor-pre-save.js',
  55      'ckeditor/ckeditor.js',
  56    );
  57    private $h5p, $storage;
  58    public $ajax, $ajaxInterface;
  59  
  60    /**
  61     * Constructor for the core editor library.
  62     *
  63     * @param H5PCore $h5p Instance of core
  64     * @param H5peditorStorage $storage Instance of h5peditor storage interface
  65     * @param H5PEditorAjaxInterface $ajaxInterface Instance of h5peditor ajax
  66     * interface
  67     */
  68    function __construct($h5p, $storage, $ajaxInterface) {
  69      $this->h5p = $h5p;
  70      $this->storage = $storage;
  71      $this->ajaxInterface = $ajaxInterface;
  72      $this->ajax = new H5PEditorAjax($h5p, $this, $storage);
  73    }
  74  
  75    /**
  76     * Get list of libraries.
  77     *
  78     * @return array
  79     */
  80    public function getLibraries() {
  81      if (isset($_POST['libraries'])) {
  82        // Get details for the specified libraries.
  83        $libraries = array();
  84        foreach ($_POST['libraries'] as $libraryName) {
  85          $matches = array();
  86          preg_match_all('/(.+)\s(\d+)\.(\d+)$/', $libraryName, $matches);
  87          if ($matches && $matches[1] && $matches[2] && $matches[3]) {
  88            $libraries[] = (object) array(
  89              'uberName' => $libraryName,
  90              'name' => $matches[1][0],
  91              'majorVersion' => $matches[2][0],
  92              'minorVersion' => $matches[3][0]
  93            );
  94          }
  95        }
  96      }
  97  
  98      $libraries = $this->storage->getLibraries(!isset($libraries) ? NULL : $libraries);
  99  
 100      if ($this->h5p->development_mode & H5PDevelopment::MODE_LIBRARY) {
 101        $devLibs = $this->h5p->h5pD->getLibraries();
 102      }
 103  
 104      for ($i = 0, $s = count($libraries); $i < $s; $i++) {
 105        if (!empty($devLibs)) {
 106          $lid = $libraries[$i]->name . ' ' . $libraries[$i]->majorVersion . '.' . $libraries[$i]->minorVersion;
 107          if (isset($devLibs[$lid])) {
 108            // Replace library with devlib
 109            $isOld = !empty($libraries[$i]->isOld) && $libraries[$i]->isOld === TRUE;
 110            $libraries[$i] = (object) array(
 111              'uberName' => $lid,
 112              'name' => $devLibs[$lid]['machineName'],
 113              'title' => $devLibs[$lid]['title'],
 114              'majorVersion' => $devLibs[$lid]['majorVersion'],
 115              'minorVersion' => $devLibs[$lid]['minorVersion'],
 116              'runnable' => $devLibs[$lid]['runnable'],
 117              'restricted' => $libraries[$i]->restricted,
 118              'tutorialUrl' => $libraries[$i]->tutorialUrl,
 119              'metadataSettings' => $devLibs[$lid]['metadataSettings'],
 120            );
 121            if ($isOld) {
 122              $libraries[$i]->isOld = TRUE;
 123            }
 124          }
 125        }
 126  
 127        // Some libraries rely on an LRS to work and must be enabled manually
 128        if (in_array($libraries[$i]->name, array('H5P.Questionnaire', 'H5P.FreeTextQuestion')) &&
 129            !$this->h5p->h5pF->getOption('enable_lrs_content_types')) {
 130          $libraries[$i]->restricted = TRUE;
 131        }
 132      }
 133  
 134      return $libraries;
 135    }
 136  
 137    /**
 138     * Get translations for a language for a list of libraries
 139     *
 140     * @param array $libraries An array of libraries, in the form "<machineName> <majorVersion>.<minorVersion>
 141     * @param string $language_code
 142     * @return array
 143     */
 144    public function getTranslations($libraries, $language_code) {
 145      return $this->ajaxInterface->getTranslations($libraries, $language_code);
 146    }
 147  
 148    /**
 149     * Move uploaded files, remove old files and update library usage.
 150     *
 151     * @param stdClass $content
 152     * @param array $newLibrary
 153     * @param array $newParameters
 154     * @param array $oldLibrary
 155     * @param array $oldParameters
 156     */
 157    public function processParameters($content, $newLibrary, $newParameters, $oldLibrary = NULL, $oldParameters = NULL) {
 158      $newFiles = array();
 159      $oldFiles = array();
 160  
 161      // Keep track of current content ID (used when processing files)
 162      $this->content = $content;
 163  
 164      // Find new libraries/content dependencies and files.
 165      // Start by creating a fake library field to process. This way we get all the dependencies of the main library as well.
 166      $field = (object) array(
 167        'type' => 'library'
 168      );
 169      $libraryParams = (object) array(
 170        'library' => H5PCore::libraryToString($newLibrary),
 171        'params' => $newParameters
 172      );
 173      $this->processField($field, $libraryParams, $newFiles);
 174  
 175      if ($oldLibrary !== NULL) {
 176        // Find old files and libraries.
 177        $this->processSemantics($oldFiles, $this->h5p->loadLibrarySemantics($oldLibrary['name'], $oldLibrary['majorVersion'], $oldLibrary['minorVersion']), $oldParameters);
 178  
 179        // Remove old files.
 180        for ($i = 0, $s = count($oldFiles); $i < $s; $i++) {
 181          if (!in_array($oldFiles[$i], $newFiles) &&
 182              preg_match('/^(\w+:\/\/|\.\.\/)/i', $oldFiles[$i]) === 0) {
 183            $this->h5p->fs->removeContentFile($oldFiles[$i], $content);
 184            // (optionally we could just have marked them as tmp files)
 185          }
 186        }
 187      }
 188    }
 189  
 190    /**
 191     * Recursive function that moves the new files in to the h5p content folder and generates a list over the old files.
 192     * Also locates all the librares.
 193     *
 194     * @param array $files
 195     * @param array $libraries
 196     * @param array $semantics
 197     * @param array $params
 198     */
 199    private function processSemantics(&$files, $semantics, &$params) {
 200      for ($i = 0, $s = count($semantics); $i < $s; $i++) {
 201        $field = $semantics[$i];
 202        if (!isset($params->{$field->name})) {
 203          continue;
 204        }
 205        $this->processField($field, $params->{$field->name}, $files);
 206      }
 207    }
 208  
 209    /**
 210     * Process a single field.
 211     *
 212     * @staticvar string $h5peditor_path
 213     * @param object $field
 214     * @param mixed $params
 215     * @param array $files
 216     */
 217    private function processField(&$field, &$params, &$files) {
 218      switch ($field->type) {
 219        case 'file':
 220        case 'image':
 221          if (isset($params->path)) {
 222            $this->processFile($params, $files);
 223  
 224            // Process original image
 225            if (isset($params->originalImage) && isset($params->originalImage->path)) {
 226              $this->processFile($params->originalImage, $files);
 227            }
 228          }
 229          break;
 230  
 231        case 'video':
 232        case 'audio':
 233          if (is_array($params)) {
 234            for ($i = 0, $s = count($params); $i < $s; $i++) {
 235              $this->processFile($params[$i], $files);
 236            }
 237          }
 238          break;
 239  
 240        case 'library':
 241          if (isset($params->library) && isset($params->params)) {
 242            $library = H5PCore::libraryFromString($params->library);
 243            $semantics = $this->h5p->loadLibrarySemantics($library['machineName'], $library['majorVersion'], $library['minorVersion']);
 244  
 245            // Process parameters for the library.
 246            $this->processSemantics($files, $semantics, $params->params);
 247          }
 248          break;
 249  
 250        case 'group':
 251          if (isset($params)) {
 252            $isSubContent = isset($field->isSubContent) && $field->isSubContent == TRUE;
 253  
 254            if (count($field->fields) == 1 && !$isSubContent) {
 255              $params = (object) array($field->fields[0]->name => $params);
 256            }
 257            $this->processSemantics($files, $field->fields, $params);
 258          }
 259          break;
 260  
 261        case 'list':
 262          if (is_array($params)) {
 263            for ($j = 0, $t = count($params); $j < $t; $j++) {
 264              $this->processField($field->field, $params[$j], $files);
 265            }
 266          }
 267          break;
 268      }
 269    }
 270  
 271    /**
 272     * @param mixed $params
 273     * @param array $files
 274     */
 275    private function processFile(&$params, &$files) {
 276      if (preg_match('/^https?:\/\//', $params->path)) {
 277        return; // Skip external files
 278      }
 279  
 280      // Remove temporary files suffix
 281      if (substr($params->path, -4, 4) === '#tmp') {
 282        $params->path = substr($params->path, 0, strlen($params->path) - 4);
 283      }
 284  
 285      // File could be copied from another content folder.
 286      $matches = array();
 287      if (preg_match($this->h5p->relativePathRegExp, $params->path, $matches)) {
 288  
 289        // Create a copy of the file
 290        $this->h5p->fs->cloneContentFile($matches[5], $matches[4], $this->content);
 291  
 292        // Update Params with correct filename
 293        $params->path = $matches[5];
 294      }
 295      else {
 296        // Check if file exists in content folder
 297        $fileId = $this->h5p->fs->getContentFile($params->path, $this->content);
 298        if ($fileId) {
 299          // Mark the file as a keeper
 300          $this->storage->keepFile($fileId);
 301        }
 302        else {
 303          // File is not in content folder, try to copy it from the editor tmp dir
 304          // to content folder.
 305          $this->h5p->fs->cloneContentFile($params->path, 'editor', $this->content);
 306          // (not removed in case someone has copied it)
 307          // (will automatically be removed after 24 hours)
 308        }
 309      }
 310  
 311      $files[] = $params->path;
 312    }
 313  
 314    /**
 315     * TODO: Consider moving to core.
 316     */
 317    public function getLibraryLanguage($machineName, $majorVersion, $minorVersion, $languageCode) {
 318      if ($this->h5p->development_mode & H5PDevelopment::MODE_LIBRARY) {
 319        // Try to get language development library first.
 320        $language = $this->h5p->h5pD->getLanguage($machineName, $majorVersion, $minorVersion, $languageCode);
 321      }
 322  
 323      if (isset($language) === FALSE) {
 324        $language = $this->storage->getLanguage($machineName, $majorVersion, $minorVersion, $languageCode);
 325      }
 326  
 327      return ($language === FALSE ? NULL : $language);
 328    }
 329  
 330    /**
 331     * Return all libraries used by the given editor library.
 332     *
 333     * @param string $machineName Library identfier part 1
 334     * @param int $majorVersion Library identfier part 2
 335     * @param int $minorVersion Library identfier part 3
 336     */
 337    public function findEditorLibraries($machineName, $majorVersion, $minorVersion) {
 338      $library = $this->h5p->loadLibrary($machineName, $majorVersion, $minorVersion);
 339      $dependencies = array();
 340      $this->h5p->findLibraryDependencies($dependencies, $library);
 341  
 342      // Load addons for wysiwyg editors
 343      if (in_array($machineName, self::$hasWYSIWYGEditor)) {
 344        $addons = $this->h5p->h5pF->loadAddons();
 345        foreach ($addons as $addon) {
 346          $key = 'editor-' . $addon['machineName'];
 347          $dependencies[$key]['weight'] = sizeof($dependencies)+1;
 348          $dependencies[$key]['type'] = 'editor';
 349          $dependencies[$key]['library'] = $addon;
 350        }
 351      }
 352  
 353      // Order dependencies by weight
 354      $orderedDependencies = array();
 355      for ($i = 1, $s = count($dependencies); $i <= $s; $i++) {
 356        foreach ($dependencies as $dependency) {
 357          if ($dependency['weight'] === $i && $dependency['type'] === 'editor') {
 358            // Only load editor libraries.
 359            $dependency['library']['id'] = $dependency['library']['libraryId'];
 360            $orderedDependencies[$dependency['library']['libraryId']] = $dependency['library'];
 361            break;
 362          }
 363        }
 364      }
 365  
 366      return $orderedDependencies;
 367    }
 368  
 369    /**
 370     * Get all scripts, css and semantics data for a library
 371     *
 372     * @param string $machineName Library name
 373     * @param int $majorVersion
 374     * @param int $minorVersion
 375     * @param string $prefix Optional part to add between URL and asset path
 376     * @param string $fileDir Optional file dir to read files from
 377     *
 378     * @return array Libraries that was requested
 379     */
 380    public function getLibraryData($machineName, $majorVersion, $minorVersion, $languageCode, $prefix = '', $fileDir = '', $defaultLanguage = '') {
 381      $libraryData = new stdClass();
 382  
 383      $library = $this->h5p->loadLibrary($machineName, $majorVersion, $minorVersion);
 384  
 385      // Include name and version in data object for convenience
 386      $libraryData->name = $machineName;
 387      $libraryData->version = (object) array('major' => $majorVersion, 'minor' => $minorVersion);
 388      $libraryData->title = $library['title'];
 389  
 390      $libraryData->upgradesScript = $this->h5p->fs->getUpgradeScript($machineName, $majorVersion, $minorVersion);
 391      if ($libraryData->upgradesScript !== NULL) {
 392        // If valid add URL prefix
 393        $libraryData->upgradesScript = $this->h5p->url . $prefix . $libraryData->upgradesScript;
 394      }
 395  
 396      $libraries              = $this->findEditorLibraries($machineName, $majorVersion, $minorVersion);
 397      $libraryData->semantics = $this->h5p->loadLibrarySemantics($machineName, $majorVersion, $minorVersion);
 398      $libraryData->language  = $this->getLibraryLanguage($machineName, $majorVersion, $minorVersion, $languageCode);
 399      $libraryData->defaultLanguage = empty($defaultLanguage) ? NULL : $this->getLibraryLanguage($machineName, $majorVersion, $minorVersion, $defaultLanguage);
 400      $libraryData->languages = $this->storage->getAvailableLanguages($machineName, $majorVersion, $minorVersion);
 401  
 402      // Temporarily disable asset aggregation
 403      $aggregateAssets            = $this->h5p->aggregateAssets;
 404      $this->h5p->aggregateAssets = FALSE;
 405      // This is done to prevent files being loaded multiple times due to how
 406      // the editor works.
 407  
 408      // Get list of JS and CSS files that belongs to the dependencies
 409      $files = $this->h5p->getDependenciesFiles($libraries, $prefix);
 410      $libraryName = H5PCore::libraryToString(compact('machineName', 'majorVersion', 'minorVersion'), true);
 411      if ($this->hasPresave($libraryName) === true) {
 412        $this->addPresaveFile($files, $library, $prefix);
 413      }
 414      $this->storage->alterLibraryFiles($files, $libraries);
 415  
 416      // Restore asset aggregation setting
 417      $this->h5p->aggregateAssets = $aggregateAssets;
 418  
 419      // Create base URL
 420      $url = $this->h5p->url;
 421  
 422      // Javascripts
 423      if (!empty($files['scripts'])) {
 424        foreach ($files['scripts'] as $script) {
 425          if (preg_match('/:\/\//', $script->path) === 1) {
 426            // External file
 427            $libraryData->javascript[] = $script->path . $script->version;
 428          }
 429          else {
 430            // Local file
 431            $path = $url . $script->path;
 432            if (!isset($this->h5p->h5pD)) {
 433              $path .= $script->version;
 434            }
 435            $libraryData->javascript[] = $path;
 436          }
 437        }
 438      }
 439  
 440      // Stylesheets
 441      if (!empty($files['styles'])) {
 442        foreach ($files['styles'] as $css) {
 443          if (preg_match('/:\/\//', $css->path) === 1) {
 444            // External file
 445            $libraryData->css[] = $css->path . $css->version;
 446          }
 447          else {
 448            // Local file
 449            $path = $url . $css->path;
 450            if (!isset($this->h5p->h5pD)) {
 451              $path .= $css->version;
 452            }
 453            $libraryData->css[] = $path;
 454          }
 455        }
 456      }
 457  
 458      $translations = array();
 459      // Add translations for libraries.
 460      foreach ($libraries as $library) {
 461        if (empty($library['semantics'])) {
 462          $translation = $this->getLibraryLanguage($library['machineName'], $library['majorVersion'], $library['minorVersion'], $languageCode);
 463  
 464          // If translation was not found, and this is not the English one, try to load
 465          // the English translation
 466          if ($translation === NULL && $languageCode !== 'en') {
 467            $translation = $this->getLibraryLanguage($library['machineName'], $library['majorVersion'], $library['minorVersion'], 'en');
 468          }
 469  
 470          if ($translation !== NULL) {
 471            $translations[$library['machineName']] = json_decode($translation);
 472          }
 473        }
 474      }
 475  
 476      $libraryData->translations = $translations;
 477  
 478      return $libraryData;
 479    }
 480  
 481    /**
 482     * This function will prefix all paths within a CSS file.
 483     * Copied from Drupal 6.
 484     *
 485     * @staticvar type $_base
 486     * @param type $matches
 487     * @param type $base
 488     * @return type
 489     */
 490    public static function buildCssPath($matches, $base = NULL) {
 491      static $_base;
 492      // Store base path for preg_replace_callback.
 493      if (isset($base)) {
 494        $_base = $base;
 495      }
 496  
 497      // Prefix with base and remove '../' segments where possible.
 498      $path = $_base . $matches[1];
 499      $last = '';
 500      while ($path != $last) {
 501        $last = $path;
 502        $path = preg_replace('`(^|/)(?!\.\./)([^/]+)/\.\./`', '$1', $path);
 503      }
 504      return 'url('. $path .')';
 505    }
 506  
 507    /**
 508     * Gets content type cache, applies user specific properties and formats
 509     * as camelCase.
 510     *
 511     * @return array $libraries Cached libraries from the H5P Hub with user specific
 512     * permission properties
 513     */
 514    public function getUserSpecificContentTypeCache() {
 515      $cached_libraries = $this->ajaxInterface->getContentTypeCache();
 516  
 517      // Check if user has access to install libraries
 518      $libraries = array();
 519      foreach ($cached_libraries as &$result) {
 520        // Check if user can install content type
 521        $result->restricted = !$this->canInstallContentType($result);
 522  
 523        // Formats json
 524        $libraries[] = $this->getCachedLibsMap($result);
 525      }
 526  
 527      return $libraries;
 528    }
 529  
 530    public function canInstallContentType($contentType) {
 531      $canInstallAll         = $this->h5p->h5pF->hasPermission(H5PPermission::UPDATE_LIBRARIES);
 532      $canInstallRecommended = $this->h5p->h5pF->hasPermission(H5PPermission::INSTALL_RECOMMENDED);
 533  
 534      return $canInstallAll || $contentType->is_recommended && $canInstallRecommended;
 535    }
 536  
 537    /**
 538     * Gets local and external libraries data with metadata to display
 539     * all libraries that are currently available for the user.
 540     *
 541     * @return array $libraries Latest local and external libraries data with
 542     * user specific permissions
 543     */
 544    public function getLatestGlobalLibrariesData() {
 545      $latest_local_libraries = $this->ajaxInterface->getLatestLibraryVersions();
 546      $cached_libraries       = $this->getUserSpecificContentTypeCache();
 547      $this->mergeLocalLibsIntoCachedLibs($latest_local_libraries, $cached_libraries);
 548      return $cached_libraries;
 549    }
 550  
 551  
 552    /**
 553     * Extract library properties from cached library so they are ready to be
 554     * returned as JSON
 555     *
 556     * @param object $cached_library A single library from the content type cache
 557     *
 558     * @return array A map containing the necessary properties for a cached
 559     * library to send to the front-end
 560     */
 561    public function getCachedLibsMap($cached_library) {
 562      $restricted = isset($cached_library->restricted) ? $cached_library->restricted : FALSE;
 563  
 564      // Add mandatory fields
 565      $lib = array(
 566        'id'              => intval($cached_library->id),
 567        'machineName'     => $cached_library->machine_name,
 568        'majorVersion'    => intval( $cached_library->major_version),
 569        'minorVersion'    => intval($cached_library->minor_version),
 570        'patchVersion'    => intval($cached_library->patch_version),
 571        'h5pMajorVersion' => intval($cached_library->h5p_major_version),
 572        'h5pMinorVersion' => intval($cached_library->h5p_minor_version),
 573        'title'           => $cached_library->title,
 574        'summary'         => $cached_library->summary,
 575        'description'     => $cached_library->description,
 576        'icon'            => $cached_library->icon,
 577        'createdAt'       => intval($cached_library->created_at),
 578        'updatedAt'       => intval($cached_library->updated_at),
 579        'isRecommended'   => $cached_library->is_recommended != 0,
 580        'popularity'      => intval($cached_library->popularity),
 581        'screenshots'     => json_decode($cached_library->screenshots),
 582        'license'         => json_decode($cached_library->license),
 583        'owner'           => $cached_library->owner,
 584        'installed'       => FALSE,
 585        'isUpToDate'      => FALSE,
 586        'restricted'      => $restricted,
 587        'canInstall'      => !$restricted
 588      );
 589  
 590      // Add optional fields
 591      if (!empty($cached_library->categories)) {
 592        $lib['categories'] = json_decode($cached_library->categories);
 593      }
 594      if (!empty($cached_library->keywords)) {
 595        $lib['keywords'] = json_decode($cached_library->keywords);
 596      }
 597      if (!empty($cached_library->tutorial)) {
 598        $lib['tutorial'] = $cached_library->tutorial;
 599      }
 600      if (!empty($cached_library->example)) {
 601        $lib['example'] = $cached_library->example;
 602      }
 603  
 604      return $lib;
 605    }
 606  
 607  
 608    /**
 609     * Merge local libraries into cached libraries so that local libraries will
 610     * get supplemented with the additional info from externally cached libraries.
 611     *
 612     * Also sets whether a given cached library is installed and up to date with
 613     * the locally installed libraries
 614     *
 615     * @param array $local_libraries Locally installed libraries
 616     * @param array $cached_libraries Cached libraries from the H5P hub
 617     */
 618    public function mergeLocalLibsIntoCachedLibs($local_libraries, &$cached_libraries) {
 619      $can_create_restricted = $this->h5p->h5pF->hasPermission(H5PPermission::CREATE_RESTRICTED);
 620  
 621      // Add local libraries to supplement content type cache
 622      foreach ($local_libraries as $local_lib) {
 623        $is_local_only = TRUE;
 624        $icon_path = NULL;
 625  
 626        // Check if icon is available locally:
 627        if ($local_lib->has_icon) {
 628          // Create path to icon:
 629          $library_folder = H5PCore::libraryToString(array(
 630            'machineName' => $local_lib->machine_name,
 631            'majorVersion' => $local_lib->major_version,
 632            'minorVersion' => $local_lib->minor_version
 633          ), TRUE);
 634          $icon_path = $this->h5p->h5pF->getLibraryFileUrl($library_folder, 'icon.svg');
 635        }
 636  
 637        foreach ($cached_libraries as &$cached_lib) {
 638          // Determine if library is local
 639          $is_matching_library = $cached_lib['machineName'] === $local_lib->machine_name;
 640          if ($is_matching_library) {
 641            $is_local_only = FALSE;
 642  
 643            // Set icon if it exists locally
 644            if (isset($icon_path)) {
 645              $cached_lib['icon'] = $icon_path;
 646            }
 647  
 648            // Set local properties
 649            $cached_lib['installed']  = TRUE;
 650            $cached_lib['restricted'] = $can_create_restricted ? FALSE
 651              : ($local_lib->restricted ? TRUE : FALSE);
 652  
 653            // Set local version
 654            $cached_lib['localMajorVersion'] = (int) $local_lib->major_version;
 655            $cached_lib['localMinorVersion'] = (int) $local_lib->minor_version;
 656            $cached_lib['localPatchVersion'] = (int) $local_lib->patch_version;
 657  
 658            // Determine if library is newer or same as cache
 659            $major_is_updated =
 660              $cached_lib['majorVersion'] < $cached_lib['localMajorVersion'];
 661  
 662            $minor_is_updated =
 663              $cached_lib['majorVersion'] === $cached_lib['localMajorVersion'] &&
 664              $cached_lib['minorVersion'] < $cached_lib['localMinorVersion'];
 665  
 666            $patch_is_updated =
 667              $cached_lib['majorVersion'] === $cached_lib['localMajorVersion'] &&
 668              $cached_lib['minorVersion'] === $cached_lib['localMinorVersion'] &&
 669              $cached_lib['patchVersion'] <= $cached_lib['localPatchVersion'];
 670  
 671            $is_updated_library =
 672              $major_is_updated ||
 673              $minor_is_updated ||
 674              $patch_is_updated;
 675  
 676            if ($is_updated_library) {
 677              $cached_lib['isUpToDate'] = TRUE;
 678            }
 679          }
 680        }
 681  
 682        // Add minimal data to display local only libraries
 683        if ($is_local_only) {
 684          $local_only_lib = array(
 685            'id'                => (int) $local_lib->id,
 686            'machineName'       => $local_lib->machine_name,
 687            'title'             => $local_lib->title,
 688            'description'       => '',
 689            'majorVersion'      => (int) $local_lib->major_version,
 690            'minorVersion'      => (int) $local_lib->minor_version,
 691            'patchVersion'      => (int) $local_lib->patch_version,
 692            'localMajorVersion' => (int) $local_lib->major_version,
 693            'localMinorVersion' => (int) $local_lib->minor_version,
 694            'localPatchVersion' => (int) $local_lib->patch_version,
 695            'canInstall'        => FALSE,
 696            'installed'         => TRUE,
 697            'isUpToDate'        => TRUE,
 698            'owner'             => '',
 699            'restricted'        => $can_create_restricted ? FALSE :
 700              ($local_lib->restricted ? TRUE : FALSE)
 701          );
 702  
 703          if (isset($icon_path)) {
 704            $local_only_lib['icon'] = $icon_path;
 705          }
 706  
 707          $cached_libraries[] = $local_only_lib;
 708        }
 709      }
 710  
 711      // Restrict LRS dependent content
 712      if (!$this->h5p->h5pF->getOption('enable_lrs_content_types')) {
 713        foreach ($cached_libraries as &$lib) {
 714          if (in_array($lib['machineName'], array('H5P.Questionnaire', 'H5P.FreeTextQuestion'))) {
 715            $lib['restricted'] = TRUE;
 716          }
 717        }
 718      }
 719    }
 720  
 721    /**
 722     * Determine if a library has a presave.js file in the root folder
 723     *
 724     * @param string $libraryName
 725     * @return bool
 726     */
 727    public function hasPresave($libraryName){
 728      if( isset($this->h5p->h5pD) ){
 729        $parsedLibrary = H5PCore::libraryFromString($libraryName);
 730        if($parsedLibrary !== false){
 731          $machineName = $parsedLibrary['machineName'];
 732          $majorVersion = $parsedLibrary['majorVersion'];
 733          $minorVersion = $parsedLibrary['minorVersion'];
 734          $library = $this->h5p->h5pD->getLibrary($machineName, $majorVersion, $minorVersion);
 735          if( !is_null($library)){
 736            return $this->h5p->fs->hasPresave($libraryName, $library['path']);
 737          }
 738        }
 739      }
 740      return $this->h5p->fs->hasPresave($libraryName);
 741    }
 742  
 743    /**
 744     * Adds the path to the presave.js file to the list of dependency assets for the library
 745     *
 746     * @param array $assets
 747     * @param array $library
 748     * @param string $prefix
 749     */
 750    public function addPresaveFile(&$assets, $library, $prefix = ''){
 751      $path = 'libraries' . '/' . H5PCore::libraryToString($library, true);
 752      if( array_key_exists('path', $library)){
 753        $path = $library['path'];
 754      }
 755      $version = "?ver={$library['majorVersion']}.{$library['minorVersion']}.{$library['patchVersion']}";
 756      if( array_key_exists('version', $library) ){
 757        $version = $library['version'];
 758      }
 759  
 760      $assets['scripts'][] = (object) array(
 761        'path' => $prefix . '/' . $path . '/' . 'presave.js',
 762        'version' => $version,
 763      );
 764    }
 765  }