MDL-68068 core_h5p: Replace $_SESSION by $SESSION
[moodle.git] / h5p / h5plib / v124 / joubel / core / h5p.classes.php
CommitLineData
45899564
AA
1<?php
2/**
3 * Interface defining functions the h5p library needs the framework to implement
4 */
5interface H5PFrameworkInterface {
6
7 /**
8 * Returns info for the current platform
9 *
10 * @return array
11 * An associative array containing:
12 * - name: The name of the platform, for instance "Wordpress"
13 * - version: The version of the platform, for instance "4.0"
14 * - h5pVersion: The version of the H5P plugin/module
15 */
16 public function getPlatformInfo();
17
18
19 /**
20 * Fetches a file from a remote server using HTTP GET
21 *
22 * @param string $url Where you want to get or send data.
23 * @param array $data Data to post to the URL.
24 * @param bool $blocking Set to 'FALSE' to instantly time out (fire and forget).
25 * @param string $stream Path to where the file should be saved.
26 * @return string The content (response body). NULL if something went wrong
27 */
28 public function fetchExternalData($url, $data = NULL, $blocking = TRUE, $stream = NULL);
29
30 /**
31 * Set the tutorial URL for a library. All versions of the library is set
32 *
33 * @param string $machineName
34 * @param string $tutorialUrl
35 */
36 public function setLibraryTutorialUrl($machineName, $tutorialUrl);
37
38 /**
39 * Show the user an error message
40 *
41 * @param string $message The error message
42 * @param string $code An optional code
43 */
44 public function setErrorMessage($message, $code = NULL);
45
46 /**
47 * Show the user an information message
48 *
49 * @param string $message
50 * The error message
51 */
52 public function setInfoMessage($message);
53
54 /**
55 * Return messages
56 *
57 * @param string $type 'info' or 'error'
58 * @return string[]
59 */
60 public function getMessages($type);
61
62 /**
63 * Translation function
64 *
65 * @param string $message
66 * The english string to be translated.
67 * @param array $replacements
68 * An associative array of replacements to make after translation. Incidences
69 * of any key in this array are replaced with the corresponding value. Based
70 * on the first character of the key, the value is escaped and/or themed:
71 * - !variable: inserted as is
72 * - @variable: escape plain text to HTML
73 * - %variable: escape text and theme as a placeholder for user-submitted
74 * content
75 * @return string Translated string
76 * Translated string
77 */
78 public function t($message, $replacements = array());
79
80 /**
81 * Get URL to file in the specific library
82 * @param string $libraryFolderName
83 * @param string $fileName
84 * @return string URL to file
85 */
86 public function getLibraryFileUrl($libraryFolderName, $fileName);
87
88 /**
89 * Get the Path to the last uploaded h5p
90 *
91 * @return string
92 * Path to the folder where the last uploaded h5p for this session is located.
93 */
94 public function getUploadedH5pFolderPath();
95
96 /**
97 * Get the path to the last uploaded h5p file
98 *
99 * @return string
100 * Path to the last uploaded h5p
101 */
102 public function getUploadedH5pPath();
103
104 /**
105 * Load addon libraries
106 *
107 * @return array
108 */
109 public function loadAddons();
110
111 /**
112 * Load config for libraries
113 *
114 * @param array $libraries
115 * @return array
116 */
117 public function getLibraryConfig($libraries = NULL);
118
119 /**
120 * Get a list of the current installed libraries
121 *
122 * @return array
123 * Associative array containing one entry per machine name.
124 * For each machineName there is a list of libraries(with different versions)
125 */
126 public function loadLibraries();
127
128 /**
129 * Returns the URL to the library admin page
130 *
131 * @return string
132 * URL to admin page
133 */
134 public function getAdminUrl();
135
136 /**
137 * Get id to an existing library.
138 * If version number is not specified, the newest version will be returned.
139 *
140 * @param string $machineName
141 * The librarys machine name
142 * @param int $majorVersion
143 * Optional major version number for library
144 * @param int $minorVersion
145 * Optional minor version number for library
146 * @return int
147 * The id of the specified library or FALSE
148 */
149 public function getLibraryId($machineName, $majorVersion = NULL, $minorVersion = NULL);
150
151 /**
152 * Get file extension whitelist
153 *
154 * The default extension list is part of h5p, but admins should be allowed to modify it
155 *
156 * @param boolean $isLibrary
157 * TRUE if this is the whitelist for a library. FALSE if it is the whitelist
158 * for the content folder we are getting
159 * @param string $defaultContentWhitelist
160 * A string of file extensions separated by whitespace
161 * @param string $defaultLibraryWhitelist
162 * A string of file extensions separated by whitespace
163 */
164 public function getWhitelist($isLibrary, $defaultContentWhitelist, $defaultLibraryWhitelist);
165
166 /**
167 * Is the library a patched version of an existing library?
168 *
169 * @param object $library
170 * An associative array containing:
171 * - machineName: The library machineName
172 * - majorVersion: The librarys majorVersion
173 * - minorVersion: The librarys minorVersion
174 * - patchVersion: The librarys patchVersion
175 * @return boolean
176 * TRUE if the library is a patched version of an existing library
177 * FALSE otherwise
178 */
179 public function isPatchedLibrary($library);
180
181 /**
182 * Is H5P in development mode?
183 *
184 * @return boolean
185 * TRUE if H5P development mode is active
186 * FALSE otherwise
187 */
188 public function isInDevMode();
189
190 /**
191 * Is the current user allowed to update libraries?
192 *
193 * @return boolean
194 * TRUE if the user is allowed to update libraries
195 * FALSE if the user is not allowed to update libraries
196 */
197 public function mayUpdateLibraries();
198
199 /**
200 * Store data about a library
201 *
202 * Also fills in the libraryId in the libraryData object if the object is new
203 *
204 * @param object $libraryData
205 * Associative array containing:
206 * - libraryId: The id of the library if it is an existing library.
207 * - title: The library's name
208 * - machineName: The library machineName
209 * - majorVersion: The library's majorVersion
210 * - minorVersion: The library's minorVersion
211 * - patchVersion: The library's patchVersion
212 * - runnable: 1 if the library is a content type, 0 otherwise
213 * - metadataSettings: Associative array containing:
214 * - disable: 1 if the library should not support setting metadata (copyright etc)
215 * - disableExtraTitleField: 1 if the library don't need the extra title field
216 * - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
217 * - embedTypes(optional): list of supported embed types
218 * - preloadedJs(optional): list of associative arrays containing:
219 * - path: path to a js file relative to the library root folder
220 * - preloadedCss(optional): list of associative arrays containing:
221 * - path: path to css file relative to the library root folder
222 * - dropLibraryCss(optional): list of associative arrays containing:
223 * - machineName: machine name for the librarys that are to drop their css
224 * - semantics(optional): Json describing the content structure for the library
225 * - language(optional): associative array containing:
226 * - languageCode: Translation in json format
227 * @param bool $new
228 * @return
229 */
230 public function saveLibraryData(&$libraryData, $new = TRUE);
231
232 /**
233 * Insert new content.
234 *
235 * @param array $content
236 * An associative array containing:
237 * - id: The content id
238 * - params: The content in json format
239 * - library: An associative array containing:
240 * - libraryId: The id of the main library for this content
241 * @param int $contentMainId
242 * Main id for the content if this is a system that supports versions
243 */
244 public function insertContent($content, $contentMainId = NULL);
245
246 /**
247 * Update old content.
248 *
249 * @param array $content
250 * An associative array containing:
251 * - id: The content id
252 * - params: The content in json format
253 * - library: An associative array containing:
254 * - libraryId: The id of the main library for this content
255 * @param int $contentMainId
256 * Main id for the content if this is a system that supports versions
257 */
258 public function updateContent($content, $contentMainId = NULL);
259
260 /**
261 * Resets marked user data for the given content.
262 *
263 * @param int $contentId
264 */
265 public function resetContentUserData($contentId);
266
267 /**
268 * Save what libraries a library is depending on
269 *
270 * @param int $libraryId
271 * Library Id for the library we're saving dependencies for
272 * @param array $dependencies
273 * List of dependencies as associative arrays containing:
274 * - machineName: The library machineName
275 * - majorVersion: The library's majorVersion
276 * - minorVersion: The library's minorVersion
277 * @param string $dependency_type
278 * What type of dependency this is, the following values are allowed:
279 * - editor
280 * - preloaded
281 * - dynamic
282 */
283 public function saveLibraryDependencies($libraryId, $dependencies, $dependency_type);
284
285 /**
286 * Give an H5P the same library dependencies as a given H5P
287 *
288 * @param int $contentId
289 * Id identifying the content
290 * @param int $copyFromId
291 * Id identifying the content to be copied
292 * @param int $contentMainId
293 * Main id for the content, typically used in frameworks
294 * That supports versions. (In this case the content id will typically be
295 * the version id, and the contentMainId will be the frameworks content id
296 */
297 public function copyLibraryUsage($contentId, $copyFromId, $contentMainId = NULL);
298
299 /**
300 * Deletes content data
301 *
302 * @param int $contentId
303 * Id identifying the content
304 */
305 public function deleteContentData($contentId);
306
307 /**
308 * Delete what libraries a content item is using
309 *
310 * @param int $contentId
311 * Content Id of the content we'll be deleting library usage for
312 */
313 public function deleteLibraryUsage($contentId);
314
315 /**
316 * Saves what libraries the content uses
317 *
318 * @param int $contentId
319 * Id identifying the content
320 * @param array $librariesInUse
321 * List of libraries the content uses. Libraries consist of associative arrays with:
322 * - library: Associative array containing:
323 * - dropLibraryCss(optional): comma separated list of machineNames
324 * - machineName: Machine name for the library
325 * - libraryId: Id of the library
326 * - type: The dependency type. Allowed values:
327 * - editor
328 * - dynamic
329 * - preloaded
330 */
331 public function saveLibraryUsage($contentId, $librariesInUse);
332
333 /**
334 * Get number of content/nodes using a library, and the number of
335 * dependencies to other libraries
336 *
337 * @param int $libraryId
338 * Library identifier
339 * @param boolean $skipContent
340 * Flag to indicate if content usage should be skipped
341 * @return array
342 * Associative array containing:
343 * - content: Number of content using the library
344 * - libraries: Number of libraries depending on the library
345 */
346 public function getLibraryUsage($libraryId, $skipContent = FALSE);
347
348 /**
349 * Loads a library
350 *
351 * @param string $machineName
352 * The library's machine name
353 * @param int $majorVersion
354 * The library's major version
355 * @param int $minorVersion
356 * The library's minor version
357 * @return array|FALSE
358 * FALSE if the library does not exist.
359 * Otherwise an associative array containing:
360 * - libraryId: The id of the library if it is an existing library.
361 * - title: The library's name
362 * - machineName: The library machineName
363 * - majorVersion: The library's majorVersion
364 * - minorVersion: The library's minorVersion
365 * - patchVersion: The library's patchVersion
366 * - runnable: 1 if the library is a content type, 0 otherwise
367 * - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
368 * - embedTypes(optional): list of supported embed types
369 * - preloadedJs(optional): comma separated string with js file paths
370 * - preloadedCss(optional): comma separated sting with css file paths
371 * - dropLibraryCss(optional): list of associative arrays containing:
372 * - machineName: machine name for the librarys that are to drop their css
373 * - semantics(optional): Json describing the content structure for the library
374 * - preloadedDependencies(optional): list of associative arrays containing:
375 * - machineName: Machine name for a library this library is depending on
376 * - majorVersion: Major version for a library this library is depending on
377 * - minorVersion: Minor for a library this library is depending on
378 * - dynamicDependencies(optional): list of associative arrays containing:
379 * - machineName: Machine name for a library this library is depending on
380 * - majorVersion: Major version for a library this library is depending on
381 * - minorVersion: Minor for a library this library is depending on
382 * - editorDependencies(optional): list of associative arrays containing:
383 * - machineName: Machine name for a library this library is depending on
384 * - majorVersion: Major version for a library this library is depending on
385 * - minorVersion: Minor for a library this library is depending on
386 */
387 public function loadLibrary($machineName, $majorVersion, $minorVersion);
388
389 /**
390 * Loads library semantics.
391 *
392 * @param string $machineName
393 * Machine name for the library
394 * @param int $majorVersion
395 * The library's major version
396 * @param int $minorVersion
397 * The library's minor version
398 * @return string
399 * The library's semantics as json
400 */
401 public function loadLibrarySemantics($machineName, $majorVersion, $minorVersion);
402
403 /**
404 * Makes it possible to alter the semantics, adding custom fields, etc.
405 *
406 * @param array $semantics
407 * Associative array representing the semantics
408 * @param string $machineName
409 * The library's machine name
410 * @param int $majorVersion
411 * The library's major version
412 * @param int $minorVersion
413 * The library's minor version
414 */
415 public function alterLibrarySemantics(&$semantics, $machineName, $majorVersion, $minorVersion);
416
417 /**
418 * Delete all dependencies belonging to given library
419 *
420 * @param int $libraryId
421 * Library identifier
422 */
423 public function deleteLibraryDependencies($libraryId);
424
425 /**
426 * Start an atomic operation against the dependency storage
427 */
428 public function lockDependencyStorage();
429
430 /**
431 * Stops an atomic operation against the dependency storage
432 */
433 public function unlockDependencyStorage();
434
435
436 /**
437 * Delete a library from database and file system
438 *
439 * @param stdClass $library
440 * Library object with id, name, major version and minor version.
441 */
442 public function deleteLibrary($library);
443
444 /**
445 * Load content.
446 *
447 * @param int $id
448 * Content identifier
449 * @return array
450 * Associative array containing:
451 * - contentId: Identifier for the content
452 * - params: json content as string
453 * - embedType: csv of embed types
454 * - title: The contents title
455 * - language: Language code for the content
456 * - libraryId: Id for the main library
457 * - libraryName: The library machine name
458 * - libraryMajorVersion: The library's majorVersion
459 * - libraryMinorVersion: The library's minorVersion
460 * - libraryEmbedTypes: CSV of the main library's embed types
461 * - libraryFullscreen: 1 if fullscreen is supported. 0 otherwise.
462 */
463 public function loadContent($id);
464
465 /**
466 * Load dependencies for the given content of the given type.
467 *
468 * @param int $id
469 * Content identifier
470 * @param int $type
471 * Dependency types. Allowed values:
472 * - editor
473 * - preloaded
474 * - dynamic
475 * @return array
476 * List of associative arrays containing:
477 * - libraryId: The id of the library if it is an existing library.
478 * - machineName: The library machineName
479 * - majorVersion: The library's majorVersion
480 * - minorVersion: The library's minorVersion
481 * - patchVersion: The library's patchVersion
482 * - preloadedJs(optional): comma separated string with js file paths
483 * - preloadedCss(optional): comma separated sting with css file paths
484 * - dropCss(optional): csv of machine names
485 */
486 public function loadContentDependencies($id, $type = NULL);
487
488 /**
489 * Get stored setting.
490 *
491 * @param string $name
492 * Identifier for the setting
493 * @param string $default
494 * Optional default value if settings is not set
495 * @return mixed
496 * Whatever has been stored as the setting
497 */
498 public function getOption($name, $default = NULL);
499
500 /**
501 * Stores the given setting.
502 * For example when did we last check h5p.org for updates to our libraries.
503 *
504 * @param string $name
505 * Identifier for the setting
506 * @param mixed $value Data
507 * Whatever we want to store as the setting
508 */
509 public function setOption($name, $value);
510
511 /**
512 * This will update selected fields on the given content.
513 *
514 * @param int $id Content identifier
515 * @param array $fields Content fields, e.g. filtered or slug.
516 */
517 public function updateContentFields($id, $fields);
518
519 /**
520 * Will clear filtered params for all the content that uses the specified
521 * libraries. This means that the content dependencies will have to be rebuilt,
522 * and the parameters re-filtered.
523 *
524 * @param array $library_ids
525 */
526 public function clearFilteredParameters($library_ids);
527
528 /**
529 * Get number of contents that has to get their content dependencies rebuilt
530 * and parameters re-filtered.
531 *
532 * @return int
533 */
534 public function getNumNotFiltered();
535
536 /**
537 * Get number of contents using library as main library.
538 *
539 * @param int $libraryId
540 * @param array $skip
541 * @return int
542 */
543 public function getNumContent($libraryId, $skip = NULL);
544
545 /**
546 * Determines if content slug is used.
547 *
548 * @param string $slug
549 * @return boolean
550 */
551 public function isContentSlugAvailable($slug);
552
553 /**
554 * Generates statistics from the event log per library
555 *
556 * @param string $type Type of event to generate stats for
557 * @return array Number values indexed by library name and version
558 */
559 public function getLibraryStats($type);
560
561 /**
562 * Aggregate the current number of H5P authors
563 * @return int
564 */
565 public function getNumAuthors();
566
567 /**
568 * Stores hash keys for cached assets, aggregated JavaScripts and
569 * stylesheets, and connects it to libraries so that we know which cache file
570 * to delete when a library is updated.
571 *
572 * @param string $key
573 * Hash key for the given libraries
574 * @param array $libraries
575 * List of dependencies(libraries) used to create the key
576 */
577 public function saveCachedAssets($key, $libraries);
578
579 /**
580 * Locate hash keys for given library and delete them.
581 * Used when cache file are deleted.
582 *
583 * @param int $library_id
584 * Library identifier
585 * @return array
586 * List of hash keys removed
587 */
588 public function deleteCachedAssets($library_id);
589
590 /**
591 * Get the amount of content items associated to a library
592 * return int
593 */
594 public function getLibraryContentCount();
595
596 /**
597 * Will trigger after the export file is created.
598 */
599 public function afterExportCreated($content, $filename);
600
601 /**
602 * Check if user has permissions to an action
603 *
604 * @method hasPermission
605 * @param [H5PPermission] $permission Permission type, ref H5PPermission
606 * @param [int] $id Id need by platform to determine permission
607 * @return boolean
608 */
609 public function hasPermission($permission, $id = NULL);
610
611 /**
612 * Replaces existing content type cache with the one passed in
613 *
614 * @param object $contentTypeCache Json with an array called 'libraries'
615 * containing the new content type cache that should replace the old one.
616 */
617 public function replaceContentTypeCache($contentTypeCache);
618
619 /**
620 * Checks if the given library has a higher version.
621 *
622 * @param array $library
623 * @return boolean
624 */
625 public function libraryHasUpgrade($library);
626}
627
628/**
629 * This class is used for validating H5P files
630 */
631class H5PValidator {
632 public $h5pF;
633 public $h5pC;
634
635 // Schemas used to validate the h5p files
636 private $h5pRequired = array(
637 'title' => '/^.{1,255}$/',
638 'language' => '/^[-a-zA-Z]{1,10}$/',
639 'preloadedDependencies' => array(
640 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
641 'majorVersion' => '/^[0-9]{1,5}$/',
642 'minorVersion' => '/^[0-9]{1,5}$/',
643 ),
644 'mainLibrary' => '/^[$a-z_][0-9a-z_\.$]{1,254}$/i',
645 'embedTypes' => array('iframe', 'div'),
646 );
647
648 private $h5pOptional = array(
649 'contentType' => '/^.{1,255}$/',
650 'dynamicDependencies' => array(
651 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
652 'majorVersion' => '/^[0-9]{1,5}$/',
653 'minorVersion' => '/^[0-9]{1,5}$/',
654 ),
655 // deprecated
656 'author' => '/^.{1,255}$/',
657 'authors' => array(
658 'name' => '/^.{1,255}$/',
659 'role' => '/^\w+$/',
660 ),
661 'source' => '/^(http[s]?:\/\/.+)$/',
662 'license' => '/^(CC BY|CC BY-SA|CC BY-ND|CC BY-NC|CC BY-NC-SA|CC BY-NC-ND|CC0 1\.0|GNU GPL|PD|ODC PDDL|CC PDM|U|C)$/',
663 'licenseVersion' => '/^(1\.0|2\.0|2\.5|3\.0|4\.0)$/',
664 'licenseExtras' => '/^.{1,5000}$/',
665 'yearsFrom' => '/^([0-9]{1,4})$/',
666 'yearsTo' => '/^([0-9]{1,4})$/',
667 'changes' => array(
668 'date' => '/^[0-9]{2}-[0-9]{2}-[0-9]{2} [0-9]{1,2}:[0-9]{2}:[0-9]{2}$/',
669 'author' => '/^.{1,255}$/',
670 'log' => '/^.{1,5000}$/'
671 ),
672 'authorComments' => '/^.{1,5000}$/',
673 'w' => '/^[0-9]{1,4}$/',
674 'h' => '/^[0-9]{1,4}$/',
675 // deprecated
676 'metaKeywords' => '/^.{1,}$/',
677 // deprecated
678 'metaDescription' => '/^.{1,}$/',
679 );
680
681 // Schemas used to validate the library files
682 private $libraryRequired = array(
683 'title' => '/^.{1,255}$/',
684 'majorVersion' => '/^[0-9]{1,5}$/',
685 'minorVersion' => '/^[0-9]{1,5}$/',
686 'patchVersion' => '/^[0-9]{1,5}$/',
687 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
688 'runnable' => '/^(0|1)$/',
689 );
690
691 private $libraryOptional = array(
692 'author' => '/^.{1,255}$/',
693 'license' => '/^(cc-by|cc-by-sa|cc-by-nd|cc-by-nc|cc-by-nc-sa|cc-by-nc-nd|pd|cr|MIT|GPL1|GPL2|GPL3|MPL|MPL2)$/',
694 'description' => '/^.{1,}$/',
695 'metadataSettings' => array(
696 'disable' => '/^(0|1)$/',
697 'disableExtraTitleField' => '/^(0|1)$/'
698 ),
699 'dynamicDependencies' => array(
700 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
701 'majorVersion' => '/^[0-9]{1,5}$/',
702 'minorVersion' => '/^[0-9]{1,5}$/',
703 ),
704 'preloadedDependencies' => array(
705 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
706 'majorVersion' => '/^[0-9]{1,5}$/',
707 'minorVersion' => '/^[0-9]{1,5}$/',
708 ),
709 'editorDependencies' => array(
710 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
711 'majorVersion' => '/^[0-9]{1,5}$/',
712 'minorVersion' => '/^[0-9]{1,5}$/',
713 ),
714 'preloadedJs' => array(
715 'path' => '/^((\\\|\/)?[a-z_\-\s0-9\.]+)+\.js$/i',
716 ),
717 'preloadedCss' => array(
718 'path' => '/^((\\\|\/)?[a-z_\-\s0-9\.]+)+\.css$/i',
719 ),
720 'dropLibraryCss' => array(
721 'machineName' => '/^[\w0-9\-\.]{1,255}$/i',
722 ),
723 'w' => '/^[0-9]{1,4}$/',
724 'h' => '/^[0-9]{1,4}$/',
725 'embedTypes' => array('iframe', 'div'),
726 'fullscreen' => '/^(0|1)$/',
727 'coreApi' => array(
728 'majorVersion' => '/^[0-9]{1,5}$/',
729 'minorVersion' => '/^[0-9]{1,5}$/',
730 ),
731 );
732
733 /**
734 * Constructor for the H5PValidator
735 *
736 * @param H5PFrameworkInterface $H5PFramework
737 * The frameworks implementation of the H5PFrameworkInterface
738 * @param H5PCore $H5PCore
739 */
740 public function __construct($H5PFramework, $H5PCore) {
741 $this->h5pF = $H5PFramework;
742 $this->h5pC = $H5PCore;
743 $this->h5pCV = new H5PContentValidator($this->h5pF, $this->h5pC);
744 }
745
746 /**
747 * Validates a .h5p file
748 *
749 * @param bool $skipContent
750 * @param bool $upgradeOnly
751 * @return bool TRUE if the .h5p file is valid
752 * TRUE if the .h5p file is valid
753 */
754 public function isValidPackage($skipContent = FALSE, $upgradeOnly = FALSE) {
755 // Check dependencies, make sure Zip is present
756 if (!class_exists('ZipArchive')) {
757 $this->h5pF->setErrorMessage($this->h5pF->t('Your PHP version does not support ZipArchive.'), 'zip-archive-unsupported');
758 unlink($tmpPath);
759 return FALSE;
760 }
5da7a7fc
SA
761 // Moodle: the extension mbstring is optional.
762 /*
45899564
AA
763 if (!extension_loaded('mbstring')) {
764 $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
765 unlink($tmpPath);
766 return FALSE;
767 }
5da7a7fc 768 */
45899564
AA
769
770 // Create a temporary dir to extract package in.
771 $tmpDir = $this->h5pF->getUploadedH5pFolderPath();
772 $tmpPath = $this->h5pF->getUploadedH5pPath();
773
774 // Only allow files with the .h5p extension:
775 if (strtolower(substr($tmpPath, -3)) !== 'h5p') {
776 $this->h5pF->setErrorMessage($this->h5pF->t('The file you uploaded is not a valid HTML5 Package (It does not have the .h5p file extension)'), 'missing-h5p-extension');
777 unlink($tmpPath);
778 return FALSE;
779 }
780
781 // Extract and then remove the package file.
782 $zip = new ZipArchive;
783
784 // Open the package
785 if ($zip->open($tmpPath) !== TRUE) {
786 $this->h5pF->setErrorMessage($this->h5pF->t('The file you uploaded is not a valid HTML5 Package (We are unable to unzip it)'), 'unable-to-unzip');
787 unlink($tmpPath);
788 return FALSE;
789 }
790
791 if ($this->h5pC->disableFileCheck !== TRUE) {
792 list($contentWhitelist, $contentRegExp) = $this->getWhitelistRegExp(FALSE);
793 list($libraryWhitelist, $libraryRegExp) = $this->getWhitelistRegExp(TRUE);
794 }
795 $canInstall = $this->h5pC->mayUpdateLibraries();
796
797 $valid = TRUE;
798 $libraries = array();
799
800 $totalSize = 0;
801 $mainH5pExists = FALSE;
802 $contentExists = FALSE;
803
804 // Check for valid file types, JSON files + file sizes before continuing to unpack.
805 for ($i = 0; $i < $zip->numFiles; $i++) {
806 $fileStat = $zip->statIndex($i);
807
808 if (!empty($this->h5pC->maxFileSize) && $fileStat['size'] > $this->h5pC->maxFileSize) {
809 // Error file is too large
810 $this->h5pF->setErrorMessage($this->h5pF->t('One of the files inside the package exceeds the maximum file size allowed. (%file %used > %max)', array('%file' => $fileStat['name'], '%used' => ($fileStat['size'] / 1048576) . ' MB', '%max' => ($this->h5pC->maxFileSize / 1048576) . ' MB')), 'file-size-too-large');
811 $valid = FALSE;
812 }
813 $totalSize += $fileStat['size'];
814
5da7a7fc 815 $fileName = \core_text::strtolower($fileStat['name']);
45899564
AA
816 if (preg_match('/(^[\._]|\/[\._])/', $fileName) !== 0) {
817 continue; // Skip any file or folder starting with a . or _
818 }
819 elseif ($fileName === 'h5p.json') {
820 $mainH5pExists = TRUE;
821 }
822 elseif ($fileName === 'content/content.json') {
823 $contentExists = TRUE;
824 }
825 elseif (substr($fileName, 0, 8) === 'content/') {
826 // This is a content file, check that the file type is allowed
827 if ($skipContent === FALSE && $this->h5pC->disableFileCheck !== TRUE && !preg_match($contentRegExp, $fileName)) {
828 $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $fileStat['name'], '%files-allowed' => $contentWhitelist)), 'not-in-whitelist');
829 $valid = FALSE;
830 }
831 }
832 elseif ($canInstall && strpos($fileName, '/') !== FALSE) {
833 // This is a library file, check that the file type is allowed
834 if ($this->h5pC->disableFileCheck !== TRUE && !preg_match($libraryRegExp, $fileName)) {
835 $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $fileStat['name'], '%files-allowed' => $libraryWhitelist)), 'not-in-whitelist');
836 $valid = FALSE;
837 }
838
839 // Further library validation happens after the files are extracted
840 }
841 }
842
843 if (!empty($this->h5pC->maxTotalSize) && $totalSize > $this->h5pC->maxTotalSize) {
844 // Error total size of the zip is too large
845 $this->h5pF->setErrorMessage($this->h5pF->t('The total size of the unpacked files exceeds the maximum size allowed. (%used > %max)', array('%used' => ($totalSize / 1048576) . ' MB', '%max' => ($this->h5pC->maxTotalSize / 1048576) . ' MB')), 'total-size-too-large');
846 $valid = FALSE;
847 }
848
849 if ($skipContent === FALSE) {
850 // Not skipping content, require two valid JSON files from the package
851 if (!$contentExists) {
852 $this->h5pF->setErrorMessage($this->h5pF->t('A valid content folder is missing'), 'invalid-content-folder');
853 $valid = FALSE;
854 }
855 else {
856 $contentJsonData = $this->getJson($tmpPath, $zip, 'content/content.json'); // TODO: Is this case-senstivie?
857 if ($contentJsonData === NULL) {
858 return FALSE; // Breaking error when reading from the archive.
859 }
860 elseif ($contentJsonData === FALSE) {
861 $valid = FALSE; // Validation error when parsing JSON
862 }
863 }
864
865 if (!$mainH5pExists) {
866 $this->h5pF->setErrorMessage($this->h5pF->t('A valid main h5p.json file is missing'), 'invalid-h5p-json-file');
867 $valid = FALSE;
868 }
869 else {
870 $mainH5pData = $this->getJson($tmpPath, $zip, 'h5p.json', TRUE);
871 if ($mainH5pData === NULL) {
872 return FALSE; // Breaking error when reading from the archive.
873 }
874 elseif ($mainH5pData === FALSE) {
875 $valid = FALSE; // Validation error when parsing JSON
876 }
877 elseif (!$this->isValidH5pData($mainH5pData, 'h5p.json', $this->h5pRequired, $this->h5pOptional)) {
878 $this->h5pF->setErrorMessage($this->h5pF->t('The main h5p.json file is not valid'), 'invalid-h5p-json-file'); // Is this message a bit redundant?
879 $valid = FALSE;
880 }
881 }
882 }
883
884 if (!$valid) {
885 // If something has failed during the initial checks of the package
886 // we will not unpack it or continue validation.
887 $zip->close();
888 unlink($tmpPath);
889 return FALSE;
890 }
891
892 // Extract the files from the package
893 for ($i = 0; $i < $zip->numFiles; $i++) {
894 $fileName = $zip->statIndex($i)['name'];
895
896 if (preg_match('/(^[\._]|\/[\._])/', $fileName) !== 0) {
897 continue; // Skip any file or folder starting with a . or _
898 }
899
900 $isContentFile = (substr($fileName, 0, 8) === 'content/');
901 $isFolder = (strpos($fileName, '/') !== FALSE);
902
903 if ($skipContent !== FALSE && $isContentFile) {
904 continue; // Skipping any content files
905 }
906
907 if (!($isContentFile || ($canInstall && $isFolder))) {
908 continue; // Not something we want to unpack
909 }
910
911 // Get file stream
912 $fileStream = $zip->getStream($fileName);
913 if (!$fileStream) {
914 // This is a breaking error, there's no need to continue. (the rest of the files will fail as well)
915 $this->h5pF->setErrorMessage($this->h5pF->t('Unable to read file from the package: %fileName', array('%fileName' => $fileName)), 'unable-to-read-package-file');
916 $zip->close();
917 unlink($path);
918 H5PCore::deleteFileTree($tmpDir);
919 return FALSE;
920 }
921
922 // Use file interface to allow overrides
923 $this->h5pC->fs->saveFileFromZip($tmpDir, $fileName, $fileStream);
924
925 // Clean up
926 if (is_resource($fileStream)) {
927 fclose($fileStream);
928 }
929 }
930
931 // We're done with the zip file, clean up the stuff
932 $zip->close();
933 unlink($tmpPath);
934
935 if ($canInstall) {
936 // Process and validate libraries using the unpacked library folders
937 $files = scandir($tmpDir);
938 foreach ($files as $file) {
64153166 939 $filePath = $tmpDir . '/' . $file;
45899564
AA
940
941 if ($file === '.' || $file === '..' || $file === 'content' || !is_dir($filePath)) {
942 continue; // Skip
943 }
944
945 $libraryH5PData = $this->getLibraryData($file, $filePath, $tmpDir);
946 if ($libraryH5PData === FALSE) {
947 $valid = FALSE;
948 continue; // Failed, but continue validating the rest of the libraries
949 }
950
951 // Library's directory name must be:
952 // - <machineName>
953 // - or -
954 // - <machineName>-<majorVersion>.<minorVersion>
955 // where machineName, majorVersion and minorVersion is read from library.json
956 if ($libraryH5PData['machineName'] !== $file && H5PCore::libraryToString($libraryH5PData, TRUE) !== $file) {
957 $this->h5pF->setErrorMessage($this->h5pF->t('Library directory name must match machineName or machineName-majorVersion.minorVersion (from library.json). (Directory: %directoryName , machineName: %machineName, majorVersion: %majorVersion, minorVersion: %minorVersion)', array(
958 '%directoryName' => $file,
959 '%machineName' => $libraryH5PData['machineName'],
960 '%majorVersion' => $libraryH5PData['majorVersion'],
961 '%minorVersion' => $libraryH5PData['minorVersion'])), 'library-directory-name-mismatch');
962 $valid = FALSE;
963 continue; // Failed, but continue validating the rest of the libraries
964 }
965
966 $libraryH5PData['uploadDirectory'] = $filePath;
967 $libraries[H5PCore::libraryToString($libraryH5PData)] = $libraryH5PData;
968 }
969 }
970
971 if ($valid) {
972 if ($upgradeOnly) {
973 // When upgrading, we only add the already installed libraries, and
974 // the new dependent libraries
975 $upgrades = array();
976 foreach ($libraries as $libString => &$library) {
977 // Is this library already installed?
978 if ($this->h5pF->getLibraryId($library['machineName']) !== FALSE) {
979 $upgrades[$libString] = $library;
980 }
981 }
982 while ($missingLibraries = $this->getMissingLibraries($upgrades)) {
983 foreach ($missingLibraries as $libString => $missing) {
984 $library = $libraries[$libString];
985 if ($library) {
986 $upgrades[$libString] = $library;
987 }
988 }
989 }
990
991 $libraries = $upgrades;
992 }
993
994 $this->h5pC->librariesJsonData = $libraries;
995
996 if ($skipContent === FALSE) {
997 $this->h5pC->mainJsonData = $mainH5pData;
998 $this->h5pC->contentJsonData = $contentJsonData;
999 $libraries['mainH5pData'] = $mainH5pData; // Check for the dependencies in h5p.json as well as in the libraries
1000 }
1001
1002 $missingLibraries = $this->getMissingLibraries($libraries);
1003 foreach ($missingLibraries as $libString => $missing) {
1004 if ($this->h5pC->getLibraryId($missing, $libString)) {
1005 unset($missingLibraries[$libString]);
1006 }
1007 }
1008
1009 if (!empty($missingLibraries)) {
1010 // We still have missing libraries, check if our main library has an upgrade (BUT only if we has content)
1011 $mainDependency = NULL;
1012 if (!$skipContent && !empty($mainH5pData)) {
1013 foreach ($mainH5pData['preloadedDependencies'] as $dep) {
1014 if ($dep['machineName'] === $mainH5pData['mainLibrary']) {
1015 $mainDependency = $dep;
1016 }
1017 }
1018 }
1019
1020 if ($skipContent || !$mainDependency || !$this->h5pF->libraryHasUpgrade(array(
1021 'machineName' => $mainDependency['machineName'],
1022 'majorVersion' => $mainDependency['majorVersion'],
1023 'minorVersion' => $mainDependency['minorVersion']
1024 ))) {
1025 foreach ($missingLibraries as $libString => $library) {
1026 $this->h5pF->setErrorMessage($this->h5pF->t('Missing required library @library', array('@library' => $libString)), 'missing-required-library');
1027 $valid = FALSE;
1028 }
1029 if (!$this->h5pC->mayUpdateLibraries()) {
1030 $this->h5pF->setInfoMessage($this->h5pF->t("Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries. Contact the site administrator about this."));
1031 $valid = FALSE;
1032 }
1033 }
1034 }
1035 }
1036 if (!$valid) {
1037 H5PCore::deleteFileTree($tmpDir);
1038 }
1039 return $valid;
1040 }
1041
1042 /**
1043 * Help read JSON from the archive
1044 *
1045 * @param string $path
1046 * @param ZipArchive $zip
1047 * @param string $file
1048 * @return mixed JSON content if valid, FALSE for invalid, NULL for breaking error.
1049 */
1050 private function getJson($path, $zip, $file, $assoc = FALSE) {
1051 // Get stream
1052 $stream = $zip->getStream($file);
1053 if (!$stream) {
1054 // Breaking error, no need to continue validating.
1055 $this->h5pF->setErrorMessage($this->h5pF->t('Unable to read file from the package: %fileName', array('%fileName' => $file)), 'unable-to-read-package-file');
1056 $zip->close();
1057 unlink($path);
1058 return NULL;
1059 }
1060
1061 // Read data
1062 $contents = '';
1063 while (!feof($stream)) {
1064 $contents .= fread($stream, 2);
1065 }
1066
1067 // Decode the data
1068 $json = json_decode($contents, $assoc);
1069 if ($json === NULL) {
1070 // JSON cannot be decoded or the recursion limit has been reached.
1071 $this->h5pF->setErrorMessage($this->h5pF->t('Unable to parse JSON from the package: %fileName', array('%fileName' => $file)), 'unable-to-parse-package');
1072 return FALSE;
1073 }
1074
1075 // All OK
1076 return $json;
1077 }
1078
1079 /**
1080 * Help retrieve file type regexp whitelist from plugin.
1081 *
1082 * @param bool $isLibrary Separate list with more allowed file types
1083 * @return string RegExp
1084 */
1085 private function getWhitelistRegExp($isLibrary) {
1086 $whitelist = $this->h5pF->getWhitelist($isLibrary, H5PCore::$defaultContentWhitelist, H5PCore::$defaultLibraryWhitelistExtras);
1087 return array($whitelist, '/\.(' . preg_replace('/ +/i', '|', preg_quote($whitelist)) . ')$/i');
1088 }
1089
1090 /**
1091 * Validates a H5P library
1092 *
1093 * @param string $file
1094 * Name of the library folder
1095 * @param string $filePath
1096 * Path to the library folder
1097 * @param string $tmpDir
1098 * Path to the temporary upload directory
1099 * @return boolean|array
1100 * H5P data from library.json and semantics if the library is valid
1101 * FALSE if the library isn't valid
1102 */
1103 public function getLibraryData($file, $filePath, $tmpDir) {
1104 if (preg_match('/^[\w0-9\-\.]{1,255}$/i', $file) === 0) {
1105 $this->h5pF->setErrorMessage($this->h5pF->t('Invalid library name: %name', array('%name' => $file)), 'invalid-library-name');
1106 return FALSE;
1107 }
64153166 1108 $h5pData = $this->getJsonData($filePath . '/' . 'library.json');
45899564
AA
1109 if ($h5pData === FALSE) {
1110 $this->h5pF->setErrorMessage($this->h5pF->t('Could not find library.json file with valid json format for library %name', array('%name' => $file)), 'invalid-library-json-file');
1111 return FALSE;
1112 }
1113
1114 // validate json if a semantics file is provided
64153166 1115 $semanticsPath = $filePath . '/' . 'semantics.json';
45899564
AA
1116 if (file_exists($semanticsPath)) {
1117 $semantics = $this->getJsonData($semanticsPath, TRUE);
1118 if ($semantics === FALSE) {
1119 $this->h5pF->setErrorMessage($this->h5pF->t('Invalid semantics.json file has been included in the library %name', array('%name' => $file)), 'invalid-semantics-json-file');
1120 return FALSE;
1121 }
1122 else {
1123 $h5pData['semantics'] = $semantics;
1124 }
1125 }
1126
1127 // validate language folder if it exists
64153166 1128 $languagePath = $filePath . '/' . 'language';
45899564
AA
1129 if (is_dir($languagePath)) {
1130 $languageFiles = scandir($languagePath);
1131 foreach ($languageFiles as $languageFile) {
1132 if (in_array($languageFile, array('.', '..'))) {
1133 continue;
1134 }
1135 if (preg_match('/^(-?[a-z]+){1,7}\.json$/i', $languageFile) === 0) {
1136 $this->h5pF->setErrorMessage($this->h5pF->t('Invalid language file %file in library %library', array('%file' => $languageFile, '%library' => $file)), 'invalid-language-file');
1137 return FALSE;
1138 }
64153166 1139 $languageJson = $this->getJsonData($languagePath . '/' . $languageFile, TRUE);
45899564
AA
1140 if ($languageJson === FALSE) {
1141 $this->h5pF->setErrorMessage($this->h5pF->t('Invalid language file %languageFile has been included in the library %name', array('%languageFile' => $languageFile, '%name' => $file)), 'invalid-language-file');
1142 return FALSE;
1143 }
1144 $parts = explode('.', $languageFile); // $parts[0] is the language code
1145 $h5pData['language'][$parts[0]] = $languageJson;
1146 }
1147 }
1148
1149 // Check for icon:
64153166 1150 $h5pData['hasIcon'] = file_exists($filePath . '/' . 'icon.svg');
45899564
AA
1151
1152 $validLibrary = $this->isValidH5pData($h5pData, $file, $this->libraryRequired, $this->libraryOptional);
1153
1154 //$validLibrary = $this->h5pCV->validateContentFiles($filePath, TRUE) && $validLibrary;
1155
1156 if (isset($h5pData['preloadedJs'])) {
1157 $validLibrary = $this->isExistingFiles($h5pData['preloadedJs'], $tmpDir, $file) && $validLibrary;
1158 }
1159 if (isset($h5pData['preloadedCss'])) {
1160 $validLibrary = $this->isExistingFiles($h5pData['preloadedCss'], $tmpDir, $file) && $validLibrary;
1161 }
1162 if ($validLibrary) {
1163 return $h5pData;
1164 }
1165 else {
1166 return FALSE;
1167 }
1168 }
1169
1170 /**
1171 * Use the dependency declarations to find any missing libraries
1172 *
1173 * @param array $libraries
1174 * A multidimensional array of libraries keyed with machineName first and majorVersion second
1175 * @return array
1176 * A list of libraries that are missing keyed with machineName and holds objects with
1177 * machineName, majorVersion and minorVersion properties
1178 */
1179 private function getMissingLibraries($libraries) {
1180 $missing = array();
1181 foreach ($libraries as $library) {
1182 if (isset($library['preloadedDependencies'])) {
1183 $missing = array_merge($missing, $this->getMissingDependencies($library['preloadedDependencies'], $libraries));
1184 }
1185 if (isset($library['dynamicDependencies'])) {
1186 $missing = array_merge($missing, $this->getMissingDependencies($library['dynamicDependencies'], $libraries));
1187 }
1188 if (isset($library['editorDependencies'])) {
1189 $missing = array_merge($missing, $this->getMissingDependencies($library['editorDependencies'], $libraries));
1190 }
1191 }
1192 return $missing;
1193 }
1194
1195 /**
1196 * Helper function for getMissingLibraries, searches for dependency required libraries in
1197 * the provided list of libraries
1198 *
1199 * @param array $dependencies
1200 * A list of objects with machineName, majorVersion and minorVersion properties
1201 * @param array $libraries
1202 * An array of libraries keyed with machineName
1203 * @return
1204 * A list of libraries that are missing keyed with machineName and holds objects with
1205 * machineName, majorVersion and minorVersion properties
1206 */
1207 private function getMissingDependencies($dependencies, $libraries) {
1208 $missing = array();
1209 foreach ($dependencies as $dependency) {
1210 $libString = H5PCore::libraryToString($dependency);
1211 if (!isset($libraries[$libString])) {
1212 $missing[$libString] = $dependency;
1213 }
1214 }
1215 return $missing;
1216 }
1217
1218 /**
1219 * Figure out if the provided file paths exists
1220 *
1221 * Triggers error messages if files doesn't exist
1222 *
1223 * @param array $files
1224 * List of file paths relative to $tmpDir
1225 * @param string $tmpDir
1226 * Path to the directory where the $files are stored.
1227 * @param string $library
1228 * Name of the library we are processing
1229 * @return boolean
1230 * TRUE if all the files excists
1231 */
1232 private function isExistingFiles($files, $tmpDir, $library) {
1233 foreach ($files as $file) {
64153166
SA
1234 $path = str_replace(array('/', '\\'), '/', $file['path']);
1235 if (!file_exists($tmpDir . '/' . $library . '/' . $path)) {
45899564
AA
1236 $this->h5pF->setErrorMessage($this->h5pF->t('The file "%file" is missing from library: "%name"', array('%file' => $path, '%name' => $library)), 'library-missing-file');
1237 return FALSE;
1238 }
1239 }
1240 return TRUE;
1241 }
1242
1243 /**
1244 * Validates h5p.json and library.json data
1245 *
1246 * Error messages are triggered if the data isn't valid
1247 *
1248 * @param array $h5pData
1249 * h5p data
1250 * @param string $library_name
1251 * Name of the library we are processing
1252 * @param array $required
1253 * Validation pattern for required properties
1254 * @param array $optional
1255 * Validation pattern for optional properties
1256 * @return boolean
1257 * TRUE if the $h5pData is valid
1258 */
1259 private function isValidH5pData($h5pData, $library_name, $required, $optional) {
1260 $valid = $this->isValidRequiredH5pData($h5pData, $required, $library_name);
1261 $valid = $this->isValidOptionalH5pData($h5pData, $optional, $library_name) && $valid;
1262
1263 // Check the library's required API version of Core.
1264 // If no requirement is set this implicitly means 1.0.
1265 if (isset($h5pData['coreApi']) && !empty($h5pData['coreApi'])) {
1266 if (($h5pData['coreApi']['majorVersion'] > H5PCore::$coreApi['majorVersion']) ||
1267 ( ($h5pData['coreApi']['majorVersion'] == H5PCore::$coreApi['majorVersion']) &&
1268 ($h5pData['coreApi']['minorVersion'] > H5PCore::$coreApi['minorVersion']) )) {
1269
1270 $this->h5pF->setErrorMessage(
1271 $this->h5pF->t('The system was unable to install the <em>%component</em> component from the package, it requires a newer version of the H5P plugin. This site is currently running version %current, whereas the required version is %required or higher. You should consider upgrading and then try again.',
1272 array(
1273 '%component' => (isset($h5pData['title']) ? $h5pData['title'] : $library_name),
1274 '%current' => H5PCore::$coreApi['majorVersion'] . '.' . H5PCore::$coreApi['minorVersion'],
1275 '%required' => $h5pData['coreApi']['majorVersion'] . '.' . $h5pData['coreApi']['minorVersion']
1276 )
1277 ),
1278 'api-version-unsupported'
1279 );
1280
1281 $valid = false;
1282 }
1283 }
1284
1285 return $valid;
1286 }
1287
1288 /**
1289 * Helper function for isValidH5pData
1290 *
1291 * Validates the optional part of the h5pData
1292 *
1293 * Triggers error messages
1294 *
1295 * @param array $h5pData
1296 * h5p data
1297 * @param array $requirements
1298 * Validation pattern
1299 * @param string $library_name
1300 * Name of the library we are processing
1301 * @return boolean
1302 * TRUE if the optional part of the $h5pData is valid
1303 */
1304 private function isValidOptionalH5pData($h5pData, $requirements, $library_name) {
1305 $valid = TRUE;
1306
1307 foreach ($h5pData as $key => $value) {
1308 if (isset($requirements[$key])) {
1309 $valid = $this->isValidRequirement($value, $requirements[$key], $library_name, $key) && $valid;
1310 }
1311 // Else: ignore, a package can have parameters that this library doesn't care about, but that library
1312 // specific implementations does care about...
1313 }
1314
1315 return $valid;
1316 }
1317
1318 /**
1319 * Validate a requirement given as regexp or an array of requirements
1320 *
1321 * @param mixed $h5pData
1322 * The data to be validated
1323 * @param mixed $requirement
1324 * The requirement the data is to be validated against, regexp or array of requirements
1325 * @param string $library_name
1326 * Name of the library we are validating(used in error messages)
1327 * @param string $property_name
1328 * Name of the property we are validating(used in error messages)
1329 * @return boolean
1330 * TRUE if valid, FALSE if invalid
1331 */
1332 private function isValidRequirement($h5pData, $requirement, $library_name, $property_name) {
1333 $valid = TRUE;
1334
1335 if (is_string($requirement)) {
1336 if ($requirement == 'boolean') {
1337 if (!is_bool($h5pData)) {
1338 $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library. Boolean expected.", array('%property' => $property_name, '%library' => $library_name)));
1339 $valid = FALSE;
1340 }
1341 }
1342 else {
1343 // The requirement is a regexp, match it against the data
1344 if (is_string($h5pData) || is_int($h5pData)) {
1345 if (preg_match($requirement, $h5pData) === 0) {
1346 $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1347 $valid = FALSE;
1348 }
1349 }
1350 else {
1351 $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1352 $valid = FALSE;
1353 }
1354 }
1355 }
1356 elseif (is_array($requirement)) {
1357 // We have sub requirements
1358 if (is_array($h5pData)) {
1359 if (is_array(current($h5pData))) {
1360 foreach ($h5pData as $sub_h5pData) {
1361 $valid = $this->isValidRequiredH5pData($sub_h5pData, $requirement, $library_name) && $valid;
1362 }
1363 }
1364 else {
1365 $valid = $this->isValidRequiredH5pData($h5pData, $requirement, $library_name) && $valid;
1366 }
1367 }
1368 else {
1369 $this->h5pF->setErrorMessage($this->h5pF->t("Invalid data provided for %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1370 $valid = FALSE;
1371 }
1372 }
1373 else {
1374 $this->h5pF->setErrorMessage($this->h5pF->t("Can't read the property %property in %library", array('%property' => $property_name, '%library' => $library_name)));
1375 $valid = FALSE;
1376 }
1377 return $valid;
1378 }
1379
1380 /**
1381 * Validates the required h5p data in libraray.json and h5p.json
1382 *
1383 * @param mixed $h5pData
1384 * Data to be validated
1385 * @param array $requirements
1386 * Array with regexp to validate the data against
1387 * @param string $library_name
1388 * Name of the library we are validating (used in error messages)
1389 * @return boolean
1390 * TRUE if all the required data exists and is valid, FALSE otherwise
1391 */
1392 private function isValidRequiredH5pData($h5pData, $requirements, $library_name) {
1393 $valid = TRUE;
1394 foreach ($requirements as $required => $requirement) {
1395 if (is_int($required)) {
1396 // We have an array of allowed options
1397 return $this->isValidH5pDataOptions($h5pData, $requirements, $library_name);
1398 }
1399 if (isset($h5pData[$required])) {
1400 $valid = $this->isValidRequirement($h5pData[$required], $requirement, $library_name, $required) && $valid;
1401 }
1402 else {
1403 $this->h5pF->setErrorMessage($this->h5pF->t('The required property %property is missing from %library', array('%property' => $required, '%library' => $library_name)), 'missing-required-property');
1404 $valid = FALSE;
1405 }
1406 }
1407 return $valid;
1408 }
1409
1410 /**
1411 * Validates h5p data against a set of allowed values(options)
1412 *
1413 * @param array $selected
1414 * The option(s) that has been specified
1415 * @param array $allowed
1416 * The allowed options
1417 * @param string $library_name
1418 * Name of the library we are validating (used in error messages)
1419 * @return boolean
1420 * TRUE if the specified data is valid, FALSE otherwise
1421 */
1422 private function isValidH5pDataOptions($selected, $allowed, $library_name) {
1423 $valid = TRUE;
1424 foreach ($selected as $value) {
1425 if (!in_array($value, $allowed)) {
1426 $this->h5pF->setErrorMessage($this->h5pF->t('Illegal option %option in %library', array('%option' => $value, '%library' => $library_name)), 'illegal-option-in-library');
1427 $valid = FALSE;
1428 }
1429 }
1430 return $valid;
1431 }
1432
1433 /**
1434 * Fetch json data from file
1435 *
1436 * @param string $filePath
1437 * Path to the file holding the json string
1438 * @param boolean $return_as_string
1439 * If true the json data will be decoded in order to validate it, but will be
1440 * returned as string
1441 * @return mixed
1442 * FALSE if the file can't be read or the contents can't be decoded
1443 * string if the $return as string parameter is set
1444 * array otherwise
1445 */
1446 private function getJsonData($filePath, $return_as_string = FALSE) {
1447 $json = file_get_contents($filePath);
1448 if ($json === FALSE) {
1449 return FALSE; // Cannot read from file.
1450 }
1451 $jsonData = json_decode($json, TRUE);
1452 if ($jsonData === NULL) {
1453 return FALSE; // JSON cannot be decoded or the recursion limit has been reached.
1454 }
1455 return $return_as_string ? $json : $jsonData;
1456 }
1457
1458 /**
1459 * Helper function that copies an array
1460 *
1461 * @param array $array
1462 * The array to be copied
1463 * @return array
1464 * Copy of $array. All objects are cloned
1465 */
1466 private function arrayCopy(array $array) {
1467 $result = array();
1468 foreach ($array as $key => $val) {
1469 if (is_array($val)) {
1470 $result[$key] = self::arrayCopy($val);
1471 }
1472 elseif (is_object($val)) {
1473 $result[$key] = clone $val;
1474 }
1475 else {
1476 $result[$key] = $val;
1477 }
1478 }
1479 return $result;
1480 }
1481}
1482
1483/**
1484 * This class is used for saving H5P files
1485 */
1486class H5PStorage {
1487
1488 public $h5pF;
1489 public $h5pC;
1490
1491 public $contentId = NULL; // Quick fix so WP can get ID of new content.
1492
1493 /**
1494 * Constructor for the H5PStorage
1495 *
1496 * @param H5PFrameworkInterface|object $H5PFramework
1497 * The frameworks implementation of the H5PFrameworkInterface
1498 * @param H5PCore $H5PCore
1499 */
1500 public function __construct(H5PFrameworkInterface $H5PFramework, H5PCore $H5PCore) {
1501 $this->h5pF = $H5PFramework;
1502 $this->h5pC = $H5PCore;
1503 }
1504
1505 /**
1506 * Saves a H5P file
1507 *
1508 * @param null $content
1509 * @param int $contentMainId
1510 * The main id for the content we are saving. This is used if the framework
1511 * we're integrating with uses content id's and version id's
1512 * @param bool $skipContent
1513 * @param array $options
1514 * @return bool TRUE if one or more libraries were updated
1515 * TRUE if one or more libraries were updated
1516 * FALSE otherwise
1517 */
1518 public function savePackage($content = NULL, $contentMainId = NULL, $skipContent = FALSE, $options = array()) {
1519 if ($this->h5pC->mayUpdateLibraries()) {
1520 // Save the libraries we processed during validation
1521 $this->saveLibraries();
1522 }
1523
1524 if (!$skipContent) {
1525 $basePath = $this->h5pF->getUploadedH5pFolderPath();
64153166 1526 $current_path = $basePath . '/' . 'content';
45899564
AA
1527
1528 // Save content
1529 if ($content === NULL) {
1530 $content = array();
1531 }
1532 if (!is_array($content)) {
1533 $content = array('id' => $content);
1534 }
1535
1536 // Find main library version
1537 foreach ($this->h5pC->mainJsonData['preloadedDependencies'] as $dep) {
1538 if ($dep['machineName'] === $this->h5pC->mainJsonData['mainLibrary']) {
1539 $dep['libraryId'] = $this->h5pC->getLibraryId($dep);
1540 $content['library'] = $dep;
1541 break;
1542 }
1543 }
1544
64153166 1545 $content['params'] = file_get_contents($current_path . '/' . 'content.json');
45899564
AA
1546
1547 if (isset($options['disable'])) {
1548 $content['disable'] = $options['disable'];
1549 }
1550 $content['id'] = $this->h5pC->saveContent($content, $contentMainId);
1551 $this->contentId = $content['id'];
1552
1553 try {
1554 // Save content folder contents
1555 $this->h5pC->fs->saveContent($current_path, $content);
1556 }
1557 catch (Exception $e) {
1558 $this->h5pF->setErrorMessage($e->getMessage(), 'save-content-failed');
1559 }
1560
1561 // Remove temp content folder
1562 H5PCore::deleteFileTree($basePath);
1563 }
1564 }
1565
1566 /**
1567 * Helps savePackage.
1568 *
1569 * @return int Number of libraries saved
1570 */
1571 private function saveLibraries() {
1572 // Keep track of the number of libraries that have been saved
1573 $newOnes = 0;
1574 $oldOnes = 0;
1575
1576 // Go through libraries that came with this package
1577 foreach ($this->h5pC->librariesJsonData as $libString => &$library) {
1578 // Find local library identifier
1579 $libraryId = $this->h5pC->getLibraryId($library, $libString);
1580
1581 // Assume new library
1582 $new = TRUE;
1583 if ($libraryId) {
1584 // Found old library
1585 $library['libraryId'] = $libraryId;
1586
1587 if ($this->h5pF->isPatchedLibrary($library)) {
1588 // This is a newer version than ours. Upgrade!
1589 $new = FALSE;
1590 }
1591 else {
1592 $library['saveDependencies'] = FALSE;
1593 // This is an older version, no need to save.
1594 continue;
1595 }
1596 }
1597
1598 // Indicate that the dependencies of this library should be saved.
1599 $library['saveDependencies'] = TRUE;
1600
1601 // Convert metadataSettings values to boolean & json_encode it before saving
1602 $library['metadataSettings'] = isset($library['metadataSettings']) ?
1603 H5PMetadata::boolifyAndEncodeSettings($library['metadataSettings']) :
1604 NULL;
1605
1606 $this->h5pF->saveLibraryData($library, $new);
1607
1608 // Save library folder
1609 $this->h5pC->fs->saveLibrary($library);
1610
1611 // Remove cached assets that uses this library
1612 if ($this->h5pC->aggregateAssets && isset($library['libraryId'])) {
1613 $removedKeys = $this->h5pF->deleteCachedAssets($library['libraryId']);
1614 $this->h5pC->fs->deleteCachedAssets($removedKeys);
1615 }
1616
1617 // Remove tmp folder
1618 H5PCore::deleteFileTree($library['uploadDirectory']);
1619
1620 if ($new) {
1621 $newOnes++;
1622 }
1623 else {
1624 $oldOnes++;
1625 }
1626 }
1627
1628 // Go through the libraries again to save dependencies.
1629 $library_ids = [];
1630 foreach ($this->h5pC->librariesJsonData as &$library) {
1631 if (!$library['saveDependencies']) {
1632 continue;
1633 }
1634
1635 // TODO: Should the table be locked for this operation?
1636
1637 // Remove any old dependencies
1638 $this->h5pF->deleteLibraryDependencies($library['libraryId']);
1639
1640 // Insert the different new ones
1641 if (isset($library['preloadedDependencies'])) {
1642 $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['preloadedDependencies'], 'preloaded');
1643 }
1644 if (isset($library['dynamicDependencies'])) {
1645 $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['dynamicDependencies'], 'dynamic');
1646 }
1647 if (isset($library['editorDependencies'])) {
1648 $this->h5pF->saveLibraryDependencies($library['libraryId'], $library['editorDependencies'], 'editor');
1649 }
1650
1651 $library_ids[] = $library['libraryId'];
1652 }
1653
1654 // Make sure libraries dependencies, parameter filtering and export files gets regenerated for all content who uses these libraries.
1655 if (!empty($library_ids)) {
1656 $this->h5pF->clearFilteredParameters($library_ids);
1657 }
1658
1659 // Tell the user what we've done.
1660 if ($newOnes && $oldOnes) {
1661 if ($newOnes === 1) {
1662 if ($oldOnes === 1) {
1663 // Singular Singular
1664 $message = $this->h5pF->t('Added %new new H5P library and updated %old old one.', array('%new' => $newOnes, '%old' => $oldOnes));
1665 }
1666 else {
1667 // Singular Plural
1668 $message = $this->h5pF->t('Added %new new H5P library and updated %old old ones.', array('%new' => $newOnes, '%old' => $oldOnes));
1669 }
1670 }
1671 else {
1672 // Plural
1673 if ($oldOnes === 1) {
1674 // Plural Singular
1675 $message = $this->h5pF->t('Added %new new H5P libraries and updated %old old one.', array('%new' => $newOnes, '%old' => $oldOnes));
1676 }
1677 else {
1678 // Plural Plural
1679 $message = $this->h5pF->t('Added %new new H5P libraries and updated %old old ones.', array('%new' => $newOnes, '%old' => $oldOnes));
1680 }
1681 }
1682 }
1683 elseif ($newOnes) {
1684 if ($newOnes === 1) {
1685 // Singular
1686 $message = $this->h5pF->t('Added %new new H5P library.', array('%new' => $newOnes));
1687 }
1688 else {
1689 // Plural
1690 $message = $this->h5pF->t('Added %new new H5P libraries.', array('%new' => $newOnes));
1691 }
1692 }
1693 elseif ($oldOnes) {
1694 if ($oldOnes === 1) {
1695 // Singular
1696 $message = $this->h5pF->t('Updated %old H5P library.', array('%old' => $oldOnes));
1697 }
1698 else {
1699 // Plural
1700 $message = $this->h5pF->t('Updated %old H5P libraries.', array('%old' => $oldOnes));
1701 }
1702 }
1703
1704 if (isset($message)) {
1705 $this->h5pF->setInfoMessage($message);
1706 }
1707 }
1708
1709 /**
1710 * Delete an H5P package
1711 *
1712 * @param $content
1713 */
1714 public function deletePackage($content) {
1715 $this->h5pC->fs->deleteContent($content);
1716 $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p');
1717 $this->h5pF->deleteContentData($content['id']);
1718 }
1719
1720 /**
1721 * Copy/clone an H5P package
1722 *
1723 * May for instance be used if the content is being revisioned without
1724 * uploading a new H5P package
1725 *
1726 * @param int $contentId
1727 * The new content id
1728 * @param int $copyFromId
1729 * The content id of the content that should be cloned
1730 * @param int $contentMainId
1731 * The main id of the new content (used in frameworks that support revisioning)
1732 */
1733 public function copyPackage($contentId, $copyFromId, $contentMainId = NULL) {
1734 $this->h5pC->fs->cloneContent($copyFromId, $contentId);
1735 $this->h5pF->copyLibraryUsage($contentId, $copyFromId, $contentMainId);
1736 }
1737}
1738
1739/**
1740* This class is used for exporting zips
1741*/
1742Class H5PExport {
1743 public $h5pF;
1744 public $h5pC;
1745
1746 /**
1747 * Constructor for the H5PExport
1748 *
1749 * @param H5PFrameworkInterface|object $H5PFramework
1750 * The frameworks implementation of the H5PFrameworkInterface
1751 * @param H5PCore $H5PCore
1752 * Reference to an instance of H5PCore
1753 */
1754 public function __construct(H5PFrameworkInterface $H5PFramework, H5PCore $H5PCore) {
1755 $this->h5pF = $H5PFramework;
1756 $this->h5pC = $H5PCore;
1757 }
1758
1759 /**
1760 * Reverts the replace pattern used by the text editor
1761 *
1762 * @param string $value
1763 * @return string
1764 */
1765 private static function revertH5PEditorTextEscape($value) {
1766 return str_replace('&lt;', '<', str_replace('&gt;', '>', str_replace('&#039;', "'", str_replace('&quot;', '"', $value))));
1767 }
1768
1769 /**
1770 * Return path to h5p package.
1771 *
1772 * Creates package if not already created
1773 *
1774 * @param array $content
1775 * @return string
1776 */
1777 public function createExportFile($content) {
1778
1779 // Get path to temporary folder, where export will be contained
1780 $tmpPath = $this->h5pC->fs->getTmpPath();
1781 mkdir($tmpPath, 0777, true);
1782
1783 try {
1784 // Create content folder and populate with files
1785 $this->h5pC->fs->exportContent($content['id'], "{$tmpPath}/content");
1786 }
1787 catch (Exception $e) {
1788 $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()), 'failed-creating-export-file');
1789 H5PCore::deleteFileTree($tmpPath);
1790 return FALSE;
1791 }
1792
1793 // Update content.json with content from database
1794 file_put_contents("{$tmpPath}/content/content.json", $content['filtered']);
1795
1796 // Make embedType into an array
1797 $embedTypes = explode(', ', $content['embedType']);
1798
1799 // Build h5p.json, the en-/de-coding will ensure proper escaping
1800 $h5pJson = array (
1801 'title' => self::revertH5PEditorTextEscape($content['title']),
1802 'language' => (isset($content['language']) && strlen(trim($content['language'])) !== 0) ? $content['language'] : 'und',
1803 'mainLibrary' => $content['library']['name'],
1804 'embedTypes' => $embedTypes
1805 );
1806
1807 foreach(array('authors', 'source', 'license', 'licenseVersion', 'licenseExtras' ,'yearFrom', 'yearTo', 'changes', 'authorComments', 'defaultLanguage') as $field) {
1808 if (isset($content['metadata'][$field]) && $content['metadata'][$field] !== '') {
1809 if (($field !== 'authors' && $field !== 'changes') || (count($content['metadata'][$field]) > 0)) {
1810 $h5pJson[$field] = json_decode(json_encode($content['metadata'][$field], TRUE));
1811 }
1812 }
1813 }
1814
1815 // Remove all values that are not set
1816 foreach ($h5pJson as $key => $value) {
1817 if (!isset($value)) {
1818 unset($h5pJson[$key]);
1819 }
1820 }
1821
1822 // Add dependencies to h5p
1823 foreach ($content['dependencies'] as $dependency) {
1824 $library = $dependency['library'];
1825
1826 try {
1827 $exportFolder = NULL;
1828
1829 // Determine path of export library
1830 if (isset($this->h5pC) && isset($this->h5pC->h5pD)) {
1831
1832 // Tries to find library in development folder
1833 $isDevLibrary = $this->h5pC->h5pD->getLibrary(
1834 $library['machineName'],
1835 $library['majorVersion'],
1836 $library['minorVersion']
1837 );
1838
1839 if ($isDevLibrary !== NULL && isset($library['path'])) {
1840 $exportFolder = "/" . $library['path'];
1841 }
1842 }
1843
1844 // Export required libraries
1845 $this->h5pC->fs->exportLibrary($library, $tmpPath, $exportFolder);
1846 }
1847 catch (Exception $e) {
1848 $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()), 'failed-creating-export-file');
1849 H5PCore::deleteFileTree($tmpPath);
1850 return FALSE;
1851 }
1852
1853 // Do not add editor dependencies to h5p json.
1854 if ($dependency['type'] === 'editor') {
1855 continue;
1856 }
1857
1858 // Add to h5p.json dependencies
1859 $h5pJson[$dependency['type'] . 'Dependencies'][] = array(
1860 'machineName' => $library['machineName'],
1861 'majorVersion' => $library['majorVersion'],
1862 'minorVersion' => $library['minorVersion']
1863 );
1864 }
1865
1866 // Save h5p.json
1867 $results = print_r(json_encode($h5pJson), true);
1868 file_put_contents("{$tmpPath}/h5p.json", $results);
1869
1870 // Get a complete file list from our tmp dir
1871 $files = array();
1872 self::populateFileList($tmpPath, $files);
1873
1874 // Get path to temporary export target file
1875 $tmpFile = $this->h5pC->fs->getTmpPath();
1876
1877 // Create new zip instance.
1878 $zip = new ZipArchive();
1879 $zip->open($tmpFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
1880
1881 // Add all the files from the tmp dir.
1882 foreach ($files as $file) {
1883 // Please note that the zip format has no concept of folders, we must
1884 // use forward slashes to separate our directories.
1885 if (file_exists(realpath($file->absolutePath))) {
1886 $zip->addFile(realpath($file->absolutePath), $file->relativePath);
1887 }
1888 }
1889
1890 // Close zip and remove tmp dir
1891 $zip->close();
1892 H5PCore::deleteFileTree($tmpPath);
1893
1894 $filename = $content['slug'] . '-' . $content['id'] . '.h5p';
1895 try {
1896 // Save export
1897 $this->h5pC->fs->saveExport($tmpFile, $filename);
1898 }
1899 catch (Exception $e) {
1900 $this->h5pF->setErrorMessage($this->h5pF->t($e->getMessage()), 'failed-creating-export-file');
1901 return false;
1902 }
1903
1904 unlink($tmpFile);
1905 $this->h5pF->afterExportCreated($content, $filename);
1906
1907 return true;
1908 }
1909
1910 /**
1911 * Recursive function the will add the files of the given directory to the
1912 * given files list. All files are objects with an absolute path and
1913 * a relative path. The relative path is forward slashes only! Great for
1914 * use in zip files and URLs.
1915 *
1916 * @param string $dir path
1917 * @param array $files list
1918 * @param string $relative prefix. Optional
1919 */
1920 private static function populateFileList($dir, &$files, $relative = '') {
1921 $strip = strlen($dir) + 1;
64153166 1922 $contents = glob($dir . '/' . '*');
45899564
AA
1923 if (!empty($contents)) {
1924 foreach ($contents as $file) {
1925 $rel = $relative . substr($file, $strip);
1926 if (is_dir($file)) {
1927 self::populateFileList($file, $files, $rel . '/');
1928 }
1929 else {
1930 $files[] = (object) array(
1931 'absolutePath' => $file,
1932 'relativePath' => $rel
1933 );
1934 }
1935 }
1936 }
1937 }
1938
1939 /**
1940 * Delete .h5p file
1941 *
1942 * @param array $content object
1943 */
1944 public function deleteExport($content) {
1945 $this->h5pC->fs->deleteExport(($content['slug'] ? $content['slug'] . '-' : '') . $content['id'] . '.h5p');
1946 }
1947
1948 /**
1949 * Add editor libraries to the list of libraries
1950 *
1951 * These are not supposed to go into h5p.json, but must be included with the rest
1952 * of the libraries
1953 *
1954 * TODO This is a private function that is not currently being used
1955 *
1956 * @param array $libraries
1957 * List of libraries keyed by machineName
1958 * @param array $editorLibraries
1959 * List of libraries keyed by machineName
1960 * @return array List of libraries keyed by machineName
1961 */
1962 private function addEditorLibraries($libraries, $editorLibraries) {
1963 foreach ($editorLibraries as $editorLibrary) {
1964 $libraries[$editorLibrary['machineName']] = $editorLibrary;
1965 }
1966 return $libraries;
1967 }
1968}
1969
1970abstract class H5PPermission {
1971 const DOWNLOAD_H5P = 0;
1972 const EMBED_H5P = 1;
1973 const CREATE_RESTRICTED = 2;
1974 const UPDATE_LIBRARIES = 3;
1975 const INSTALL_RECOMMENDED = 4;
1976 const COPY_H5P = 8;
1977}
1978
1979abstract class H5PDisplayOptionBehaviour {
1980 const NEVER_SHOW = 0;
1981 const CONTROLLED_BY_AUTHOR_DEFAULT_ON = 1;
1982 const CONTROLLED_BY_AUTHOR_DEFAULT_OFF = 2;
1983 const ALWAYS_SHOW = 3;
1984 const CONTROLLED_BY_PERMISSIONS = 4;
1985}
1986
1987abstract class H5PHubEndpoints {
1988 const CONTENT_TYPES = 'api.h5p.org/v1/content-types/';
1989 const SITES = 'api.h5p.org/v1/sites';
1990
1991 public static function createURL($endpoint) {
1992 $protocol = (extension_loaded('openssl') ? 'https' : 'http');
1993 return "{$protocol}://{$endpoint}";
1994 }
1995}
1996
1997/**
1998 * Functions and storage shared by the other H5P classes
1999 */
2000class H5PCore {
2001
2002 public static $coreApi = array(
2003 'majorVersion' => 1,
64153166 2004 'minorVersion' => 24
45899564
AA
2005 );
2006 public static $styles = array(
2007 'styles/h5p.css',
2008 'styles/h5p-confirmation-dialog.css',
2009 'styles/h5p-core-button.css'
2010 );
2011 public static $scripts = array(
2012 'js/jquery.js',
2013 'js/h5p.js',
2014 'js/h5p-event-dispatcher.js',
2015 'js/h5p-x-api-event.js',
2016 'js/h5p-x-api.js',
2017 'js/h5p-content-type.js',
2018 'js/h5p-confirmation-dialog.js',
2019 'js/h5p-action-bar.js',
2020 'js/request-queue.js',
2021 );
2022 public static $adminScripts = array(
2023 'js/jquery.js',
2024 'js/h5p-utils.js',
2025 );
2026
2027 public static $defaultContentWhitelist = 'json png jpg jpeg gif bmp tif tiff svg eot ttf woff woff2 otf webm mp4 ogg mp3 m4a wav txt pdf rtf doc docx xls xlsx ppt pptx odt ods odp xml csv diff patch swf md textile vtt webvtt';
2028 public static $defaultLibraryWhitelistExtras = 'js css';
2029
2030 public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $h5pD, $disableFileCheck;
2031 const SECONDS_IN_WEEK = 604800;
2032
2033 private $exportEnabled;
2034
2035 // Disable flags
2036 const DISABLE_NONE = 0;
2037 const DISABLE_FRAME = 1;
2038 const DISABLE_DOWNLOAD = 2;
2039 const DISABLE_EMBED = 4;
2040 const DISABLE_COPYRIGHT = 8;
2041 const DISABLE_ABOUT = 16;
2042
2043 const DISPLAY_OPTION_FRAME = 'frame';
2044 const DISPLAY_OPTION_DOWNLOAD = 'export';
2045 const DISPLAY_OPTION_EMBED = 'embed';
2046 const DISPLAY_OPTION_COPYRIGHT = 'copyright';
2047 const DISPLAY_OPTION_ABOUT = 'icon';
2048 const DISPLAY_OPTION_COPY = 'copy';
2049
2050 // Map flags to string
2051 public static $disable = array(
2052 self::DISABLE_FRAME => self::DISPLAY_OPTION_FRAME,
2053 self::DISABLE_DOWNLOAD => self::DISPLAY_OPTION_DOWNLOAD,
2054 self::DISABLE_EMBED => self::DISPLAY_OPTION_EMBED,
2055 self::DISABLE_COPYRIGHT => self::DISPLAY_OPTION_COPYRIGHT
2056 );
2057
2058 /**
2059 * Constructor for the H5PCore
2060 *
2061 * @param H5PFrameworkInterface $H5PFramework
2062 * The frameworks implementation of the H5PFrameworkInterface
2063 * @param string|\H5PFileStorage $path H5P file storage directory or class.
2064 * @param string $url To file storage directory.
2065 * @param string $language code. Defaults to english.
2066 * @param boolean $export enabled?
2067 */
2068 public function __construct(H5PFrameworkInterface $H5PFramework, $path, $url, $language = 'en', $export = FALSE) {
2069 $this->h5pF = $H5PFramework;
2070
2071 $this->fs = ($path instanceof \H5PFileStorage ? $path : new \H5PDefaultStorage($path));
2072
2073 $this->url = $url;
2074 $this->exportEnabled = $export;
2075 $this->development_mode = H5PDevelopment::MODE_NONE;
2076
2077 $this->aggregateAssets = FALSE; // Off by default.. for now
2078
2079 $this->detectSiteType();
2080 $this->fullPluginPath = preg_replace('/\/[^\/]+[\/]?$/', '' , dirname(__FILE__));
2081
2082 // Standard regex for converting copied files paths
2083 $this->relativePathRegExp = '/^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/';
2084 }
2085
2086 /**
2087 * Save content and clear cache.
2088 *
2089 * @param array $content
2090 * @param null|int $contentMainId
2091 * @return int Content ID
2092 */
2093 public function saveContent($content, $contentMainId = NULL) {
2094 if (isset($content['id'])) {
2095 $this->h5pF->updateContent($content, $contentMainId);
2096 }
2097 else {
2098 $content['id'] = $this->h5pF->insertContent($content, $contentMainId);
2099 }
2100
2101 // Some user data for content has to be reset when the content changes.
2102 $this->h5pF->resetContentUserData($contentMainId ? $contentMainId : $content['id']);
2103
2104 return $content['id'];
2105 }
2106
2107 /**
2108 * Load content.
2109 *
2110 * @param int $id for content.
2111 * @return object
2112 */
2113 public function loadContent($id) {
2114 $content = $this->h5pF->loadContent($id);
2115
2116 if ($content !== NULL) {
2117 // Validate main content's metadata
2118 $validator = new H5PContentValidator($this->h5pF, $this);
2119 $content['metadata'] = $validator->validateMetadata($content['metadata']);
2120
2121 $content['library'] = array(
2122 'id' => $content['libraryId'],
2123 'name' => $content['libraryName'],
2124 'majorVersion' => $content['libraryMajorVersion'],
2125 'minorVersion' => $content['libraryMinorVersion'],
2126 'embedTypes' => $content['libraryEmbedTypes'],
2127 'fullscreen' => $content['libraryFullscreen'],
2128 );
2129 unset($content['libraryId'], $content['libraryName'], $content['libraryEmbedTypes'], $content['libraryFullscreen']);
2130
2131// // TODO: Move to filterParameters?
2132// if (isset($this->h5pD)) {
2133// // TODO: Remove Drupal specific stuff
2134// $json_content_path = file_create_path(file_directory_path() . '/' . variable_get('h5p_default_path', 'h5p') . '/content/' . $id . '/content.json');
2135// if (file_exists($json_content_path) === TRUE) {
2136// $json_content = file_get_contents($json_content_path);
2137// if (json_decode($json_content, TRUE) !== FALSE) {
2138// drupal_set_message(t('Invalid json in json content'), 'warning');
2139// }
2140// $content['params'] = $json_content;
2141// }
2142// }
2143 }
2144
2145 return $content;
2146 }
2147
2148 /**
2149 * Filter content run parameters, rebuild content dependency cache and export file.
2150 *
2151 * @param Object|array $content
2152 * @return Object NULL on failure.
2153 */
2154 public function filterParameters(&$content) {
2155 if (!empty($content['filtered']) &&
2156 (!$this->exportEnabled ||
2157 ($content['slug'] &&
2158 $this->fs->hasExport($content['slug'] . '-' . $content['id'] . '.h5p')))) {
2159 return $content['filtered'];
2160 }
2161
2162 if (!(isset($content['library']) && isset($content['params']))) {
2163 return NULL;
2164 }
2165
2166 // Validate and filter against main library semantics.
2167 $validator = new H5PContentValidator($this->h5pF, $this);
2168 $params = (object) array(
2169 'library' => H5PCore::libraryToString($content['library']),
2170 'params' => json_decode($content['params'])
2171 );
2172 if (!$params->params) {
2173 return NULL;
2174 }
2175 $validator->validateLibrary($params, (object) array('options' => array($params->library)));
2176
2177 // Handle addons:
2178 $addons = $this->h5pF->loadAddons();
2179 foreach ($addons as $addon) {
2180 $add_to = json_decode($addon['addTo']);
2181
2182 if (isset($add_to->content->types)) {
2183 foreach($add_to->content->types as $type) {
2184
2185 if (isset($type->text->regex) &&
2186 $this->textAddonMatches($params->params, $type->text->regex)) {
2187 $validator->addon($addon);
2188
2189 // An addon shall only be added once
2190 break;
2191 }
2192 }
2193 }
2194 }
2195
2196 $params = json_encode($params->params);
2197
2198 // Update content dependencies.
2199 $content['dependencies'] = $validator->getDependencies();
2200
2201 // Sometimes the parameters are filtered before content has been created
2202 if ($content['id']) {
2203 $this->h5pF->deleteLibraryUsage($content['id']);
2204 $this->h5pF->saveLibraryUsage($content['id'], $content['dependencies']);
2205
2206 if (!$content['slug']) {
2207 $content['slug'] = $this->generateContentSlug($content);
2208
2209 // Remove old export file
2210 $this->fs->deleteExport($content['id'] . '.h5p');
2211 }
2212
2213 if ($this->exportEnabled) {
2214 // Recreate export file
2215 $exporter = new H5PExport($this->h5pF, $this);
2216 $content['filtered'] = $params;
2217 $exporter->createExportFile($content);
2218 }
2219
2220 // Cache.
2221 $this->h5pF->updateContentFields($content['id'], array(
2222 'filtered' => $params,
2223 'slug' => $content['slug']
2224 ));
2225 }
2226 return $params;
2227 }
2228
2229 /**
2230 * Retrieve a value from a nested mixed array structure.
2231 *
2232 * @param Array $params Array to be looked in.
2233 * @param String $path Supposed path to the value.
2234 * @param String [$delimiter='.'] Property delimiter within the path.
2235 * @return Object|NULL The object found or NULL.
2236 */
2237 private function retrieveValue ($params, $path, $delimiter='.') {
2238 $path = explode($delimiter, $path);
2239
2240 // Property not found
2241 if (!isset($params[$path[0]])) {
2242 return NULL;
2243 }
2244
2245 $first = $params[$path[0]];
2246
2247 // End of path, done
2248 if (sizeof($path) === 1) {
2249 return $first;
2250 }
2251
2252 // We cannot go deeper
2253 if (!is_array($first)) {
2254 return NULL;
2255 }
2256
2257 // Regular Array
2258 if (isset($first[0])) {
2259 foreach($first as $number => $object) {
2260 $found = $this->retrieveValue($object, implode($delimiter, array_slice($path, 1)));
2261 if (isset($found)) {
2262 return $found;
2263 }
2264 }
2265 return NULL;
2266 }
2267
2268 // Associative Array
2269 return $this->retrieveValue($first, implode('.', array_slice($path, 1)));
2270 }
2271
2272 /**
2273 * Determine if params contain any match.
2274 *
2275 * @param {object} params - Parameters.
2276 * @param {string} [pattern] - Regular expression to identify pattern.
2277 * @param {boolean} [found] - Used for recursion.
2278 * @return {boolean} True, if params matches pattern.
2279 */
2280 private function textAddonMatches($params, $pattern, $found = false) {
2281 $type = gettype($params);
2282 if ($type === 'string') {
2283 if (preg_match($pattern, $params) === 1) {
2284 return true;
2285 }
2286 }
2287 elseif ($type === 'array' || $type === 'object') {
2288 foreach ($params as $value) {
2289 $found = $this->textAddonMatches($value, $pattern, $found);
2290 if ($found === true) {
2291 return true;
2292 }
2293 }
2294 }
2295 return false;
2296 }
2297
2298 /**
2299 * Generate content slug
2300 *
2301 * @param array $content object
2302 * @return string unique content slug
2303 */
2304 private function generateContentSlug($content) {
2305 $slug = H5PCore::slugify($content['title']);
2306
2307 $available = NULL;
2308 while (!$available) {
2309 if ($available === FALSE) {
2310 // If not available, add number suffix.
2311 $matches = array();
2312 if (preg_match('/(.+-)([0-9]+)$/', $slug, $matches)) {
2313 $slug = $matches[1] . (intval($matches[2]) + 1);
2314 }
2315 else {
2316 $slug .= '-2';
2317 }
2318 }
2319 $available = $this->h5pF->isContentSlugAvailable($slug);
2320 }
2321
2322 return $slug;
2323 }
2324
2325 /**
2326 * Find the files required for this content to work.
2327 *
2328 * @param int $id for content.
2329 * @param null $type
2330 * @return array
2331 */
2332 public function loadContentDependencies($id, $type = NULL) {
2333 $dependencies = $this->h5pF->loadContentDependencies($id, $type);
2334
2335 if (isset($this->h5pD)) {
2336 $developmentLibraries = $this->h5pD->getLibraries();
2337
2338 foreach ($dependencies as $key => $dependency) {
2339 $libraryString = H5PCore::libraryToString($dependency);
2340 if (isset($developmentLibraries[$libraryString])) {
2341 $developmentLibraries[$libraryString]['dependencyType'] = $dependencies[$key]['dependencyType'];
2342 $dependencies[$key] = $developmentLibraries[$libraryString];
2343 }
2344 }
2345 }
2346
2347 return $dependencies;
2348 }
2349
2350 /**
2351 * Get all dependency assets of the given type
2352 *
2353 * @param array $dependency
2354 * @param string $type
2355 * @param array $assets
2356 * @param string $prefix Optional. Make paths relative to another dir.
2357 */
2358 private function getDependencyAssets($dependency, $type, &$assets, $prefix = '') {
2359 // Check if dependency has any files of this type
2360 if (empty($dependency[$type]) || $dependency[$type][0] === '') {
2361 return;
2362 }
2363
2364 // Check if we should skip CSS.
2365 if ($type === 'preloadedCss' && (isset($dependency['dropCss']) && $dependency['dropCss'] === '1')) {
2366 return;
2367 }
2368 foreach ($dependency[$type] as $file) {
2369 $assets[] = (object) array(
2370 'path' => $prefix . '/' . $dependency['path'] . '/' . trim(is_array($file) ? $file['path'] : $file),
2371 'version' => $dependency['version']
2372 );
2373 }
2374 }
2375
2376 /**
2377 * Combines path with cache buster / version.
2378 *
2379 * @param array $assets
2380 * @return array
2381 */
2382 public function getAssetsUrls($assets) {
2383 $urls = array();
2384
2385 foreach ($assets as $asset) {
2386 $url = $asset->path;
2387
2388 // Add URL prefix if not external
2389 if (strpos($asset->path, '://') === FALSE) {
2390 $url = $this->url . $url;
2391 }
2392
2393 // Add version/cache buster if set
2394 if (isset($asset->version)) {
2395 $url .= $asset->version;
2396 }
2397
2398 $urls[] = $url;
2399 }
2400
2401 return $urls;
2402 }
2403
2404 /**
2405 * Return file paths for all dependencies files.
2406 *
2407 * @param array $dependencies
2408 * @param string $prefix Optional. Make paths relative to another dir.
2409 * @return array files.
2410 */
2411 public function getDependenciesFiles($dependencies, $prefix = '') {
2412 // Build files list for assets
2413 $files = array(
2414 'scripts' => array(),
2415 'styles' => array()
2416 );
2417
2418 $key = null;
2419
2420 // Avoid caching empty files
2421 if (empty($dependencies)) {
2422 return $files;
2423 }
2424
2425 if ($this->aggregateAssets) {
2426 // Get aggregated files for assets
2427 $key = self::getDependenciesHash($dependencies);
2428
2429 $cachedAssets = $this->fs->getCachedAssets($key);
2430 if ($cachedAssets !== NULL) {
2431 return array_merge($files, $cachedAssets); // Using cached assets
2432 }
2433 }
2434
2435 // Using content dependencies
2436 foreach ($dependencies as $dependency) {
2437 if (isset($dependency['path']) === FALSE) {
5da7a7fc 2438 $dependency['path'] = $this->getDependencyPath($dependency);
45899564
AA
2439 $dependency['preloadedJs'] = explode(',', $dependency['preloadedJs']);
2440 $dependency['preloadedCss'] = explode(',', $dependency['preloadedCss']);
2441 }
2442 $dependency['version'] = "?ver={$dependency['majorVersion']}.{$dependency['minorVersion']}.{$dependency['patchVersion']}";
2443 $this->getDependencyAssets($dependency, 'preloadedJs', $files['scripts'], $prefix);
2444 $this->getDependencyAssets($dependency, 'preloadedCss', $files['styles'], $prefix);
2445 }
2446
2447 if ($this->aggregateAssets) {
2448 // Aggregate and store assets
2449 $this->fs->cacheAssets($files, $key);
2450
2451 // Keep track of which libraries have been cached in case they are updated
2452 $this->h5pF->saveCachedAssets($key, $dependencies);
2453 }
2454
2455 return $files;
2456 }
2457
5da7a7fc
SA
2458 /**
2459 * Get the path to the dependency.
2460 *
2461 * @param stdClass $dependency
2462 * @return string
2463 */
2464 protected function getDependencyPath(array $dependency): string {
2465 return H5PCore::libraryToString($dependency, TRUE);
2466 }
2467
45899564
AA
2468 private static function getDependenciesHash(&$dependencies) {
2469 // Build hash of dependencies
2470 $toHash = array();
2471
2472 // Use unique identifier for each library version
2473 foreach ($dependencies as $dep) {
2474 $toHash[] = "{$dep['machineName']}-{$dep['majorVersion']}.{$dep['minorVersion']}.{$dep['patchVersion']}";
2475 }
2476
2477 // Sort in case the same dependencies comes in a different order
2478 sort($toHash);
2479
2480 // Calculate hash sum
2481 return hash('sha1', implode('', $toHash));
2482 }
2483
2484 /**
2485 * Load library semantics.
2486 *
2487 * @param $name
2488 * @param $majorVersion
2489 * @param $minorVersion
2490 * @return string
2491 */
2492 public function loadLibrarySemantics($name, $majorVersion, $minorVersion) {
2493 $semantics = NULL;
2494 if (isset($this->h5pD)) {
2495 // Try to load from dev lib
2496 $semantics = $this->h5pD->getSemantics($name, $majorVersion, $minorVersion);
2497 }
2498
2499 if ($semantics === NULL) {
2500 // Try to load from DB.
2501 $semantics = $this->h5pF->loadLibrarySemantics($name, $majorVersion, $minorVersion);
2502 }
2503
2504 if ($semantics !== NULL) {
2505 $semantics = json_decode($semantics);
2506 $this->h5pF->alterLibrarySemantics($semantics, $name, $majorVersion, $minorVersion);
2507 }
2508
2509 return $semantics;
2510 }
2511
2512 /**
2513 * Load library.
2514 *
2515 * @param $name
2516 * @param $majorVersion
2517 * @param $minorVersion
2518 * @return array or null.
2519 */
2520 public function loadLibrary($name, $majorVersion, $minorVersion) {
2521 $library = NULL;
2522 if (isset($this->h5pD)) {
2523 // Try to load from dev
2524 $library = $this->h5pD->getLibrary($name, $majorVersion, $minorVersion);
2525 if ($library !== NULL) {
2526 $library['semantics'] = $this->h5pD->getSemantics($name, $majorVersion, $minorVersion);
2527 }
2528 }
2529
2530 if ($library === NULL) {
2531 // Try to load from DB.
2532 $library = $this->h5pF->loadLibrary($name, $majorVersion, $minorVersion);
2533 }
2534
2535 return $library;
2536 }
2537
2538 /**
2539 * Deletes a library
2540 *
2541 * @param stdClass $libraryId
2542 */
2543 public function deleteLibrary($libraryId) {
2544 $this->h5pF->deleteLibrary($libraryId);
2545 }
2546
2547 /**
2548 * Recursive. Goes through the dependency tree for the given library and
2549 * adds all the dependencies to the given array in a flat format.
2550 *
2551 * @param $dependencies
2552 * @param array $library To find all dependencies for.
2553 * @param int $nextWeight An integer determining the order of the libraries
2554 * when they are loaded
2555 * @param bool $editor Used internally to force all preloaded sub dependencies
2556 * of an editor dependency to be editor dependencies.
2557 * @return int
2558 */
2559 public function findLibraryDependencies(&$dependencies, $library, $nextWeight = 1, $editor = FALSE) {
2560 foreach (array('dynamic', 'preloaded', 'editor') as $type) {
2561 $property = $type . 'Dependencies';
2562 if (!isset($library[$property])) {
2563 continue; // Skip, no such dependencies.
2564 }
2565
2566 if ($type === 'preloaded' && $editor === TRUE) {
2567 // All preloaded dependencies of an editor library is set to editor.
2568 $type = 'editor';
2569 }
2570
2571 foreach ($library[$property] as $dependency) {
2572 $dependencyKey = $type . '-' . $dependency['machineName'];
2573 if (isset($dependencies[$dependencyKey]) === TRUE) {
2574 continue; // Skip, already have this.
2575 }
2576
2577 $dependencyLibrary = $this->loadLibrary($dependency['machineName'], $dependency['majorVersion'], $dependency['minorVersion']);
2578 if ($dependencyLibrary) {
2579 $dependencies[$dependencyKey] = array(
2580 'library' => $dependencyLibrary,
2581 'type' => $type
2582 );
2583 $nextWeight = $this->findLibraryDependencies($dependencies, $dependencyLibrary, $nextWeight, $type === 'editor');
2584 $dependencies[$dependencyKey]['weight'] = $nextWeight++;
2585 }
2586 else {
2587 // This site is missing a dependency!
2588 $this->h5pF->setErrorMessage($this->h5pF->t('Missing dependency @dep required by @lib.', array('@dep' => H5PCore::libraryToString($dependency), '@lib' => H5PCore::libraryToString($library))), 'missing-library-dependency');
2589 }
2590 }
2591 }
2592 return $nextWeight;
2593 }
2594
2595 /**
2596 * Check if a library is of the version we're looking for
2597 *
2598 * Same version means that the majorVersion and minorVersion is the same
2599 *
2600 * @param array $library
2601 * Data from library.json
2602 * @param array $dependency
2603 * Definition of what library we're looking for
2604 * @return boolean
2605 * TRUE if the library is the same version as the dependency
2606 * FALSE otherwise
2607 */
2608 public function isSameVersion($library, $dependency) {
2609 if ($library['machineName'] != $dependency['machineName']) {
2610 return FALSE;
2611 }
2612 if ($library['majorVersion'] != $dependency['majorVersion']) {
2613 return FALSE;
2614 }
2615 if ($library['minorVersion'] != $dependency['minorVersion']) {
2616 return FALSE;
2617 }
2618 return TRUE;
2619 }
2620
2621 /**
2622 * Recursive function for removing directories.
2623 *
2624 * @param string $dir
2625 * Path to the directory we'll be deleting
2626 * @return boolean
2627 * Indicates if the directory existed.
2628 */
2629 public static function deleteFileTree($dir) {
2630 if (!is_dir($dir)) {
2631 return false;
2632 }
2633 if (is_link($dir)) {
2634 // Do not traverse and delete linked content, simply unlink.
2635 unlink($dir);
2636 return;
2637 }
2638 $files = array_diff(scandir($dir), array('.','..'));
2639 foreach ($files as $file) {
2640 $filepath = "$dir/$file";
2641 // Note that links may resolve as directories
2642 if (!is_dir($filepath) || is_link($filepath)) {
2643 // Unlink files and links
2644 unlink($filepath);
2645 }
2646 else {
2647 // Traverse subdir and delete files
2648 self::deleteFileTree($filepath);
2649 }
2650 }
2651 return rmdir($dir);
2652 }
2653
2654 /**
2655 * Writes library data as string on the form {machineName} {majorVersion}.{minorVersion}
2656 *
2657 * @param array $library
2658 * With keys machineName, majorVersion and minorVersion
2659 * @param boolean $folderName
2660 * Use hyphen instead of space in returned string.
2661 * @return string
2662 * On the form {machineName} {majorVersion}.{minorVersion}
2663 */
2664 public static function libraryToString($library, $folderName = FALSE) {
2665 return (isset($library['machineName']) ? $library['machineName'] : $library['name']) . ($folderName ? '-' : ' ') . $library['majorVersion'] . '.' . $library['minorVersion'];
2666 }
2667
2668 /**
2669 * Parses library data from a string on the form {machineName} {majorVersion}.{minorVersion}
2670 *
2671 * @param string $libraryString
2672 * On the form {machineName} {majorVersion}.{minorVersion}
2673 * @return array|FALSE
2674 * With keys machineName, majorVersion and minorVersion.
2675 * Returns FALSE only if string is not parsable in the normal library
2676 * string formats "Lib.Name-x.y" or "Lib.Name x.y"
2677 */
2678 public static function libraryFromString($libraryString) {
2679 $re = '/^([\w0-9\-\.]{1,255})[\-\ ]([0-9]{1,5})\.([0-9]{1,5})$/i';
2680 $matches = array();
2681 $res = preg_match($re, $libraryString, $matches);
2682 if ($res) {
2683 return array(
2684 'machineName' => $matches[1],
2685 'majorVersion' => $matches[2],
2686 'minorVersion' => $matches[3]
2687 );
2688 }
2689 return FALSE;
2690 }
2691
2692 /**
2693 * Determine the correct embed type to use.
2694 *
2695 * @param $contentEmbedType
2696 * @param $libraryEmbedTypes
2697 * @return string 'div' or 'iframe'.
2698 */
2699 public static function determineEmbedType($contentEmbedType, $libraryEmbedTypes) {
2700 // Detect content embed type
2701 $embedType = strpos(strtolower($contentEmbedType), 'div') !== FALSE ? 'div' : 'iframe';
2702
2703 if ($libraryEmbedTypes !== NULL && $libraryEmbedTypes !== '') {
2704 // Check that embed type is available for library
2705 $embedTypes = strtolower($libraryEmbedTypes);
2706 if (strpos($embedTypes, $embedType) === FALSE) {
2707 // Not available, pick default.
2708 $embedType = strpos($embedTypes, 'div') !== FALSE ? 'div' : 'iframe';
2709 }
2710 }
2711
2712 return $embedType;
2713 }
2714
2715 /**
2716 * Get the absolute version for the library as a human readable string.
2717 *
2718 * @param object $library
2719 * @return string
2720 */
2721 public static function libraryVersion($library) {
2722 return $library->major_version . '.' . $library->minor_version . '.' . $library->patch_version;
2723 }
2724
2725 /**
2726 * Determine which versions content with the given library can be upgraded to.
2727 *
2728 * @param object $library
2729 * @param array $versions
2730 * @return array
2731 */
2732 public function getUpgrades($library, $versions) {
2733 $upgrades = array();
2734
2735 foreach ($versions as $upgrade) {
2736 if ($upgrade->major_version > $library->major_version || $upgrade->major_version === $library->major_version && $upgrade->minor_version > $library->minor_version) {
2737 $upgrades[$upgrade->id] = H5PCore::libraryVersion($upgrade);
2738 }
2739 }
2740
2741 return $upgrades;
2742 }
2743
2744 /**
2745 * Converts all the properties of the given object or array from
2746 * snake_case to camelCase. Useful after fetching data from the database.
2747 *
2748 * Note that some databases does not support camelCase.
2749 *
2750 * @param mixed $arr input
2751 * @param boolean $obj return object
2752 * @return mixed object or array
2753 */
2754 public static function snakeToCamel($arr, $obj = false) {
2755 $newArr = array();
2756
2757 foreach ($arr as $key => $val) {
2758 $next = -1;
2759 while (($next = strpos($key, '_', $next + 1)) !== FALSE) {
712a321d 2760 $key = substr_replace($key, strtoupper($key[$next + 1]), $next, 2);
45899564
AA
2761 }
2762
2763 $newArr[$key] = $val;
2764 }
2765
2766 return $obj ? (object) $newArr : $newArr;
2767 }
2768
2769 /**
2770 * Detects if the site was accessed from localhost,
2771 * through a local network or from the internet.
2772 */
2773 public function detectSiteType() {
2774 $type = $this->h5pF->getOption('site_type', 'local');
2775
2776 // Determine remote/visitor origin
2777 if ($type === 'network' ||
2778 ($type === 'local' &&
2779 isset($_SERVER['REMOTE_ADDR']) &&
2780 !preg_match('/^localhost$|^127(?:\.[0-9]+){0,2}\.[0-9]+$|^(?:0*\:)*?:?0*1$/i', $_SERVER['REMOTE_ADDR']))) {
2781 if (isset($_SERVER['REMOTE_ADDR']) && filter_var($_SERVER['REMOTE_ADDR'], FILTER_VALIDATE_IP, FILTER_FLAG_NO_PRIV_RANGE)) {
2782 // Internet
2783 $this->h5pF->setOption('site_type', 'internet');
2784 }
2785 elseif ($type === 'local') {
2786 // Local network
2787 $this->h5pF->setOption('site_type', 'network');
2788 }
2789 }
2790 }
2791
2792 /**
2793 * Get a list of installed libraries, different minor versions will
2794 * return separate entries.
2795 *
2796 * @return array
2797 * A distinct array of installed libraries
2798 */
2799 public function getLibrariesInstalled() {
2800 $librariesInstalled = array();
2801 $libs = $this->h5pF->loadLibraries();
2802
2803 foreach($libs as $libName => $library) {
2804 foreach($library as $libVersion) {
2805 $librariesInstalled[$libName.' '.$libVersion->major_version.'.'.$libVersion->minor_version] = $libVersion->patch_version;
2806 }
2807 }
2808
2809 return $librariesInstalled;
2810 }
2811
2812 /**
2813 * Easy way to combine similar data sets.
2814 *
2815 * @param array $inputs Multiple arrays with data
2816 * @return array
2817 */
2818 public function combineArrayValues($inputs) {
2819 $results = array();
2820 foreach ($inputs as $index => $values) {
2821 foreach ($values as $key => $value) {
2822 $results[$key][$index] = $value;
2823 }
2824 }
2825 return $results;
2826 }
2827
2828 /**
2829 * Communicate with H5P.org and get content type cache. Each platform
2830 * implementation is responsible for invoking this, eg using cron
2831 *
2832 * @param bool $fetchingDisabled
2833 *
2834 * @return bool|object Returns endpoint data if found, otherwise FALSE
2835 */
2836 public function fetchLibrariesMetadata($fetchingDisabled = FALSE) {
2837 // Gather data
2838 $uuid = $this->h5pF->getOption('site_uuid', '');
2839 $platform = $this->h5pF->getPlatformInfo();
2840 $registrationData = array(
2841 'uuid' => $uuid,
2842 'platform_name' => $platform['name'],
2843 'platform_version' => $platform['version'],
2844 'h5p_version' => $platform['h5pVersion'],
2845 'disabled' => $fetchingDisabled ? 1 : 0,
2846 'local_id' => hash('crc32', $this->fullPluginPath),
2847 'type' => $this->h5pF->getOption('site_type', 'local'),
2848 'core_api_version' => H5PCore::$coreApi['majorVersion'] . '.' .
2849 H5PCore::$coreApi['minorVersion']
2850 );
2851
2852 // Register site if it is not registered
2853 if (empty($uuid)) {
2854 $registration = $this->h5pF->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::SITES), $registrationData);
2855
2856 // Failed retrieving uuid
2857 if (!$registration) {
2858 $errorMessage = $this->h5pF->t('Site could not be registered with the hub. Please contact your site administrator.');
2859 $this->h5pF->setErrorMessage($errorMessage);
2860 $this->h5pF->setErrorMessage(
2861 $this->h5pF->t('The H5P Hub has been disabled until this problem can be resolved. You may still upload libraries through the "H5P Libraries" page.'),
2862 'registration-failed-hub-disabled'
2863 );
2864 return FALSE;
2865 }
2866
2867 // Successfully retrieved new uuid
2868 $json = json_decode($registration);
2869 $registrationData['uuid'] = $json->uuid;
2870 $this->h5pF->setOption('site_uuid', $json->uuid);
2871 $this->h5pF->setInfoMessage(
2872 $this->h5pF->t('Your site was successfully registered with the H5P Hub.')
2873 );
2874 // TODO: Uncomment when key is once again available in H5P Settings
2875// $this->h5pF->setInfoMessage(
2876// $this->h5pF->t('You have been provided a unique key that identifies you with the Hub when receiving new updates. The key is available for viewing in the "H5P Settings" page.')
2877// );
2878 }
2879
2880 if ($this->h5pF->getOption('send_usage_statistics', TRUE)) {
2881 $siteData = array_merge(
2882 $registrationData,
2883 array(
2884 'num_authors' => $this->h5pF->getNumAuthors(),
2885 'libraries' => json_encode($this->combineArrayValues(array(
2886 'patch' => $this->getLibrariesInstalled(),
2887 'content' => $this->h5pF->getLibraryContentCount(),
2888 'loaded' => $this->h5pF->getLibraryStats('library'),
2889 'created' => $this->h5pF->getLibraryStats('content create'),
2890 'createdUpload' => $this->h5pF->getLibraryStats('content create upload'),
2891 'deleted' => $this->h5pF->getLibraryStats('content delete'),
2892 'resultViews' => $this->h5pF->getLibraryStats('results content'),
2893 'shortcodeInserts' => $this->h5pF->getLibraryStats('content shortcode insert')
2894 )))
2895 )
2896 );
2897 }
2898 else {
2899 $siteData = $registrationData;
2900 }
2901
2902 $result = $this->updateContentTypeCache($siteData);
2903
2904 // No data received
2905 if (!$result || empty($result)) {
2906 return FALSE;
2907 }
2908
2909 // Handle libraries metadata
2910 if (isset($result->libraries)) {
2911 foreach ($result->libraries as $library) {
2912 if (isset($library->tutorialUrl) && isset($library->machineName)) {
2913 $this->h5pF->setLibraryTutorialUrl($library->machineNamee, $library->tutorialUrl);
2914 }
2915 }
2916 }
2917
2918 return $result;
2919 }
2920
2921 /**
2922 * Create representation of display options as int
2923 *
2924 * @param array $sources
2925 * @param int $current
2926 * @return int
2927 */
2928 public function getStorableDisplayOptions(&$sources, $current) {
2929 // Download - force setting it if always on or always off
2930 $download = $this->h5pF->getOption(self::DISPLAY_OPTION_DOWNLOAD, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
2931 if ($download == H5PDisplayOptionBehaviour::ALWAYS_SHOW ||
2932 $download == H5PDisplayOptionBehaviour::NEVER_SHOW) {
2933 $sources[self::DISPLAY_OPTION_DOWNLOAD] = ($download == H5PDisplayOptionBehaviour::ALWAYS_SHOW);
2934 }
2935
2936 // Embed - force setting it if always on or always off
2937 $embed = $this->h5pF->getOption(self::DISPLAY_OPTION_EMBED, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
2938 if ($embed == H5PDisplayOptionBehaviour::ALWAYS_SHOW ||
2939 $embed == H5PDisplayOptionBehaviour::NEVER_SHOW) {
2940 $sources[self::DISPLAY_OPTION_EMBED] = ($embed == H5PDisplayOptionBehaviour::ALWAYS_SHOW);
2941 }
2942
2943 foreach (H5PCore::$disable as $bit => $option) {
2944 if (!isset($sources[$option]) || !$sources[$option]) {
2945 $current |= $bit; // Disable
2946 }
2947 else {
2948 $current &= ~$bit; // Enable
2949 }
2950 }
2951 return $current;
2952 }
2953
2954 /**
2955 * Determine display options visibility and value on edit
2956 *
2957 * @param int $disable
2958 * @return array
2959 */
2960 public function getDisplayOptionsForEdit($disable = NULL) {
2961 $display_options = array();
2962
2963 $current_display_options = $disable === NULL ? array() : $this->getDisplayOptionsAsArray($disable);
2964
2965 if ($this->h5pF->getOption(self::DISPLAY_OPTION_FRAME, TRUE)) {
2966 $display_options[self::DISPLAY_OPTION_FRAME] =
2967 isset($current_display_options[self::DISPLAY_OPTION_FRAME]) ?
2968 $current_display_options[self::DISPLAY_OPTION_FRAME] :
2969 TRUE;
2970
2971 // Download
2972 $export = $this->h5pF->getOption(self::DISPLAY_OPTION_DOWNLOAD, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
2973 if ($export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON ||
2974 $export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF) {
2975 $display_options[self::DISPLAY_OPTION_DOWNLOAD] =
2976 isset($current_display_options[self::DISPLAY_OPTION_DOWNLOAD]) ?
2977 $current_display_options[self::DISPLAY_OPTION_DOWNLOAD] :
2978 ($export == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON);
2979 }
2980
2981 // Embed
2982 $embed = $this->h5pF->getOption(self::DISPLAY_OPTION_EMBED, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
2983 if ($embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON ||
2984 $embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF) {
2985 $display_options[self::DISPLAY_OPTION_EMBED] =
2986 isset($current_display_options[self::DISPLAY_OPTION_EMBED]) ?
2987 $current_display_options[self::DISPLAY_OPTION_EMBED] :
2988 ($embed == H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_ON);
2989 }
2990
2991 // Copyright
2992 if ($this->h5pF->getOption(self::DISPLAY_OPTION_COPYRIGHT, TRUE)) {
2993 $display_options[self::DISPLAY_OPTION_COPYRIGHT] =
2994 isset($current_display_options[self::DISPLAY_OPTION_COPYRIGHT]) ?
2995 $current_display_options[self::DISPLAY_OPTION_COPYRIGHT] :
2996 TRUE;
2997 }
2998 }
2999
3000 return $display_options;
3001 }
3002
3003 /**
3004 * Helper function used to figure out embed & download behaviour
3005 *
3006 * @param string $option_name
3007 * @param H5PPermission $permission
3008 * @param int $id
3009 * @param bool &$value
3010 */
3011 private function setDisplayOptionOverrides($option_name, $permission, $id, &$value) {
3012 $behaviour = $this->h5pF->getOption($option_name, H5PDisplayOptionBehaviour::ALWAYS_SHOW);
3013 // If never show globally, force hide
3014 if ($behaviour == H5PDisplayOptionBehaviour::NEVER_SHOW) {
3015 $value = false;
3016 }
3017 elseif ($behaviour == H5PDisplayOptionBehaviour::ALWAYS_SHOW) {
3018 // If always show or permissions say so, force show
3019 $value = true;
3020 }
3021 elseif ($behaviour == H5PDisplayOptionBehaviour::CONTROLLED_BY_PERMISSIONS) {
3022 $value = $this->h5pF->hasPermission($permission, $id);
3023 }
3024 }
3025
3026 /**
3027 * Determine display option visibility when viewing H5P
3028 *
3029 * @param int $display_options
3030 * @param int $id Might be content id or user id.
3031 * Depends on what the platform needs to be able to determine permissions.
3032 * @return array
3033 */
3034 public function getDisplayOptionsForView($disable, $id) {
3035 $display_options = $this->getDisplayOptionsAsArray($disable);
3036
3037 if ($this->h5pF->getOption(self::DISPLAY_OPTION_FRAME, TRUE) == FALSE) {
3038 $display_options[self::DISPLAY_OPTION_FRAME] = false;
3039 }
3040 else {
3041 $this->setDisplayOptionOverrides(self::DISPLAY_OPTION_DOWNLOAD, H5PPermission::DOWNLOAD_H5P, $id, $display_options[self::DISPLAY_OPTION_DOWNLOAD]);
3042 $this->setDisplayOptionOverrides(self::DISPLAY_OPTION_EMBED, H5PPermission::EMBED_H5P, $id, $display_options[self::DISPLAY_OPTION_EMBED]);
3043
3044 if ($this->h5pF->getOption(self::DISPLAY_OPTION_COPYRIGHT, TRUE) == FALSE) {
3045 $display_options[self::DISPLAY_OPTION_COPYRIGHT] = false;
3046 }
3047 }
3048 $display_options[self::DISPLAY_OPTION_COPY] = $this->h5pF->hasPermission(H5PPermission::COPY_H5P, $id);
3049
3050 return $display_options;
3051 }
3052
3053 /**
3054 * Convert display options as single byte to array
3055 *
3056 * @param int $disable
3057 * @return array
3058 */
3059 private function getDisplayOptionsAsArray($disable) {
3060 return array(
3061 self::DISPLAY_OPTION_FRAME => !($disable & H5PCore::DISABLE_FRAME),
3062 self::DISPLAY_OPTION_DOWNLOAD => !($disable & H5PCore::DISABLE_DOWNLOAD),
3063 self::DISPLAY_OPTION_EMBED => !($disable & H5PCore::DISABLE_EMBED),
3064 self::DISPLAY_OPTION_COPYRIGHT => !($disable & H5PCore::DISABLE_COPYRIGHT),
3065 self::DISPLAY_OPTION_ABOUT => !!$this->h5pF->getOption(self::DISPLAY_OPTION_ABOUT, TRUE),
3066 );
3067 }
3068
3069 /**
3070 * Small helper for getting the library's ID.
3071 *
3072 * @param array $library
3073 * @param string [$libString]
3074 * @return int Identifier, or FALSE if non-existent
3075 */
3076 public function getLibraryId($library, $libString = NULL) {
3077 if (!$libString) {
3078 $libString = self::libraryToString($library);
3079 }
3080
3081 if (!isset($libraryIdMap[$libString])) {
3082 $libraryIdMap[$libString] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
3083 }
3084
3085 return $libraryIdMap[$libString];
3086 }
3087
3088 /**
3089 * Convert strings of text into simple kebab case slugs.
3090 * Very useful for readable urls etc.
3091 *
3092 * @param string $input
3093 * @return string
3094 */
3095 public static function slugify($input) {
3096 // Down low
3097 $input = strtolower($input);
3098
3099 // Replace common chars
3100 $input = str_replace(
3101 array('æ', 'ø', 'ö', 'ó', 'ô', 'Ò', 'Õ', 'Ý', 'ý', 'ÿ', 'ā', 'ă', 'ą', 'œ', 'å', 'ä', 'á', 'à', 'â', 'ã', 'ç', 'ć', 'ĉ', 'ċ', 'č', 'é', 'è', 'ê', 'ë', 'í', 'ì', 'î', 'ï', 'ú', 'ñ', 'ü', 'ù', 'û', 'ß', 'ď', 'đ', 'ē', 'ĕ', 'ė', 'ę', 'ě', 'ĝ', 'ğ', 'ġ', 'ģ', 'ĥ', 'ħ', 'ĩ', 'ī', 'ĭ', 'į', 'ı', 'ij', 'ĵ', 'ķ', 'ĺ', 'ļ', 'ľ', 'ŀ', 'ł', 'ń', 'ņ', 'ň', 'ʼn', 'ō', 'ŏ', 'ő', 'ŕ', 'ŗ', 'ř', 'ś', 'ŝ', 'ş', 'š', 'ţ', 'ť', 'ŧ', 'ũ', 'ū', 'ŭ', 'ů', 'ű', 'ų', 'ŵ', 'ŷ', 'ź', 'ż', 'ž', 'ſ', 'ƒ', 'ơ', 'ư', 'ǎ', 'ǐ', 'ǒ', 'ǔ', 'ǖ', 'ǘ', 'ǚ', 'ǜ', 'ǻ', 'ǽ', 'ǿ'),
3102 array('ae', 'oe', 'o', 'o', 'o', 'oe', 'o', 'o', 'y', 'y', 'y', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'a', 'c', 'c', 'c', 'c', 'c', 'e', 'e', 'e', 'e', 'i', 'i', 'i', 'i', 'u', 'n', 'u', 'u', 'u', 'es', 'd', 'd', 'e', 'e', 'e', 'e', 'e', 'g', 'g', 'g', 'g', 'h', 'h', 'i', 'i', 'i', 'i', 'i', 'ij', 'j', 'k', 'l', 'l', 'l', 'l', 'l', 'n', 'n', 'n', 'n', 'o', 'o', 'o', 'r', 'r', 'r', 's', 's', 's', 's', 't', 't', 't', 'u', 'u', 'u', 'u', 'u', 'u', 'w', 'y', 'z', 'z', 'z', 's', 'f', 'o', 'u', 'a', 'i', 'o', 'u', 'u', 'u', 'u', 'u', 'a', 'ae', 'oe'),
3103 $input);
3104
3105 // Replace everything else
3106 $input = preg_replace('/[^a-z0-9]/', '-', $input);
3107
3108 // Prevent double hyphen
3109 $input = preg_replace('/-{2,}/', '-', $input);
3110
3111 // Prevent hyphen in beginning or end
3112 $input = trim($input, '-');
3113
3114 // Prevent to long slug
3115 if (strlen($input) > 91) {
3116 $input = substr($input, 0, 92);
3117 }
3118
3119 // Prevent empty slug
3120 if ($input === '') {
3121 $input = 'interactive';
3122 }
3123
3124 return $input;
3125 }
3126
3127 /**
3128 * Makes it easier to print response when AJAX request succeeds.
3129 *
3130 * @param mixed $data
3131 * @since 1.6.0
3132 */
3133 public static function ajaxSuccess($data = NULL, $only_data = FALSE) {
3134 $response = array(
3135 'success' => TRUE
3136 );
3137 if ($data !== NULL) {
3138 $response['data'] = $data;
3139
3140 // Pass data flatly to support old methods
3141 if ($only_data) {
3142 $response = $data;
3143 }
3144 }
3145 self::printJson($response);
3146 }
3147
3148 /**
3149 * Makes it easier to print response when AJAX request fails.
3150 * Will exit after printing error.
3151 *
3152 * @param string $message A human readable error message
3153 * @param string $error_code An machine readable error code that a client
3154 * should be able to interpret
3155 * @param null|int $status_code Http response code
3156 * @param array [$details=null] Better description of the error and possible which action to take
3157 * @since 1.6.0
3158 */
3159 public static function ajaxError($message = NULL, $error_code = NULL, $status_code = NULL, $details = NULL) {
3160 $response = array(
3161 'success' => FALSE
3162 );
3163 if ($message !== NULL) {
3164 $response['message'] = $message;
3165 }
3166
3167 if ($error_code !== NULL) {
3168 $response['errorCode'] = $error_code;
3169 }
3170
3171 if ($details !== NULL) {
3172 $response['details'] = $details;
3173 }
3174
3175 self::printJson($response, $status_code);
3176 }
3177
3178 /**
3179 * Print JSON headers with UTF-8 charset and json encode response data.
3180 * Makes it easier to respond using JSON.
3181 *
3182 * @param mixed $data
3183 * @param null|int $status_code Http response code
3184 */
3185 private static function printJson($data, $status_code = NULL) {
3186 header('Cache-Control: no-cache');
3187 header('Content-Type: application/json; charset=utf-8');
3188 print json_encode($data);
3189 }
3190
3191 /**
3192 * Get a new H5P security token for the given action.
3193 *
3194 * @param string $action
3195 * @return string token
3196 */
3197 public static function createToken($action) {
3198 // Create and return token
3199 return self::hashToken($action, self::getTimeFactor());
3200 }
3201
3202 /**
3203 * Create a time based number which is unique for each 12 hour.
3204 * @return int
3205 */
3206 private static function getTimeFactor() {
3207 return ceil(time() / (86400 / 2));
3208 }
3209
3210 /**
3211 * Generate a unique hash string based on action, time and token
3212 *
3213 * @param string $action
3214 * @param int $time_factor
3215 * @return string
3216 */
3217 private static function hashToken($action, $time_factor) {
254add38 3218 global $SESSION;
3219
3220 if (!isset($SESSION->h5p_token)) {
45899564
AA
3221 // Create an unique key which is used to create action tokens for this session.
3222 if (function_exists('random_bytes')) {
254add38 3223 $SESSION->h5p_token = base64_encode(random_bytes(15));
45899564
AA
3224 }
3225 else if (function_exists('openssl_random_pseudo_bytes')) {
254add38 3226 $SESSION->h5p_token = base64_encode(openssl_random_pseudo_bytes(15));
45899564
AA
3227 }
3228 else {
254add38 3229 $SESSION->h5p_token = uniqid('', TRUE);
45899564
AA
3230 }
3231 }
3232
3233 // Create hash and return
254add38 3234 return substr(hash('md5', $action . $time_factor . $SESSION->h5p_token), -16, 13);
45899564
AA
3235 }
3236
3237 /**
3238 * Verify if the given token is valid for the given action.
3239 *
3240 * @param string $action
3241 * @param string $token
3242 * @return boolean valid token
3243 */
3244 public static function validToken($action, $token) {
3245 // Get the timefactor
3246 $time_factor = self::getTimeFactor();
3247
3248 // Check token to see if it's valid
3249 return $token === self::hashToken($action, $time_factor) || // Under 12 hours
3250 $token === self::hashToken($action, $time_factor - 1); // Between 12-24 hours
3251 }
3252
3253 /**
3254 * Update content type cache
3255 *
3256 * @param object $postData Data sent to the hub
3257 *
3258 * @return bool|object Returns endpoint data if found, otherwise FALSE
3259 */
3260 public function updateContentTypeCache($postData = NULL) {
3261 $interface = $this->h5pF;
3262
3263 // Make sure data is sent!
3264 if (!isset($postData) || !isset($postData['uuid'])) {
3265 return $this->fetchLibrariesMetadata();
3266 }
3267
3268 $postData['current_cache'] = $this->h5pF->getOption('content_type_cache_updated_at', 0);
3269
3270 $data = $interface->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT_TYPES), $postData);
3271
3272 if (! $this->h5pF->getOption('hub_is_enabled', TRUE)) {
3273 return TRUE;
3274 }
3275
3276 // No data received
3277 if (!$data) {
3278 $interface->setErrorMessage(
3279 $interface->t("Couldn't communicate with the H5P Hub. Please try again later."),
3280 'failed-communicationg-with-hub'
3281 );
3282 return FALSE;
3283 }
3284
3285 $json = json_decode($data);
3286
3287 // No libraries received
3288 if (!isset($json->contentTypes) || empty($json->contentTypes)) {
3289 $interface->setErrorMessage(
3290 $interface->t('No content types were received from the H5P Hub. Please try again later.'),
3291 'no-content-types-from-hub'
3292 );
3293 return FALSE;
3294 }
3295
3296 // Replace content type cache
3297 $interface->replaceContentTypeCache($json);
3298
3299 // Inform of the changes and update timestamp
3300 $interface->setInfoMessage($interface->t('Library cache was successfully updated!'));
3301 $interface->setOption('content_type_cache_updated_at', time());
3302 return $data;
3303 }
3304
3305 /**
3306 * Check if the current server setup is valid and set error messages
3307 *
3308 * @return object Setup object with errors and disable hub properties
3309 */
3310 public function checkSetupErrorMessage() {
3311 $setup = (object) array(
3312 'errors' => array(),
3313 'disable_hub' => FALSE
3314 );
3315
3316 if (!class_exists('ZipArchive')) {
3317 $setup->errors[] = $this->h5pF->t('Your PHP version does not support ZipArchive.');
3318 $setup->disable_hub = TRUE;
3319 }
3320
5da7a7fc
SA
3321 // Moodle: the extension mbstring is optional.
3322 /*
45899564
AA
3323 if (!extension_loaded('mbstring')) {
3324 $setup->errors[] = $this->h5pF->t(
3325 'The mbstring PHP extension is not loaded. H5P needs this to function properly'
3326 );
3327 $setup->disable_hub = TRUE;
3328 }
5da7a7fc 3329 */
45899564
AA
3330
3331 // Check php version >= 5.2
3332 $php_version = explode('.', phpversion());
3333 if ($php_version[0] < 5 || ($php_version[0] === 5 && $php_version[1] < 2)) {
3334 $setup->errors[] = $this->h5pF->t('Your PHP version is outdated. H5P requires version 5.2 to function properly. Version 5.6 or later is recommended.');
3335 $setup->disable_hub = TRUE;
3336 }
3337
3338 // Check write access
3339 if (!$this->fs->hasWriteAccess()) {
3340 $setup->errors[] = $this->h5pF->t('A problem with the server write access was detected. Please make sure that your server can write to your data folder.');
3341 $setup->disable_hub = TRUE;
3342 }
3343
3344 $max_upload_size = self::returnBytes(ini_get('upload_max_filesize'));
3345 $max_post_size = self::returnBytes(ini_get('post_max_size'));
3346 $byte_threshold = 5000000; // 5MB
3347 if ($max_upload_size < $byte_threshold) {
3348 $setup->errors[] =
3349 $this->h5pF->t('Your PHP max upload size is quite small. With your current setup, you may not upload files larger than %number MB. This might be a problem when trying to upload H5Ps, images and videos. Please consider to increase it to more than 5MB.', array('%number' => number_format($max_upload_size / 1024 / 1024, 2, '.', ' ')));
3350 }
3351
3352 if ($max_post_size < $byte_threshold) {
3353 $setup->errors[] =
3354 $this->h5pF->t('Your PHP max post size is quite small. With your current setup, you may not upload files larger than %number MB. This might be a problem when trying to upload H5Ps, images and videos. Please consider to increase it to more than 5MB', array('%number' => number_format($max_upload_size / 1024 / 1024, 2, '.', ' ')));
3355 }
3356
3357 if ($max_upload_size > $max_post_size) {
3358 $setup->errors[] =
3359 $this->h5pF->t('Your PHP max upload size is bigger than your max post size. This is known to cause issues in some installations.');
3360 }
3361
3362 // Check SSL
3363 if (!extension_loaded('openssl')) {
3364 $setup->errors[] =
3365 $this->h5pF->t('Your server does not have SSL enabled. SSL should be enabled to ensure a secure connection with the H5P hub.');
3366 $setup->disable_hub = TRUE;
3367 }
3368
3369 return $setup;
3370 }
3371
3372 /**
3373 * Check that all H5P requirements for the server setup is met.
3374 */
3375 public function checkSetupForRequirements() {
3376 $setup = $this->checkSetupErrorMessage();
3377
3378 $this->h5pF->setOption('hub_is_enabled', !$setup->disable_hub);
3379 if (!empty($setup->errors)) {
3380 foreach ($setup->errors as $err) {
3381 $this->h5pF->setErrorMessage($err);
3382 }
3383 }
3384
3385 if ($setup->disable_hub) {
3386 // Inform how to re-enable hub
3387 $this->h5pF->setErrorMessage(
3388 $this->h5pF->t('H5P hub communication has been disabled because one or more H5P requirements failed.')
3389 );
3390 $this->h5pF->setErrorMessage(
3391 $this->h5pF->t('When you have revised your server setup you may re-enable H5P hub communication in H5P Settings.')
3392 );
3393 }
3394 }
3395
3396 /**
3397 * Return bytes from php_ini string value
3398 *
3399 * @param string $val
3400 *
3401 * @return int|string
3402 */
3403 public static function returnBytes($val) {
3404 $val = trim($val);
3405 $last = strtolower($val[strlen($val) - 1]);
3406 $bytes = (int) $val;
3407
3408 switch ($last) {
3409 case 'g':
3410 $bytes *= 1024;
3411 case 'm':
3412 $bytes *= 1024;
3413 case 'k':
3414 $bytes *= 1024;
3415 }
3416
3417 return $bytes;
3418 }
3419
3420 /**
3421 * Check if the current user has permission to update and install new
3422 * libraries.
3423 *
3424 * @param bool [$set] Optional, sets the permission
3425 * @return bool
3426 */
3427 public function mayUpdateLibraries($set = null) {
3428 static $can;
3429
3430 if ($set !== null) {
3431 // Use value set
3432 $can = $set;
3433 }
3434
3435 if ($can === null) {
3436 // Ask our framework
3437 $can = $this->h5pF->mayUpdateLibraries();
3438 }
3439
3440 return $can;
3441 }
3442
3443 /**
3444 * Provide localization for the Core JS
3445 * @return array
3446 */
3447 public function getLocalization() {
3448 return array(
3449 'fullscreen' => $this->h5pF->t('Fullscreen'),
3450 'disableFullscreen' => $this->h5pF->t('Disable fullscreen'),
3451 'download' => $this->h5pF->t('Download'),
3452 'copyrights' => $this->h5pF->t('Rights of use'),
3453 'embed' => $this->h5pF->t('Embed'),
3454 'size' => $this->h5pF->t('Size'),
3455 'showAdvanced' => $this->h5pF->t('Show advanced'),
3456 'hideAdvanced' => $this->h5pF->t('Hide advanced'),
3457 'advancedHelp' => $this->h5pF->t('Include this script on your website if you want dynamic sizing of the embedded content:'),
3458 'copyrightInformation' => $this->h5pF->t('Rights of use'),
3459 'close' => $this->h5pF->t('Close'),
3460 'title' => $this->h5pF->t('Title'),
3461 'author' => $this->h5pF->t('Author'),
3462 'year' => $this->h5pF->t('Year'),
3463 'source' => $this->h5pF->t('Source'),
3464 'license' => $this->h5pF->t('License'),
3465 'thumbnail' => $this->h5pF->t('Thumbnail'),
3466 'noCopyrights' => $this->h5pF->t('No copyright information available for this content.'),
3467 'reuse' => $this->h5pF->t('Reuse'),
3468 'reuseContent' => $this->h5pF->t('Reuse Content'),
3469 'reuseDescription' => $this->h5pF->t('Reuse this content.'),
3470 'downloadDescription' => $this->h5pF->t('Download this content as a H5P file.'),
3471 'copyrightsDescription' => $this->h5pF->t('View copyright information for this content.'),
3472 'embedDescription' => $this->h5pF->t('View the embed code for this content.'),
3473 'h5pDescription' => $this->h5pF->t('Visit H5P.org to check out more cool content.'),
3474 'contentChanged' => $this->h5pF->t('This content has changed since you last used it.'),
3475 'startingOver' => $this->h5pF->t("You'll be starting over."),
3476 'by' => $this->h5pF->t('by'),
3477 'showMore' => $this->h5pF->t('Show more'),
3478 'showLess' => $this->h5pF->t('Show less'),
3479 'subLevel' => $this->h5pF->t('Sublevel'),
3480 'confirmDialogHeader' => $this->h5pF->t('Confirm action'),
3481 'confirmDialogBody' => $this->h5pF->t('Please confirm that you wish to proceed. This action is not reversible.'),
3482 'cancelLabel' => $this->h5pF->t('Cancel'),
3483 'confirmLabel' => $this->h5pF->t('Confirm'),
3484 'licenseU' => $this->h5pF->t('Undisclosed'),
3485 'licenseCCBY' => $this->h5pF->t('Attribution'),
3486 'licenseCCBYSA' => $this->h5pF->t('Attribution-ShareAlike'),
3487 'licenseCCBYND' => $this->h5pF->t('Attribution-NoDerivs'),
3488 'licenseCCBYNC' => $this->h5pF->t('Attribution-NonCommercial'),
3489 'licenseCCBYNCSA' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike'),
3490 'licenseCCBYNCND' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs'),
3491 'licenseCC40' => $this->h5pF->t('4.0 International'),
3492 'licenseCC30' => $this->h5pF->t('3.0 Unported'),
3493 'licenseCC25' => $this->h5pF->t('2.5 Generic'),
3494 'licenseCC20' => $this->h5pF->t('2.0 Generic'),
3495 'licenseCC10' => $this->h5pF->t('1.0 Generic'),
3496 'licenseGPL' => $this->h5pF->t('General Public License'),
3497 'licenseV3' => $this->h5pF->t('Version 3'),
3498 'licenseV2' => $this->h5pF->t('Version 2'),
3499 'licenseV1' => $this->h5pF->t('Version 1'),
3500 'licensePD' => $this->h5pF->t('Public Domain'),
3501 'licenseCC010' => $this->h5pF->t('CC0 1.0 Universal (CC0 1.0) Public Domain Dedication'),
3502 'licensePDM' => $this->h5pF->t('Public Domain Mark'),
3503 'licenseC' => $this->h5pF->t('Copyright'),
3504 'contentType' => $this->h5pF->t('Content Type'),
3505 'licenseExtras' => $this->h5pF->t('License Extras'),
3506 'changes' => $this->h5pF->t('Changelog'),
3507 'contentCopied' => $this->h5pF->t('Content is copied to the clipboard'),
3508 'connectionLost' => $this->h5pF->t('Connection lost. Results will be stored and sent when you regain connection.'),
3509 'connectionReestablished' => $this->h5pF->t('Connection reestablished.'),
3510 'resubmitScores' => $this->h5pF->t('Attempting to submit stored results.'),
3511 'offlineDialogHeader' => $this->h5pF->t('Your connection to the server was lost'),
3512 'offlineDialogBody' => $this->h5pF->t('We were unable to send information about your completion of this task. Please check your internet connection.'),
3513 'offlineDialogRetryMessage' => $this->h5pF->t('Retrying in :num....'),
3514 'offlineDialogRetryButtonLabel' => $this->h5pF->t('Retry now'),
3515 'offlineSuccessfulSubmit' => $this->h5pF->t('Successfully submitted results.'),
3516 );
3517 }
3518}
3519
3520/**
3521 * Functions for validating basic types from H5P library semantics.
3522 * @property bool allowedStyles
3523 */
3524class H5PContentValidator {
3525 public $h5pF;
3526 public $h5pC;
3527 private $typeMap, $libraries, $dependencies, $nextWeight;
3528 private static $allowed_styleable_tags = array('span', 'p', 'div','h1','h2','h3', 'td');
3529
3530 /**
3531 * Constructor for the H5PContentValidator
3532 *
3533 * @param object $H5PFramework
3534 * The frameworks implementation of the H5PFrameworkInterface
3535 * @param object $H5PCore
3536 * The main H5PCore instance
3537 */
3538 public function __construct($H5PFramework, $H5PCore) {
3539 $this->h5pF = $H5PFramework;
3540 $this->h5pC = $H5PCore;
3541 $this->typeMap = array(
3542 'text' => 'validateText',
3543 'number' => 'validateNumber',
3544 'boolean' => 'validateBoolean',
3545 'list' => 'validateList',
3546 'group' => 'validateGroup',
3547 'file' => 'validateFile',
3548 'image' => 'validateImage',
3549 'video' => 'validateVideo',
3550 'audio' => 'validateAudio',
3551 'select' => 'validateSelect',
3552 'library' => 'validateLibrary',
3553 );
3554 $this->nextWeight = 1;
3555
3556 // Keep track of the libraries we load to avoid loading it multiple times.
3557 $this->libraries = array();
3558
3559 // Keep track of all dependencies for the given content.
3560 $this->dependencies = array();
3561 }
3562
3563 /**
3564 * Add Addon library.
3565 */
3566 public function addon($library) {
3567 $depKey = 'preloaded-' . $library['machineName'];
3568 $this->dependencies[$depKey] = array(
3569 'library' => $library,
3570 'type' => 'preloaded'
3571 );
3572 $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight);
3573 $this->dependencies[$depKey]['weight'] = $this->nextWeight++;
3574 }
3575
3576 /**
3577 * Get the flat dependency tree.
3578 *
3579 * @return array
3580 */
3581 public function getDependencies() {
3582 return $this->dependencies;
3583 }
3584
3585 /**
3586 * Validate metadata
3587 *
3588 * @param array $metadata
3589 * @return array Validated & filtered
3590 */
3591 public function validateMetadata($metadata) {
3592 $semantics = $this->getMetadataSemantics();
3593 $group = (object)$metadata;
3594
3595 // Stop complaining about "invalid selected option in select" for
3596 // old content without license chosen.
3597 if (!isset($group->license)) {
3598 $group->license = 'U';
3599 }
3600
3601 $this->validateGroup($group, (object) array(
3602 'type' => 'group',
3603 'fields' => $semantics,
3604 ), FALSE);
3605
3606 return (array)$group;
3607 }
3608
3609 /**
3610 * Validate given text value against text semantics.
3611 * @param $text
3612 * @param $semantics
3613 */
3614 public function validateText(&$text, $semantics) {
3615 if (!is_string($text)) {
3616 $text = '';
3617 }
3618 if (isset($semantics->tags)) {
3619 // Not testing for empty array allows us to use the 4 defaults without
3620 // specifying them in semantics.
3621 $tags = array_merge(array('div', 'span', 'p', 'br'), $semantics->tags);
3622
3623 // Add related tags for table etc.
3624 if (in_array('table', $tags)) {
3625 $tags = array_merge($tags, array('tr', 'td', 'th', 'colgroup', 'thead', 'tbody', 'tfoot'));
3626 }
3627 if (in_array('b', $tags) && ! in_array('strong', $tags)) {
3628 $tags[] = 'strong';
3629 }
3630 if (in_array('i', $tags) && ! in_array('em', $tags)) {
3631 $tags[] = 'em';
3632 }
3633 if (in_array('ul', $tags) || in_array('ol', $tags) && ! in_array('li', $tags)) {
3634 $tags[] = 'li';
3635 }
3636 if (in_array('del', $tags) || in_array('strike', $tags) && ! in_array('s', $tags)) {
3637 $tags[] = 's';
3638 }
3639
3640 // Determine allowed style tags
3641 $stylePatterns = array();
3642 // All styles must be start to end patterns (^...$)
3643 if (isset($semantics->font)) {
3644 if (isset($semantics->font->size) && $semantics->font->size) {
3645 $stylePatterns[] = '/^font-size: *[0-9.]+(em|px|%) *;?$/i';
3646 }
3647 if (isset($semantics->font->family) && $semantics->font->family) {
3648 $stylePatterns[] = '/^font-family: *[-a-z0-9," ]+;?$/i';
3649 }
3650 if (isset($semantics->font->color) && $semantics->font->color) {
3651 $stylePatterns[] = '/^color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i';
3652 }
3653 if (isset($semantics->font->background) && $semantics->font->background) {
3654 $stylePatterns[] = '/^background-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i';
3655 }
3656 if (isset($semantics->font->spacing) && $semantics->font->spacing) {
3657 $stylePatterns[] = '/^letter-spacing: *[0-9.]+(em|px|%) *;?$/i';
3658 }
3659 if (isset($semantics->font->height) && $semantics->font->height) {
3660 $stylePatterns[] = '/^line-height: *[0-9.]+(em|px|%|) *;?$/i';
3661 }
3662 }
3663
3664 // Alignment is allowed for all wysiwyg texts
3665 $stylePatterns[] = '/^text-align: *(center|left|right);?$/i';
3666
3667 // Strip invalid HTML tags.
3668 $text = $this->filter_xss($text, $tags, $stylePatterns);
3669 }
3670 else {
3671 // Filter text to plain text.
3672 $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8', FALSE);
3673 }
3674
3675 // Check if string is within allowed length
3676 if (isset($semantics->maxLength)) {
5da7a7fc
SA
3677 // Moodle: the extension mbstring is optional.
3678 /*
45899564
AA
3679 if (!extension_loaded('mbstring')) {
3680 $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
3681 }
5da7a7fc
SA
3682 */
3683 $text = \core_text::substr($text, 0, $semantics->maxLength);
45899564
AA
3684 }
3685
3686 // Check if string is according to optional regexp in semantics
3687 if (!($text === '' && isset($semantics->optional) && $semantics->optional) && isset($semantics->regexp)) {
3688 // Escaping '/' found in patterns, so that it does not break regexp fencing.
3689 $pattern = '/' . str_replace('/', '\\/', $semantics->regexp->pattern) . '/';
3690 $pattern .= isset($semantics->regexp->modifiers) ? $semantics->regexp->modifiers : '';
3691 if (preg_match($pattern, $text) === 0) {
3692 // Note: explicitly ignore return value FALSE, to avoid removing text
3693 // if regexp is invalid...
3694 $this->h5pF->setErrorMessage($this->h5pF->t('Provided string is not valid according to regexp in semantics. (value: "%value", regexp: "%regexp")', array('%value' => $text, '%regexp' => $pattern)), 'semantics-invalid-according-regexp');
3695 $text = '';
3696 }
3697 }
3698 }
3699
3700 /**
3701 * Validates content files
3702 *
3703 * @param string $contentPath
3704 * The path containing content files to validate.
3705 * @param bool $isLibrary
3706 * @return bool TRUE if all files are valid
3707 * TRUE if all files are valid
3708 * FALSE if one or more files fail validation. Error message should be set accordingly by validator.
3709 */
3710 public function validateContentFiles($contentPath, $isLibrary = FALSE) {
3711 if ($this->h5pC->disableFileCheck === TRUE) {
3712 return TRUE;
3713 }
3714
3715 // Scan content directory for files, recurse into sub directories.
3716 $files = array_diff(scandir($contentPath), array('.','..'));
3717 $valid = TRUE;
3718 $whitelist = $this->h5pF->getWhitelist($isLibrary, H5PCore::$defaultContentWhitelist, H5PCore::$defaultLibraryWhitelistExtras);
3719
3720 $wl_regex = '/\.(' . preg_replace('/ +/i', '|', preg_quote($whitelist)) . ')$/i';
3721
3722 foreach ($files as $file) {
64153166 3723 $filePath = $contentPath . '/' . $file;
45899564
AA
3724 if (is_dir($filePath)) {
3725 $valid = $this->validateContentFiles($filePath, $isLibrary) && $valid;
3726 }
3727 else {
3728 // Snipped from drupal 6 "file_validate_extensions". Using own code
3729 // to avoid 1. creating a file-like object just to test for the known
3730 // file name, 2. testing against a returned error array that could
3731 // never be more than 1 element long anyway, 3. recreating the regex
3732 // for every file.
5da7a7fc
SA
3733 // Moodle: the extension mbstring is optional.
3734 /*
45899564
AA
3735 if (!extension_loaded('mbstring')) {
3736 $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
3737 $valid = FALSE;
3738 }
5da7a7fc
SA
3739 */
3740 if (!preg_match($wl_regex, \core_text::strtolower($file))) {
45899564
AA
3741 $this->h5pF->setErrorMessage($this->h5pF->t('File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.', array('%filename' => $file, '%files-allowed' => $whitelist)), 'not-in-whitelist');
3742 $valid = FALSE;
3743 }
3744 }
3745 }
3746 return $valid;
3747 }
3748
3749 /**
3750 * Validate given value against number semantics
3751 * @param $number
3752 * @param $semantics
3753 */
3754 public function validateNumber(&$number, $semantics) {
3755 // Validate that $number is indeed a number
3756 if (!is_numeric($number)) {
3757 $number = 0;
3758 }
3759 // Check if number is within valid bounds. Move within bounds if not.
3760 if (isset($semantics->min) && $number < $semantics->min) {
3761 $number = $semantics->min;
3762 }
3763 if (isset($semantics->max) && $number > $semantics->max) {
3764 $number = $semantics->max;
3765 }
3766 // Check if number is within allowed bounds even if step value is set.
3767 if (isset($semantics->step)) {
3768 $testNumber = $number - (isset($semantics->min) ? $semantics->min : 0);
3769 $rest = $testNumber % $semantics->step;
3770 if ($rest !== 0) {
3771 $number -= $rest;
3772 }
3773 }
3774 // Check if number has proper number of decimals.
3775 if (isset($semantics->decimals)) {
3776 $number = round($number, $semantics->decimals);
3777 }
3778 }
3779
3780 /**
3781 * Validate given value against boolean semantics
3782 * @param $bool
3783 * @return bool
3784 */
3785 public function validateBoolean(&$bool) {
3786 return is_bool($bool);
3787 }
3788
3789 /**
3790 * Validate select values
3791 * @param $select
3792 * @param $semantics
3793 */
3794 public function validateSelect(&$select, $semantics) {
3795 $optional = isset($semantics->optional) && $semantics->optional;
3796 $strict = FALSE;
3797 if (isset($semantics->options) && !empty($semantics->options)) {
3798 // We have a strict set of options to choose from.
3799 $strict = TRUE;
3800 $options = array();
3801
3802 foreach ($semantics->options as $option) {
3803 // Support optgroup - just flatten options into one
3804 if (isset($option->type) && $option->type === 'optgroup') {
3805 foreach ($option->options as $suboption) {
3806 $options[$suboption->value] = TRUE;
3807 }
3808 }
3809 elseif (isset($option->value)) {
3810 $options[$option->value] = TRUE;
3811 }
3812 }
3813 }
3814
3815 if (isset($semantics->multiple) && $semantics->multiple) {
3816 // Multi-choice generates array of values. Test each one against valid
3817 // options, if we are strict. First make sure we are working on an
3818 // array.
3819 if (!is_array($select)) {
3820 $select = array($select);
3821 }
3822
3823 foreach ($select as $key => &$value) {
3824 if ($strict && !$optional && !isset($options[$value])) {
3825 $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in multi-select.'));
3826 unset($select[$key]);
3827 }
3828 else {
3829 $select[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8', FALSE);
3830 }
3831 }
3832 }
3833 else {
3834 // Single mode. If we get an array in here, we chop off the first
3835 // element and use that instead.
3836 if (is_array($select)) {
3837 $select = $select[0];
3838 }
3839
3840 if ($strict && !$optional && !isset($options[$select])) {
3841 $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in select.'));
3842 $select = $semantics->options[0]->value;
3843 }
3844 $select = htmlspecialchars($select, ENT_QUOTES, 'UTF-8', FALSE);
3845 }
3846 }
3847
3848 /**
3849 * Validate given list value against list semantics.
3850 * Will recurse into validating each item in the list according to the type.
3851 * @param $list
3852 * @param $semantics
3853 */
3854 public function validateList(&$list, $semantics) {
3855 $field = $semantics->field;
3856 $function = $this->typeMap[$field->type];
3857
3858 // Check that list is not longer than allowed length. We do this before
3859 // iterating to avoid unnecessary work.
3860 if (isset($semantics->max)) {
3861 array_splice($list, $semantics->max);
3862 }
3863
3864 if (!is_array($list)) {
3865 $list = array();
3866 }
3867
3868 // Validate each element in list.
3869 foreach ($list as $key => &$value) {
3870 if (!is_int($key)) {
3871 array_splice($list, $key, 1);
3872 continue;
3873 }
3874 $this->$function($value, $field);
3875 if ($value === NULL) {
3876 array_splice($list, $key, 1);
3877 }
3878 }
3879
3880 if (count($list) === 0) {
3881 $list = NULL;
3882 }
3883 }
3884
3885 /**
3886 * Validate a file like object, such as video, image, audio and file.
3887 * @param $file
3888 * @param $semantics
3889 * @param array $typeValidKeys
3890 */
3891 private function _validateFilelike(&$file, $semantics, $typeValidKeys = array()) {
3892 // Do not allow to use files from other content folders.
3893 $matches = array();
3894 if (preg_match($this->h5pC->relativePathRegExp, $file->path, $matches)) {
3895 $file->path = $matches[5];
3896 }
3897
3898 // Remove temporary files suffix
3899 if (substr($file->path, -4, 4) === '#tmp') {
3900 $file->path = substr($file->path, 0, strlen($file->path) - 4);
3901 }
3902
3903 // Make sure path and mime does not have any special chars
3904 $file->path = htmlspecialchars($file->path, ENT_QUOTES, 'UTF-8', FALSE);
3905