Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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

   1  <?php
   2  
   3  namespace Moodle;
   4  
   5  abstract class H5PEditorEndpoints {
   6  
   7    /**
   8     * Endpoint for retrieving library data necessary for displaying
   9     * content types in the editor.
  10     */
  11    const LIBRARIES = 'libraries';
  12  
  13    /**
  14     * Endpoint for retrieving a singe library's data necessary for displaying
  15     * main libraries
  16     */
  17    const SINGLE_LIBRARY = 'single-library';
  18  
  19    /**
  20     * Endpoint for retrieving the currently stored content type cache
  21     */
  22    const CONTENT_TYPE_CACHE = 'content-type-cache';
  23  
  24    /**
  25     * Endpoint for retrieving the currently stored content hub metadata cache
  26     */
  27    const CONTENT_HUB_METADATA_CACHE = 'content-hub-metadata-cache';
  28  
  29    /**
  30     * Endpoint for installing libraries from the Content Type Hub
  31     */
  32    const LIBRARY_INSTALL = 'library-install';
  33  
  34    /**
  35     * Endpoint for uploading libraries used by the editor through the Content
  36     * Type Hub.
  37     */
  38    const LIBRARY_UPLOAD = 'library-upload';
  39  
  40    /**
  41     * Endpoint for uploading files used by the editor.
  42     */
  43    const FILES = 'files';
  44  
  45    /**
  46     * Endpoint for retrieveing translation files
  47     */
  48    const TRANSLATIONS = 'translations';
  49  
  50    /**
  51     * Endpoint for filtering parameters.
  52     */
  53    const FILTER = 'filter';
  54  
  55    /**
  56     * Endpoint for installing libraries from the Content Type Hub
  57     */
  58    const GET_HUB_CONTENT = 'get-hub-content';
  59  }
  60  
  61  
  62    /**
  63   * Class H5PEditorAjax
  64   * @package modules\h5peditor\h5peditor
  65   */
  66  class H5PEditorAjax {
  67  
  68    /**
  69     * @var H5PCore
  70     */
  71    public $core;
  72  
  73    /**
  74     * @var H5peditor
  75     */
  76    public $editor;
  77  
  78    /**
  79     * @var H5peditorStorage
  80     */
  81    public $storage;
  82  
  83    /**
  84     * H5PEditorAjax constructor requires core, editor and storage as building
  85     * blocks.
  86     *
  87     * @param H5PCore $H5PCore
  88     * @param H5peditor $H5PEditor
  89     * @param H5peditorStorage $H5PEditorStorage
  90     */
  91    public function __construct(H5PCore $H5PCore, H5peditor $H5PEditor, H5peditorStorage $H5PEditorStorage) {
  92      $this->core = $H5PCore;
  93      $this->editor = $H5PEditor;
  94      $this->storage = $H5PEditorStorage;
  95    }
  96  
  97    /**
  98     * @param $endpoint
  99     */
 100    public function action($endpoint) {
 101      switch ($endpoint) {
 102        case H5PEditorEndpoints::LIBRARIES:
 103          H5PCore::ajaxSuccess($this->editor->getLibraries(), TRUE);
 104          break;
 105  
 106        case H5PEditorEndpoints::SINGLE_LIBRARY:
 107          // pass on arguments
 108          $args = func_get_args();
 109          array_shift($args);
 110          $library = call_user_func_array(
 111            array($this->editor, 'getLibraryData'), $args
 112          );
 113          H5PCore::ajaxSuccess($library, TRUE);
 114          break;
 115  
 116        case H5PEditorEndpoints::CONTENT_TYPE_CACHE:
 117          if (!$this->isHubOn()) return;
 118          H5PCore::ajaxSuccess($this->getContentTypeCache(!$this->isContentTypeCacheUpdated()), TRUE);
 119          break;
 120  
 121        case H5PEditorEndpoints::CONTENT_HUB_METADATA_CACHE:
 122          if (!$this->isHubOn()) return;
 123          header('Cache-Control: no-cache');
 124          header('Content-Type: application/json; charset=utf-8');
 125          print '{"success":true,"data":' . $this->core->getUpdatedContentHubMetadataCache(func_get_arg(1)) . '}';
 126          break;
 127  
 128        case H5PEditorEndpoints::LIBRARY_INSTALL:
 129          if (!$this->isPostRequest()) return;
 130  
 131          $token = func_get_arg(1);
 132          if (!$this->isValidEditorToken($token)) return;
 133  
 134          $machineName = func_get_arg(2);
 135          $this->libraryInstall($machineName);
 136          break;
 137  
 138        case H5PEditorEndpoints::LIBRARY_UPLOAD:
 139          if (!$this->isPostRequest()) return;
 140  
 141          $token = func_get_arg(1);
 142          if (!$this->isValidEditorToken($token)) return;
 143  
 144          $uploadPath = func_get_arg(2);
 145          $contentId = func_get_arg(3);
 146          $this->libraryUpload($uploadPath, $contentId);
 147          break;
 148  
 149        case H5PEditorEndpoints::FILES:
 150          $token = func_get_arg(1);
 151          $contentId = func_get_arg(2);
 152          if (!$this->isValidEditorToken($token)) return;
 153          $this->fileUpload($contentId);
 154          break;
 155  
 156        case H5PEditorEndpoints::TRANSLATIONS:
 157          $language = func_get_arg(1);
 158          H5PCore::ajaxSuccess($this->editor->getTranslations($_POST['libraries'], $language));
 159          break;
 160  
 161        case H5PEditorEndpoints::FILTER:
 162          $token = func_get_arg(1);
 163          if (!$this->isValidEditorToken($token)) return;
 164          $this->filter(func_get_arg(2));
 165          break;
 166  
 167        case H5PEditorEndpoints::GET_HUB_CONTENT:
 168          if (!$this->isPostRequest() || !$this->isValidEditorToken(func_get_arg(1))) {
 169            return;
 170          }
 171          $this->getHubContent(func_get_arg(2), func_get_arg(3));
 172          break;
 173      }
 174    }
 175  
 176    /**
 177     * Handles uploaded files from the editor, making sure they are validated
 178     * and ready to be permanently stored if saved.
 179     *
 180     * Marks all uploaded files as
 181     * temporary so they can be cleaned up when we have finished using them.
 182     *
 183     * @param int $contentId Id of content if already existing content
 184     */
 185    private function fileUpload($contentId = NULL) {
 186      $file = new H5peditorFile($this->core->h5pF);
 187      if (!$file->isLoaded()) {
 188        H5PCore::ajaxError($this->core->h5pF->t('File not found on server. Check file upload settings.'));
 189        return;
 190      }
 191  
 192      // Make sure file is valid and mark it for cleanup at a later time
 193      if ($file->validate()) {
 194        $file_id = $this->core->fs->saveFile($file, 0);
 195        $this->storage->markFileForCleanup($file_id, 0);
 196      }
 197      $file->printResult();
 198    }
 199  
 200    /**
 201     * Handles uploading libraries so they are ready to be modified or directly saved.
 202     *
 203     * Validates and saves any dependencies, then exposes content to the editor.
 204     *
 205     * @param {string} $uploadFilePath Path to the file that should be uploaded
 206     * @param {int} $contentId Content id of library
 207     */
 208    private function libraryUpload($uploadFilePath, $contentId) {
 209      // Verify h5p upload
 210      if (!$uploadFilePath) {
 211        H5PCore::ajaxError($this->core->h5pF->t('Could not get posted H5P.'), 'NO_CONTENT_TYPE');
 212        exit;
 213      }
 214  
 215      $file = $this->saveFileTemporarily($uploadFilePath, TRUE);
 216      if (!$file) return;
 217  
 218      $this->processContent($contentId);
 219    }
 220  
 221    /**
 222     * Process H5P content from local H5P package.
 223     *
 224     * @param integer $contentId The Local Content ID / vid. TODO Remove when JI-366 is fixed
 225     */
 226    private function processContent($contentId) {
 227      // Check if the downloaded package is valid
 228      if (!$this->isValidPackage()) {
 229        return; // Validation errors
 230      }
 231  
 232      // Install any required dependencies (libraries) from the package
 233      // (if permission allows it, of course)
 234      $storage = new H5PStorage($this->core->h5pF, $this->core);
 235      $storage->savePackage(NULL, NULL, TRUE);
 236  
 237      // Make content available to editor
 238      $files = $this->core->fs->moveContentDirectory($this->core->h5pF->getUploadedH5pFolderPath(), $contentId);
 239  
 240      // Clean up
 241      $this->storage->removeTemporarilySavedFiles($this->core->h5pF->getUploadedH5pFolderPath());
 242  
 243      // Mark all files as temporary
 244      // TODO: Uncomment once moveContentDirectory() is fixed. JI-366
 245      /*foreach ($files as $file) {
 246        $this->storage->markFileForCleanup($file, 0);
 247      }*/
 248  
 249      H5PCore::ajaxSuccess(array(
 250        'h5p' => $this->core->mainJsonData,
 251        'content' => $this->core->contentJsonData,
 252        'contentTypes' => $this->getContentTypeCache()
 253      ));
 254    }
 255  
 256    /**
 257     * Validates security tokens used for the editor
 258     *
 259     * @param string $token
 260     *
 261     * @return bool
 262     */
 263    private function isValidEditorToken($token) {
 264      $isValidToken = $this->editor->ajaxInterface->validateEditorToken($token);
 265      if (!$isValidToken) {
 266        H5PCore::ajaxError(
 267          $this->core->h5pF->t('Invalid security token.'),
 268          'INVALID_TOKEN'
 269        );
 270        return FALSE;
 271      }
 272      return TRUE;
 273    }
 274  
 275    /**
 276     * Handles installation of libraries from the Content Type Hub.
 277     *
 278     * Accepts a machine name and attempts to fetch and install it from the Hub if
 279     * it is valid. Will also install any dependencies to the requested library.
 280     *
 281     * @param string $machineName Name of library that should be installed
 282     */
 283    private function libraryInstall($machineName) {
 284  
 285      // Determine which content type to install from post data
 286      if (!$machineName) {
 287        H5PCore::ajaxError($this->core->h5pF->t('No content type was specified.'), 'NO_CONTENT_TYPE');
 288        return;
 289      }
 290  
 291      // Look up content type to ensure it's valid(and to check permissions)
 292      $contentType = $this->editor->ajaxInterface->getContentTypeCache($machineName);
 293      if (!$contentType) {
 294        H5PCore::ajaxError($this->core->h5pF->t('The chosen content type is invalid.'), 'INVALID_CONTENT_TYPE');
 295        return;
 296      }
 297  
 298      // Check install permissions
 299      if (!$this->editor->canInstallContentType($contentType)) {
 300        H5PCore::ajaxError($this->core->h5pF->t('You do not have permission to install content types. Contact the administrator of your site.'), 'INSTALL_DENIED');
 301        return;
 302      }
 303      else {
 304        // Override core permission check
 305        $this->core->mayUpdateLibraries(TRUE);
 306      }
 307  
 308      // Retrieve content type from hub endpoint
 309      $response = $this->callHubEndpoint(H5PHubEndpoints::CONTENT_TYPES . $machineName);
 310      if (!$response) return;
 311  
 312      // Session parameters has to be set for validation and saving of packages
 313      if (!$this->isValidPackage(TRUE)) return;
 314  
 315      // Save H5P
 316      $storage = new H5PStorage($this->core->h5pF, $this->core);
 317      $storage->savePackage(NULL, NULL, TRUE);
 318  
 319      // Clean up
 320      $this->storage->removeTemporarilySavedFiles($this->core->h5pF->getUploadedH5pFolderPath());
 321  
 322      // Successfully installed. Refresh content types
 323      H5PCore::ajaxSuccess($this->getContentTypeCache());
 324    }
 325  
 326    /**
 327     * End-point for filter parameter values according to semantics.
 328     *
 329     * @param {string} $libraryParameters
 330     */
 331    private function filter($libraryParameters) {
 332      $libraryParameters = json_decode($libraryParameters);
 333      if (!$libraryParameters) {
 334        H5PCore::ajaxError($this->core->h5pF->t('Could not parse post data.'), 'NO_LIBRARY_PARAMETERS');
 335        exit;
 336      }
 337  
 338      // Filter parameters and send back to client
 339      $validator = new H5PContentValidator($this->core->h5pF, $this->core);
 340      $validator->validateLibrary($libraryParameters, (object) array('options' => array($libraryParameters->library)));
 341      H5PCore::ajaxSuccess($libraryParameters);
 342    }
 343  
 344    /**
 345     * Download and use content from the HUB
 346     *
 347     * @param integer $hubId The Hub Content ID
 348     * @param integer $localContentId The Local Content ID
 349     */
 350    private function getHubContent($hubId, $localContentId) {
 351      // Download H5P file
 352      if (!$this->callHubEndpoint(H5PHubEndpoints::CONTENT . '/' . $hubId . '/export')) {
 353        return; // Download failed
 354      }
 355  
 356      $this->processContent($localContentId);
 357    }
 358  
 359    /**
 360     * Validates the package. Sets error messages if validation fails.
 361     *
 362     * @param bool $skipContent Will not validate cotent if set to TRUE
 363     *
 364     * @return bool
 365     */
 366    private function isValidPackage($skipContent = FALSE) {
 367      $validator = new H5PValidator($this->core->h5pF, $this->core);
 368      if (!$validator->isValidPackage($skipContent, FALSE)) {
 369        $this->storage->removeTemporarilySavedFiles($this->core->h5pF->getUploadedH5pPath());
 370  
 371        H5PCore::ajaxError(
 372          $this->core->h5pF->t('Validating h5p package failed.'),
 373          'VALIDATION_FAILED',
 374          NULL,
 375          $this->core->h5pF->getMessages('error')
 376        );
 377        return FALSE;
 378      }
 379  
 380      return TRUE;
 381    }
 382  
 383    /**
 384     * Saves a file or moves it temporarily. This is often necessary in order to
 385     * validate and store uploaded or fetched H5Ps.
 386     *
 387     * Sets error messages if saving fails.
 388     *
 389     * @param string $data Uri of data that should be saved as a temporary file
 390     * @param boolean $move_file Can be set to TRUE to move the data instead of saving it
 391     *
 392     * @return bool|object Returns false if saving failed or the path to the file
 393     *  if saving succeeded
 394     */
 395    private function saveFileTemporarily($data, $move_file = FALSE) {
 396      $file = $this->storage->saveFileTemporarily($data, $move_file);
 397      if (!$file) {
 398        H5PCore::ajaxError(
 399          $this->core->h5pF->t('Failed to download the requested H5P.'),
 400          'DOWNLOAD_FAILED'
 401        );
 402        return FALSE;
 403      }
 404  
 405      return $file;
 406    }
 407  
 408    /**
 409     * Calls provided hub endpoint and downloads the response to a .h5p file.
 410     *
 411     * @param string $endpoint Endpoint without protocol
 412     *
 413     * @return bool
 414     */
 415    private function callHubEndpoint($endpoint) {
 416      $path = $this->core->h5pF->getUploadedH5pPath();
 417      $response = $this->core->h5pF->fetchExternalData(H5PHubEndpoints::createURL($endpoint), NULL, TRUE, empty($path) ? TRUE : $path);
 418      if (!$response) {
 419        H5PCore::ajaxError(
 420          $this->core->h5pF->t('Failed to download the requested H5P.'),
 421          'DOWNLOAD_FAILED',
 422          NULL,
 423          $this->core->h5pF->getMessages('error')
 424        );
 425        return FALSE;
 426      }
 427  
 428      return TRUE;
 429    }
 430  
 431    /**
 432     * Checks if request is a POST. Sets error message on fail.
 433     *
 434     * @return bool
 435     */
 436    private function isPostRequest() {
 437      if ($_SERVER['REQUEST_METHOD'] !== 'POST') {
 438        H5PCore::ajaxError(
 439          $this->core->h5pF->t('A post message is required to access the given endpoint'),
 440          'REQUIRES_POST',
 441          405
 442        );
 443        return FALSE;
 444      }
 445      return TRUE;
 446    }
 447  
 448    /**
 449     * Checks if H5P Hub is enabled. Sets error message on fail.
 450     *
 451     * @return bool
 452     */
 453    private function isHubOn() {
 454      if (!$this->core->h5pF->getOption('hub_is_enabled', TRUE)) {
 455        H5PCore::ajaxError(
 456          $this->core->h5pF->t('The hub is disabled. You can enable it in the H5P settings.'),
 457          'HUB_DISABLED',
 458          403
 459        );
 460        return false;
 461      }
 462      return true;
 463    }
 464  
 465    /**
 466     * Checks if Content Type Cache is up to date. Immediately tries to fetch
 467     * a new Content Type Cache if it is outdated.
 468     * Sets error message if fetching new Content Type Cache fails.
 469     *
 470     * @return bool
 471     */
 472    private function isContentTypeCacheUpdated() {
 473  
 474      // Update content type cache if enabled and too old
 475      $ct_cache_last_update = $this->core->h5pF->getOption('content_type_cache_updated_at', 0);
 476      $outdated_cache       = $ct_cache_last_update + (60 * 60 * 24 * 7); // 1 week
 477      if (time() > $outdated_cache) {
 478        $success = $this->core->updateContentTypeCache();
 479        if (!$success) {
 480          return false;
 481        }
 482      }
 483      return true;
 484    }
 485  
 486    /**
 487     * Gets content type cache for globally available libraries and the order
 488     * in which they have been used by the author
 489     *
 490     * @param bool $cacheOutdated The cache is outdated and not able to update
 491     */
 492    private function getContentTypeCache($cacheOutdated = FALSE) {
 493      $canUpdateOrInstall = ($this->core->h5pF->hasPermission(H5PPermission::INSTALL_RECOMMENDED) ||
 494                             $this->core->h5pF->hasPermission(H5PPermission::UPDATE_LIBRARIES));
 495      return array(
 496        'outdated' => $cacheOutdated && $canUpdateOrInstall,
 497        'libraries' => $this->editor->getLatestGlobalLibrariesData(),
 498        'recentlyUsed' => $this->editor->ajaxInterface->getAuthorsRecentlyUsedLibraries(),
 499        'apiVersion' => array(
 500          'major' => H5PCore::$coreApi['majorVersion'],
 501          'minor' => H5PCore::$coreApi['minorVersion']
 502        ),
 503        'details' => $this->core->h5pF->getMessages('info')
 504      );
 505    }
 506  }