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 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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