1<?php
2
3namespace Moodle;
4
5abstract 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 */
56class 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}
456