Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 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 39 and 310] [Versions 39 and 311] [Versions 39 and 400] [Versions 39 and 401] [Versions 39 and 402] [Versions 39 and 403]

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