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