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