7654b2d9166a0abd2d20527a29c3469e46d9e334
[moodle.git] / h5p / h5plib / v124 / joubel / core / h5p.classes.php
1 <?php
2 /**
3  * Interface defining functions the h5p library needs the framework to implement
4  */
5 interface H5PFrameworkInterface {
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();
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);
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);
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);
46   /**
47    * Show the user an information message
48    *
49    * @param string $message
50    *  The error message
51    */
52   public function setInfoMessage($message);
54   /**
55    * Return messages
56    *
57    * @param string $type 'info' or 'error'
58    * @return string[]
59    */
60   public function getMessages($type);
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());
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);
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();
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();
104   /**
105    * Load addon libraries
106    *
107    * @return array
108    */
109   public function loadAddons();
111   /**
112    * Load config for libraries
113    *
114    * @param array $libraries
115    * @return array
116    */
117   public function getLibraryConfig($libraries = NULL);
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();
128   /**
129    * Returns the URL to the library admin page
130    *
131    * @return string
132    *   URL to admin page
133    */
134   public function getAdminUrl();
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);
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);
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);
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();
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();
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);
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);
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);
260   /**
261    * Resets marked user data for the given content.
262    *
263    * @param int $contentId
264    */
265   public function resetContentUserData($contentId);
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);
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);
299   /**
300    * Deletes content data
301    *
302    * @param int $contentId
303    *   Id identifying the content
304    */
305   public function deleteContentData($contentId);
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);
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);
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);
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);
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);
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);
417   /**
418    * Delete all dependencies belonging to given library
419    *
420    * @param int $libraryId
421    *   Library identifier
422    */
423   public function deleteLibraryDependencies($libraryId);
425   /**
426    * Start an atomic operation against the dependency storage
427    */
428   public function lockDependencyStorage();
430   /**
431    * Stops an atomic operation against the dependency storage
432    */
433   public function unlockDependencyStorage();
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);
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);
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);
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);
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);
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);
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);
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();
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);
545   /**
546    * Determines if content slug is used.
547    *
548    * @param string $slug
549    * @return boolean
550    */
551   public function isContentSlugAvailable($slug);
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);
561   /**
562    * Aggregate the current number of H5P authors
563    * @return int
564    */
565   public function getNumAuthors();
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);
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);
590   /**
591    * Get the amount of content items associated to a library
592    * return int
593    */
594   public function getLibraryContentCount();
596   /**
597    * Will trigger after the export file is created.
598    */
599   public function afterExportCreated($content, $filename);
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);
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);
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);
628 /**
629  * This class is used for validating H5P files
630  */
631 class H5PValidator {
632   public $h5pF;
633   public $h5pC;
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   );
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   );
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   );
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   );
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   }
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     }
761     // Moodle: the extension mbstring is optional.
762     /*
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     }
768     */
770     // Create a temporary dir to extract package in.
771     $tmpDir = $this->h5pF->getUploadedH5pFolderPath();
772     $tmpPath = $this->h5pF->getUploadedH5pPath();
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     }
781     // Extract and then remove the package file.
782     $zip = new ZipArchive;
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     }
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();
797     $valid = TRUE;
798     $libraries = array();
800     $totalSize = 0;
801     $mainH5pExists = FALSE;
802     $contentExists = FALSE;
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);
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'];
815       $fileName = \core_text::strtolower($fileStat['name']);
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         }
839         // Further library validation happens after the files are extracted
840       }
841     }
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     }
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       }
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     }
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     }
892     // Extract the files from the package
893     for ($i = 0; $i < $zip->numFiles; $i++) {
894       $fileName = $zip->statIndex($i)['name'];
896       if (preg_match('/(^[\._]|\/[\._])/', $fileName) !== 0) {
897         continue; // Skip any file or folder starting with a . or _
898       }
900       $isContentFile = (substr($fileName, 0, 8) === 'content/');
901       $isFolder = (strpos($fileName, '/') !== FALSE);
903       if ($skipContent !== FALSE && $isContentFile) {
904         continue; // Skipping any content files
905       }
907       if (!($isContentFile || ($canInstall && $isFolder))) {
908         continue; // Not something we want to unpack
909       }
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       }
922       // Use file interface to allow overrides
923       $this->h5pC->fs->saveFileFromZip($tmpDir, $fileName, $fileStream);
925       // Clean up
926       if (is_resource($fileStream)) {
927         fclose($fileStream);
928       }
929     }
931     // We're done with the zip file, clean up the stuff
932     $zip->close();
933     unlink($tmpPath);
935     if ($canInstall) {
936       // Process and validate libraries using the unpacked library folders
937       $files = scandir($tmpDir);
938       foreach ($files as $file) {
939         $filePath = $tmpDir . '/' . $file;
941         if ($file === '.' || $file === '..' || $file === 'content' || !is_dir($filePath)) {
942           continue; // Skip
943         }
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         }
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         }
966         $libraryH5PData['uploadDirectory'] = $filePath;
967         $libraries[H5PCore::libraryToString($libraryH5PData)] = $libraryH5PData;
968       }
969     }
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         }
991         $libraries = $upgrades;
992       }
994       $this->h5pC->librariesJsonData = $libraries;
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       }
1002       $missingLibraries = $this->getMissingLibraries($libraries);
1003       foreach ($missingLibraries as $libString => $missing) {
1004         if ($this->h5pC->getLibraryId($missing, $libString)) {
1005           unset($missingLibraries[$libString]);
1006         }
1007       }
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         }
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   }
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     }
1061     // Read data
1062     $contents = '';
1063     while (!feof($stream)) {
1064       $contents .= fread($stream, 2);
1065     }
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     }
1075     // All OK
1076     return $json;
1077   }
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   }
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     }
1108     $h5pData = $this->getJsonData($filePath . '/' . 'library.json');
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     }
1114     // validate json if a semantics file is provided
1115     $semanticsPath = $filePath . '/' . 'semantics.json';
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     }
1127     // validate language folder if it exists
1128     $languagePath = $filePath . '/' . 'language';
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         }
1139         $languageJson = $this->getJsonData($languagePath . '/' . $languageFile, TRUE);
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     }
1149     // Check for icon:
1150     $h5pData['hasIcon'] = file_exists($filePath . '/' . 'icon.svg');
1152     $validLibrary = $this->isValidH5pData($h5pData, $file, $this->libraryRequired, $this->libraryOptional);
1154     //$validLibrary = $this->h5pCV->validateContentFiles($filePath, TRUE) && $validLibrary;
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   }
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   }
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   }
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) {
1234       $path = str_replace(array('/', '\\'), '/', $file['path']);
1235       if (!file_exists($tmpDir . '/' . $library . '/' . $path)) {
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   }
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;
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']) )) {
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         );
1281         $valid = false;
1282       }
1283     }
1285     return $valid;
1286   }
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;
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     }
1315     return $valid;
1316   }
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;
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   }
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   }
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   }
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   }
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   }
1483 /**
1484  * This class is used for saving H5P files
1485  */
1486 class H5PStorage {
1488   public $h5pF;
1489   public $h5pC;
1491   public $contentId = NULL; // Quick fix so WP can get ID of new content.
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   }
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     }
1524     if (!$skipContent) {
1525       $basePath = $this->h5pF->getUploadedH5pFolderPath();
1526       $current_path = $basePath . '/' . 'content';
1528       // Save content
1529       if ($content === NULL) {
1530         $content = array();
1531       }
1532       if (!is_array($content)) {
1533         $content = array('id' => $content);
1534       }
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       }
1545       $content['params'] = file_get_contents($current_path . '/' . 'content.json');
1547       if (isset($options['disable'])) {
1548         $content['disable'] = $options['disable'];
1549       }
1550       $content['id'] = $this->h5pC->saveContent($content, $contentMainId);
1551       $this->contentId = $content['id'];
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       }
1561       // Remove temp content folder
1562       H5PCore::deleteFileTree($basePath);
1563     }
1564   }
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;
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);
1581       // Assume new library
1582       $new = TRUE;
1583       if ($libraryId) {
1584         // Found old library
1585         $library['libraryId'] = $libraryId;
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       }
1598       // Indicate that the dependencies of this library should be saved.
1599       $library['saveDependencies'] = TRUE;
1601       // Convert metadataSettings values to boolean & json_encode it before saving
1602       $library['metadataSettings'] = isset($library['metadataSettings']) ?
1603         H5PMetadata::boolifyAndEncodeSettings($library['metadataSettings']) :
1604         NULL;
1606       $this->h5pF->saveLibraryData($library, $new);
1608       // Save library folder
1609       $this->h5pC->fs->saveLibrary($library);
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       }
1617       // Remove tmp folder
1618       H5PCore::deleteFileTree($library['uploadDirectory']);
1620       if ($new) {
1621         $newOnes++;
1622       }
1623       else {
1624         $oldOnes++;
1625       }
1626     }
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       }
1635       // TODO: Should the table be locked for this operation?
1637       // Remove any old dependencies
1638       $this->h5pF->deleteLibraryDependencies($library['libraryId']);
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       }
1651       $library_ids[] = $library['libraryId'];
1652     }
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     }
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     }
1704     if (isset($message)) {
1705       $this->h5pF->setInfoMessage($message);
1706     }
1707   }
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   }
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   }
1739 /**
1740 * This class is used for exporting zips
1741 */
1742 Class H5PExport {
1743   public $h5pF;
1744   public $h5pC;
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   }
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   }
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) {
1779     // Get path to temporary folder, where export will be contained
1780     $tmpPath = $this->h5pC->fs->getTmpPath();
1781     mkdir($tmpPath, 0777, true);
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     }
1793     // Update content.json with content from database
1794     file_put_contents("{$tmpPath}/content/content.json", $content['filtered']);
1796     // Make embedType into an array
1797     $embedTypes = explode(', ', $content['embedType']);
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     );
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     }
1815     // Remove all values that are not set
1816     foreach ($h5pJson as $key => $value) {
1817       if (!isset($value)) {
1818         unset($h5pJson[$key]);
1819       }
1820     }
1822     // Add dependencies to h5p
1823     foreach ($content['dependencies'] as $dependency) {
1824       $library = $dependency['library'];
1826       try {
1827         $exportFolder = NULL;
1829         // Determine path of export library
1830         if (isset($this->h5pC) && isset($this->h5pC->h5pD)) {
1832           // Tries to find library in development folder
1833           $isDevLibrary = $this->h5pC->h5pD->getLibrary(
1834               $library['machineName'],
1835               $library['majorVersion'],
1836               $library['minorVersion']
1837           );
1839           if ($isDevLibrary !== NULL && isset($library['path'])) {
1840             $exportFolder = "/" . $library['path'];
1841           }
1842         }
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       }
1853       // Do not add editor dependencies to h5p json.
1854       if ($dependency['type'] === 'editor') {
1855         continue;
1856       }
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     }
1866     // Save h5p.json
1867     $results = print_r(json_encode($h5pJson), true);
1868     file_put_contents("{$tmpPath}/h5p.json", $results);
1870     // Get a complete file list from our tmp dir
1871     $files = array();
1872     self::populateFileList($tmpPath, $files);
1874     // Get path to temporary export target file
1875     $tmpFile = $this->h5pC->fs->getTmpPath();
1877     // Create new zip instance.
1878     $zip = new ZipArchive();
1879     $zip->open($tmpFile, ZipArchive::CREATE | ZipArchive::OVERWRITE);
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     }
1890     // Close zip and remove tmp dir
1891     $zip->close();
1892     H5PCore::deleteFileTree($tmpPath);
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     }
1904     unlink($tmpFile);
1905     $this->h5pF->afterExportCreated($content, $filename);
1907     return true;
1908   }
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;
1922     $contents = glob($dir . '/' . '*');
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   }
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   }
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   }
1970 abstract 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;
1979 abstract 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;
1987 abstract class H5PHubEndpoints {
1988   const CONTENT_TYPES = 'api.h5p.org/v1/content-types/';
1989   const SITES = 'api.h5p.org/v1/sites';
1991   public static function createURL($endpoint) {
1992     $protocol = (extension_loaded('openssl') ? 'https' : 'http');
1993     return "{$protocol}://{$endpoint}";
1994   }
1997 /**
1998  * Functions and storage shared by the other H5P classes
1999  */
2000 class H5PCore {
2002   public static $coreApi = array(
2003     'majorVersion' => 1,
2004     'minorVersion' => 24
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   );
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';
2030   public $librariesJsonData, $contentJsonData, $mainJsonData, $h5pF, $fs, $h5pD, $disableFileCheck;
2031   const SECONDS_IN_WEEK = 604800;
2033   private $exportEnabled;
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;
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';
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   );
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;
2071     $this->fs = ($path instanceof \H5PFileStorage ? $path : new \H5PDefaultStorage($path));
2073     $this->url = $url;
2074     $this->exportEnabled = $export;
2075     $this->development_mode = H5PDevelopment::MODE_NONE;
2077     $this->aggregateAssets = FALSE; // Off by default.. for now
2079     $this->detectSiteType();
2080     $this->fullPluginPath = preg_replace('/\/[^\/]+[\/]?$/', '' , dirname(__FILE__));
2082     // Standard regex for converting copied files paths
2083     $this->relativePathRegExp = '/^((\.\.\/){1,2})(.*content\/)?(\d+|editor)\/(.+)$/';
2084   }
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     }
2101     // Some user data for content has to be reset when the content changes.
2102     $this->h5pF->resetContentUserData($contentMainId ? $contentMainId : $content['id']);
2104     return $content['id'];
2105   }
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);
2116     if ($content !== NULL) {
2117       // Validate main content's metadata
2118       $validator = new H5PContentValidator($this->h5pF, $this);
2119       $content['metadata'] = $validator->validateMetadata($content['metadata']);
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']);
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     }
2145     return $content;
2146   }
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     }
2162     if (!(isset($content['library']) && isset($content['params']))) {
2163       return NULL;
2164     }
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)));
2177     // Handle addons:
2178     $addons = $this->h5pF->loadAddons();
2179     foreach ($addons as $addon) {
2180       $add_to = json_decode($addon['addTo']);
2182       if (isset($add_to->content->types)) {
2183         foreach($add_to->content->types as $type) {
2185           if (isset($type->text->regex) &&
2186               $this->textAddonMatches($params->params, $type->text->regex)) {
2187             $validator->addon($addon);
2189             // An addon shall only be added once
2190             break;
2191           }
2192         }
2193       }
2194     }
2196     $params = json_encode($params->params);
2198     // Update content dependencies.
2199     $content['dependencies'] = $validator->getDependencies();
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']);
2206       if (!$content['slug']) {
2207         $content['slug'] = $this->generateContentSlug($content);
2209         // Remove old export file
2210         $this->fs->deleteExport($content['id'] . '.h5p');
2211       }
2213       if ($this->exportEnabled) {
2214         // Recreate export file
2215         $exporter = new H5PExport($this->h5pF, $this);
2216         $content['filtered'] = $params;
2217         $exporter->createExportFile($content);
2218       }
2220       // Cache.
2221       $this->h5pF->updateContentFields($content['id'], array(
2222         'filtered' => $params,
2223         'slug' => $content['slug']
2224       ));
2225     }
2226     return $params;
2227   }
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);
2240     // Property not found
2241     if (!isset($params[$path[0]])) {
2242       return NULL;
2243     }
2245     $first = $params[$path[0]];
2247     // End of path, done
2248     if (sizeof($path) === 1) {
2249       return $first;
2250     }
2252     // We cannot go deeper
2253     if (!is_array($first)) {
2254       return NULL;
2255     }
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     }
2268     // Associative Array
2269     return $this->retrieveValue($first, implode('.', array_slice($path, 1)));
2270   }
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   }
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']);
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     }
2322     return $slug;
2323   }
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);
2335     if (isset($this->h5pD)) {
2336       $developmentLibraries = $this->h5pD->getLibraries();
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     }
2347     return $dependencies;
2348   }
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     }
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   }
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();
2385     foreach ($assets as $asset) {
2386       $url = $asset->path;
2388       // Add URL prefix if not external
2389       if (strpos($asset->path, '://') === FALSE) {
2390         $url = $this->url . $url;
2391       }
2393       // Add version/cache buster if set
2394       if (isset($asset->version)) {
2395         $url .= $asset->version;
2396       }
2398       $urls[] = $url;
2399     }
2401     return $urls;
2402   }
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     );
2418     $key = null;
2420     // Avoid caching empty files
2421     if (empty($dependencies)) {
2422       return $files;
2423     }
2425     if ($this->aggregateAssets) {
2426       // Get aggregated files for assets
2427       $key = self::getDependenciesHash($dependencies);
2429       $cachedAssets = $this->fs->getCachedAssets($key);
2430       if ($cachedAssets !== NULL) {
2431         return array_merge($files, $cachedAssets); // Using cached assets
2432       }
2433     }
2435     // Using content dependencies
2436     foreach ($dependencies as $dependency) {
2437       if (isset($dependency['path']) === FALSE) {
2438         $dependency['path'] = $this->getDependencyPath($dependency);
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     }
2447     if ($this->aggregateAssets) {
2448       // Aggregate and store assets
2449       $this->fs->cacheAssets($files, $key);
2451       // Keep track of which libraries have been cached in case they are updated
2452       $this->h5pF->saveCachedAssets($key, $dependencies);
2453     }
2455     return $files;
2456   }
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   }
2468   private static function getDependenciesHash(&$dependencies) {
2469     // Build hash of dependencies
2470     $toHash = array();
2472     // Use unique identifier for each library version
2473     foreach ($dependencies as $dep) {
2474       $toHash[] = "{$dep['machineName']}-{$dep['majorVersion']}.{$dep['minorVersion']}.{$dep['patchVersion']}";
2475     }
2477     // Sort in case the same dependencies comes in a different order
2478     sort($toHash);
2480     // Calculate hash sum
2481     return hash('sha1', implode('', $toHash));
2482   }
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     }
2499     if ($semantics === NULL) {
2500       // Try to load from DB.
2501       $semantics = $this->h5pF->loadLibrarySemantics($name, $majorVersion, $minorVersion);
2502     }
2504     if ($semantics !== NULL) {
2505       $semantics = json_decode($semantics);
2506       $this->h5pF->alterLibrarySemantics($semantics, $name, $majorVersion, $minorVersion);
2507     }
2509     return $semantics;
2510   }
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     }
2530     if ($library === NULL) {
2531       // Try to load from DB.
2532       $library = $this->h5pF->loadLibrary($name, $majorVersion, $minorVersion);
2533     }
2535     return $library;
2536   }
2538   /**
2539    * Deletes a library
2540    *
2541    * @param stdClass $libraryId
2542    */
2543   public function deleteLibrary($libraryId) {
2544     $this->h5pF->deleteLibrary($libraryId);
2545   }
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       }
2566       if ($type === 'preloaded' && $editor === TRUE) {
2567         // All preloaded dependencies of an editor library is set to editor.
2568         $type = 'editor';
2569       }
2571       foreach ($library[$property] as $dependency) {
2572         $dependencyKey = $type . '-' . $dependency['machineName'];
2573         if (isset($dependencies[$dependencyKey]) === TRUE) {
2574           continue; // Skip, already have this.
2575         }
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   }
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   }
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   }
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   }
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   }
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';
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     }
2712     return $embedType;
2713   }
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   }
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();
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    }
2741    return $upgrades;
2742   }
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();
2757     foreach ($arr as $key => $val) {
2758       $next = -1;
2759       while (($next = strpos($key, '_', $next + 1)) !== FALSE) {
2760         $key = substr_replace($key, strtoupper($key[$next + 1]), $next, 2);
2761       }
2763       $newArr[$key] = $val;
2764     }
2766     return $obj ? (object) $newArr : $newArr;
2767   }
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');
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   }
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();
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     }
2809     return $librariesInstalled;
2810   }
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   }
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     );
2852     // Register site if it is not registered
2853     if (empty($uuid)) {
2854       $registration = $this->h5pF->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::SITES), $registrationData);
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       }
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     }
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     }
2902     $result = $this->updateContentTypeCache($siteData);
2904     // No data received
2905     if (!$result || empty($result)) {
2906       return FALSE;
2907     }
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     }
2918     return $result;
2919   }
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     }
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     }
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   }
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();
2963     $current_display_options = $disable === NULL ? array() : $this->getDisplayOptionsAsArray($disable);
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;
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       }
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       }
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     }
3000     return $display_options;
3001   }
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   }
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);
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]);
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);
3050     return $display_options;
3051   }
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   }
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     }
3081     if (!isset($libraryIdMap[$libString])) {
3082       $libraryIdMap[$libString] = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
3083     }
3085     return $libraryIdMap[$libString];
3086   }
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);
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);
3105     // Replace everything else
3106     $input = preg_replace('/[^a-z0-9]/', '-', $input);
3108     // Prevent double hyphen
3109     $input = preg_replace('/-{2,}/', '-', $input);
3111     // Prevent hyphen in beginning or end
3112     $input = trim($input, '-');
3114     // Prevent to long slug
3115     if (strlen($input) > 91) {
3116       $input = substr($input, 0, 92);
3117     }
3119     // Prevent empty slug
3120     if ($input === '') {
3121       $input = 'interactive';
3122     }
3124     return $input;
3125   }
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;
3140       // Pass data flatly to support old methods
3141       if ($only_data) {
3142         $response = $data;
3143       }
3144     }
3145     self::printJson($response);
3146   }
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     }
3167     if ($error_code !== NULL) {
3168       $response['errorCode'] = $error_code;
3169     }
3171     if ($details !== NULL) {
3172       $response['details'] = $details;
3173     }
3175     self::printJson($response, $status_code);
3176   }
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   }
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   }
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   }
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) {
3218     if (!isset($_SESSION['h5p_token'])) {
3219       // Create an unique key which is used to create action tokens for this session.
3220       if (function_exists('random_bytes')) {
3221         $_SESSION['h5p_token'] = base64_encode(random_bytes(15));
3222       }
3223       else if (function_exists('openssl_random_pseudo_bytes')) {
3224         $_SESSION['h5p_token'] = base64_encode(openssl_random_pseudo_bytes(15));
3225       }
3226       else {
3227         $_SESSION['h5p_token'] = uniqid('', TRUE);
3228       }
3229     }
3231     // Create hash and return
3232     return substr(hash('md5', $action . $time_factor . $_SESSION['h5p_token']), -16, 13);
3233   }
3235   /**
3236    * Verify if the given token is valid for the given action.
3237    *
3238    * @param string $action
3239    * @param string $token
3240    * @return boolean valid token
3241    */
3242   public static function validToken($action, $token) {
3243     // Get the timefactor
3244     $time_factor = self::getTimeFactor();
3246     // Check token to see if it's valid
3247     return $token === self::hashToken($action, $time_factor) || // Under 12 hours
3248            $token === self::hashToken($action, $time_factor - 1); // Between 12-24 hours
3249   }
3251   /**
3252    * Update content type cache
3253    *
3254    * @param object $postData Data sent to the hub
3255    *
3256    * @return bool|object Returns endpoint data if found, otherwise FALSE
3257    */
3258   public function updateContentTypeCache($postData = NULL) {
3259     $interface = $this->h5pF;
3261     // Make sure data is sent!
3262     if (!isset($postData) || !isset($postData['uuid'])) {
3263       return $this->fetchLibrariesMetadata();
3264     }
3266     $postData['current_cache'] = $this->h5pF->getOption('content_type_cache_updated_at', 0);
3268     $data = $interface->fetchExternalData(H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT_TYPES), $postData);
3270     if (! $this->h5pF->getOption('hub_is_enabled', TRUE)) {
3271       return TRUE;
3272     }
3274     // No data received
3275     if (!$data) {
3276       $interface->setErrorMessage(
3277         $interface->t("Couldn't communicate with the H5P Hub. Please try again later."),
3278         'failed-communicationg-with-hub'
3279       );
3280       return FALSE;
3281     }
3283     $json = json_decode($data);
3285     // No libraries received
3286     if (!isset($json->contentTypes) || empty($json->contentTypes)) {
3287       $interface->setErrorMessage(
3288         $interface->t('No content types were received from the H5P Hub. Please try again later.'),
3289         'no-content-types-from-hub'
3290       );
3291       return FALSE;
3292     }
3294     // Replace content type cache
3295     $interface->replaceContentTypeCache($json);
3297     // Inform of the changes and update timestamp
3298     $interface->setInfoMessage($interface->t('Library cache was successfully updated!'));
3299     $interface->setOption('content_type_cache_updated_at', time());
3300     return $data;
3301   }
3303   /**
3304    * Check if the current server setup is valid and set error messages
3305    *
3306    * @return object Setup object with errors and disable hub properties
3307    */
3308   public function checkSetupErrorMessage() {
3309     $setup = (object) array(
3310       'errors' => array(),
3311       'disable_hub' => FALSE
3312     );
3314     if (!class_exists('ZipArchive')) {
3315       $setup->errors[] = $this->h5pF->t('Your PHP version does not support ZipArchive.');
3316       $setup->disable_hub = TRUE;
3317     }
3319     // Moodle: the extension mbstring is optional.
3320     /*
3321     if (!extension_loaded('mbstring')) {
3322       $setup->errors[] = $this->h5pF->t(
3323         'The mbstring PHP extension is not loaded. H5P needs this to function properly'
3324       );
3325       $setup->disable_hub = TRUE;
3326     }
3327     */
3329     // Check php version >= 5.2
3330     $php_version = explode('.', phpversion());
3331     if ($php_version[0] < 5 || ($php_version[0] === 5 && $php_version[1] < 2)) {
3332       $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.');
3333       $setup->disable_hub = TRUE;
3334     }
3336     // Check write access
3337     if (!$this->fs->hasWriteAccess()) {
3338       $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.');
3339       $setup->disable_hub = TRUE;
3340     }
3342     $max_upload_size = self::returnBytes(ini_get('upload_max_filesize'));
3343     $max_post_size   = self::returnBytes(ini_get('post_max_size'));
3344     $byte_threshold  = 5000000; // 5MB
3345     if ($max_upload_size < $byte_threshold) {
3346       $setup->errors[] =
3347         $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, '.', ' ')));
3348     }
3350     if ($max_post_size < $byte_threshold) {
3351       $setup->errors[] =
3352         $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, '.', ' ')));
3353     }
3355     if ($max_upload_size > $max_post_size) {
3356       $setup->errors[] =
3357         $this->h5pF->t('Your PHP max upload size is bigger than your max post size. This is known to cause issues in some installations.');
3358     }
3360     // Check SSL
3361     if (!extension_loaded('openssl')) {
3362       $setup->errors[] =
3363         $this->h5pF->t('Your server does not have SSL enabled. SSL should be enabled to ensure a secure connection with the H5P hub.');
3364       $setup->disable_hub = TRUE;
3365     }
3367     return $setup;
3368   }
3370   /**
3371    * Check that all H5P requirements for the server setup is met.
3372    */
3373   public function checkSetupForRequirements() {
3374     $setup = $this->checkSetupErrorMessage();
3376     $this->h5pF->setOption('hub_is_enabled', !$setup->disable_hub);
3377     if (!empty($setup->errors)) {
3378       foreach ($setup->errors as $err) {
3379         $this->h5pF->setErrorMessage($err);
3380       }
3381     }
3383     if ($setup->disable_hub) {
3384       // Inform how to re-enable hub
3385       $this->h5pF->setErrorMessage(
3386         $this->h5pF->t('H5P hub communication has been disabled because one or more H5P requirements failed.')
3387       );
3388       $this->h5pF->setErrorMessage(
3389         $this->h5pF->t('When you have revised your server setup you may re-enable H5P hub communication in H5P Settings.')
3390       );
3391     }
3392   }
3394   /**
3395    * Return bytes from php_ini string value
3396    *
3397    * @param string $val
3398    *
3399    * @return int|string
3400    */
3401   public static function returnBytes($val) {
3402     $val  = trim($val);
3403     $last = strtolower($val[strlen($val) - 1]);
3404     $bytes = (int) $val;
3406     switch ($last) {
3407       case 'g':
3408         $bytes *= 1024;
3409       case 'm':
3410         $bytes *= 1024;
3411       case 'k':
3412         $bytes *= 1024;
3413     }
3415     return $bytes;
3416   }
3418   /**
3419    * Check if the current user has permission to update and install new
3420    * libraries.
3421    *
3422    * @param bool [$set] Optional, sets the permission
3423    * @return bool
3424    */
3425   public function mayUpdateLibraries($set = null) {
3426     static $can;
3428     if ($set !== null) {
3429       // Use value set
3430       $can = $set;
3431     }
3433     if ($can === null) {
3434       // Ask our framework
3435       $can = $this->h5pF->mayUpdateLibraries();
3436     }
3438     return $can;
3439   }
3441   /**
3442    * Provide localization for the Core JS
3443    * @return array
3444    */
3445   public function getLocalization() {
3446     return array(
3447       'fullscreen' => $this->h5pF->t('Fullscreen'),
3448       'disableFullscreen' => $this->h5pF->t('Disable fullscreen'),
3449       'download' => $this->h5pF->t('Download'),
3450       'copyrights' => $this->h5pF->t('Rights of use'),
3451       'embed' => $this->h5pF->t('Embed'),
3452       'size' => $this->h5pF->t('Size'),
3453       'showAdvanced' => $this->h5pF->t('Show advanced'),
3454       'hideAdvanced' => $this->h5pF->t('Hide advanced'),
3455       'advancedHelp' => $this->h5pF->t('Include this script on your website if you want dynamic sizing of the embedded content:'),
3456       'copyrightInformation' => $this->h5pF->t('Rights of use'),
3457       'close' => $this->h5pF->t('Close'),
3458       'title' => $this->h5pF->t('Title'),
3459       'author' => $this->h5pF->t('Author'),
3460       'year' => $this->h5pF->t('Year'),
3461       'source' => $this->h5pF->t('Source'),
3462       'license' => $this->h5pF->t('License'),
3463       'thumbnail' => $this->h5pF->t('Thumbnail'),
3464       'noCopyrights' => $this->h5pF->t('No copyright information available for this content.'),
3465       'reuse' => $this->h5pF->t('Reuse'),
3466       'reuseContent' => $this->h5pF->t('Reuse Content'),
3467       'reuseDescription' => $this->h5pF->t('Reuse this content.'),
3468       'downloadDescription' => $this->h5pF->t('Download this content as a H5P file.'),
3469       'copyrightsDescription' => $this->h5pF->t('View copyright information for this content.'),
3470       'embedDescription' => $this->h5pF->t('View the embed code for this content.'),
3471       'h5pDescription' => $this->h5pF->t('Visit H5P.org to check out more cool content.'),
3472       'contentChanged' => $this->h5pF->t('This content has changed since you last used it.'),
3473       'startingOver' => $this->h5pF->t("You'll be starting over."),
3474       'by' => $this->h5pF->t('by'),
3475       'showMore' => $this->h5pF->t('Show more'),
3476       'showLess' => $this->h5pF->t('Show less'),
3477       'subLevel' => $this->h5pF->t('Sublevel'),
3478       'confirmDialogHeader' => $this->h5pF->t('Confirm action'),
3479       'confirmDialogBody' => $this->h5pF->t('Please confirm that you wish to proceed. This action is not reversible.'),
3480       'cancelLabel' => $this->h5pF->t('Cancel'),
3481       'confirmLabel' => $this->h5pF->t('Confirm'),
3482       'licenseU' => $this->h5pF->t('Undisclosed'),
3483       'licenseCCBY' => $this->h5pF->t('Attribution'),
3484       'licenseCCBYSA' => $this->h5pF->t('Attribution-ShareAlike'),
3485       'licenseCCBYND' => $this->h5pF->t('Attribution-NoDerivs'),
3486       'licenseCCBYNC' => $this->h5pF->t('Attribution-NonCommercial'),
3487       'licenseCCBYNCSA' => $this->h5pF->t('Attribution-NonCommercial-ShareAlike'),
3488       'licenseCCBYNCND' => $this->h5pF->t('Attribution-NonCommercial-NoDerivs'),
3489       'licenseCC40' => $this->h5pF->t('4.0 International'),
3490       'licenseCC30' => $this->h5pF->t('3.0 Unported'),
3491       'licenseCC25' => $this->h5pF->t('2.5 Generic'),
3492       'licenseCC20' => $this->h5pF->t('2.0 Generic'),
3493       'licenseCC10' => $this->h5pF->t('1.0 Generic'),
3494       'licenseGPL' => $this->h5pF->t('General Public License'),
3495       'licenseV3' => $this->h5pF->t('Version 3'),
3496       'licenseV2' => $this->h5pF->t('Version 2'),
3497       'licenseV1' => $this->h5pF->t('Version 1'),
3498       'licensePD' => $this->h5pF->t('Public Domain'),
3499       'licenseCC010' => $this->h5pF->t('CC0 1.0 Universal (CC0 1.0) Public Domain Dedication'),
3500       'licensePDM' => $this->h5pF->t('Public Domain Mark'),
3501       'licenseC' => $this->h5pF->t('Copyright'),
3502       'contentType' => $this->h5pF->t('Content Type'),
3503       'licenseExtras' => $this->h5pF->t('License Extras'),
3504       'changes' => $this->h5pF->t('Changelog'),
3505       'contentCopied' => $this->h5pF->t('Content is copied to the clipboard'),
3506       'connectionLost' => $this->h5pF->t('Connection lost. Results will be stored and sent when you regain connection.'),
3507       'connectionReestablished' => $this->h5pF->t('Connection reestablished.'),
3508       'resubmitScores' => $this->h5pF->t('Attempting to submit stored results.'),
3509       'offlineDialogHeader' => $this->h5pF->t('Your connection to the server was lost'),
3510       'offlineDialogBody' => $this->h5pF->t('We were unable to send information about your completion of this task. Please check your internet connection.'),
3511       'offlineDialogRetryMessage' => $this->h5pF->t('Retrying in :num....'),
3512       'offlineDialogRetryButtonLabel' => $this->h5pF->t('Retry now'),
3513       'offlineSuccessfulSubmit' => $this->h5pF->t('Successfully submitted results.'),
3514     );
3515   }
3518 /**
3519  * Functions for validating basic types from H5P library semantics.
3520  * @property bool allowedStyles
3521  */
3522 class H5PContentValidator {
3523   public $h5pF;
3524   public $h5pC;
3525   private $typeMap, $libraries, $dependencies, $nextWeight;
3526   private static $allowed_styleable_tags = array('span', 'p', 'div','h1','h2','h3', 'td');
3528   /**
3529    * Constructor for the H5PContentValidator
3530    *
3531    * @param object $H5PFramework
3532    *  The frameworks implementation of the H5PFrameworkInterface
3533    * @param object $H5PCore
3534    *  The main H5PCore instance
3535    */
3536   public function __construct($H5PFramework, $H5PCore) {
3537     $this->h5pF = $H5PFramework;
3538     $this->h5pC = $H5PCore;
3539     $this->typeMap = array(
3540       'text' => 'validateText',
3541       'number' => 'validateNumber',
3542       'boolean' => 'validateBoolean',
3543       'list' => 'validateList',
3544       'group' => 'validateGroup',
3545       'file' => 'validateFile',
3546       'image' => 'validateImage',
3547       'video' => 'validateVideo',
3548       'audio' => 'validateAudio',
3549       'select' => 'validateSelect',
3550       'library' => 'validateLibrary',
3551     );
3552     $this->nextWeight = 1;
3554     // Keep track of the libraries we load to avoid loading it multiple times.
3555     $this->libraries = array();
3557     // Keep track of all dependencies for the given content.
3558     $this->dependencies = array();
3559   }
3561   /**
3562    * Add Addon library.
3563    */
3564   public function addon($library) {
3565     $depKey = 'preloaded-' . $library['machineName'];
3566     $this->dependencies[$depKey] = array(
3567       'library' => $library,
3568       'type' => 'preloaded'
3569     );
3570     $this->nextWeight = $this->h5pC->findLibraryDependencies($this->dependencies, $library, $this->nextWeight);
3571     $this->dependencies[$depKey]['weight'] = $this->nextWeight++;
3572   }
3574   /**
3575    * Get the flat dependency tree.
3576    *
3577    * @return array
3578    */
3579   public function getDependencies() {
3580     return $this->dependencies;
3581   }
3583   /**
3584    * Validate metadata
3585    *
3586    * @param array $metadata
3587    * @return array Validated & filtered
3588    */
3589   public function validateMetadata($metadata) {
3590     $semantics = $this->getMetadataSemantics();
3591     $group = (object)$metadata;
3593     // Stop complaining about "invalid selected option in select" for
3594     // old content without license chosen.
3595     if (!isset($group->license)) {
3596       $group->license = 'U';
3597     }
3599     $this->validateGroup($group, (object) array(
3600       'type' => 'group',
3601       'fields' => $semantics,
3602     ), FALSE);
3604     return (array)$group;
3605   }
3607   /**
3608    * Validate given text value against text semantics.
3609    * @param $text
3610    * @param $semantics
3611    */
3612   public function validateText(&$text, $semantics) {
3613     if (!is_string($text)) {
3614       $text = '';
3615     }
3616     if (isset($semantics->tags)) {
3617       // Not testing for empty array allows us to use the 4 defaults without
3618       // specifying them in semantics.
3619       $tags = array_merge(array('div', 'span', 'p', 'br'), $semantics->tags);
3621       // Add related tags for table etc.
3622       if (in_array('table', $tags)) {
3623         $tags = array_merge($tags, array('tr', 'td', 'th', 'colgroup', 'thead', 'tbody', 'tfoot'));
3624       }
3625       if (in_array('b', $tags) && ! in_array('strong', $tags)) {
3626         $tags[] = 'strong';
3627       }
3628       if (in_array('i', $tags) && ! in_array('em', $tags)) {
3629         $tags[] = 'em';
3630       }
3631       if (in_array('ul', $tags) || in_array('ol', $tags) && ! in_array('li', $tags)) {
3632         $tags[] = 'li';
3633       }
3634       if (in_array('del', $tags) || in_array('strike', $tags) && ! in_array('s', $tags)) {
3635         $tags[] = 's';
3636       }
3638       // Determine allowed style tags
3639       $stylePatterns = array();
3640       // All styles must be start to end patterns (^...$)
3641       if (isset($semantics->font)) {
3642         if (isset($semantics->font->size) && $semantics->font->size) {
3643           $stylePatterns[] = '/^font-size: *[0-9.]+(em|px|%) *;?$/i';
3644         }
3645         if (isset($semantics->font->family) && $semantics->font->family) {
3646           $stylePatterns[] = '/^font-family: *[-a-z0-9," ]+;?$/i';
3647         }
3648         if (isset($semantics->font->color) && $semantics->font->color) {
3649           $stylePatterns[] = '/^color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i';
3650         }
3651         if (isset($semantics->font->background) && $semantics->font->background) {
3652           $stylePatterns[] = '/^background-color: *(#[a-f0-9]{3}[a-f0-9]{3}?|rgba?\([0-9, ]+\)) *;?$/i';
3653         }
3654         if (isset($semantics->font->spacing) && $semantics->font->spacing) {
3655           $stylePatterns[] = '/^letter-spacing: *[0-9.]+(em|px|%) *;?$/i';
3656         }
3657         if (isset($semantics->font->height) && $semantics->font->height) {
3658           $stylePatterns[] = '/^line-height: *[0-9.]+(em|px|%|) *;?$/i';
3659         }
3660       }
3662       // Alignment is allowed for all wysiwyg texts
3663       $stylePatterns[] = '/^text-align: *(center|left|right);?$/i';
3665       // Strip invalid HTML tags.
3666       $text = $this->filter_xss($text, $tags, $stylePatterns);
3667     }
3668     else {
3669       // Filter text to plain text.
3670       $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8', FALSE);
3671     }
3673     // Check if string is within allowed length
3674     if (isset($semantics->maxLength)) {
3675       // Moodle: the extension mbstring is optional.
3676       /*
3677       if (!extension_loaded('mbstring')) {
3678         $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
3679       }
3680       */
3681       $text = \core_text::substr($text, 0, $semantics->maxLength);
3682     }
3684     // Check if string is according to optional regexp in semantics
3685     if (!($text === '' && isset($semantics->optional) && $semantics->optional) && isset($semantics->regexp)) {
3686       // Escaping '/' found in patterns, so that it does not break regexp fencing.
3687       $pattern = '/' . str_replace('/', '\\/', $semantics->regexp->pattern) . '/';
3688       $pattern .= isset($semantics->regexp->modifiers) ? $semantics->regexp->modifiers : '';
3689       if (preg_match($pattern, $text) === 0) {
3690         // Note: explicitly ignore return value FALSE, to avoid removing text
3691         // if regexp is invalid...
3692         $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');
3693         $text = '';
3694       }
3695     }
3696   }
3698   /**
3699    * Validates content files
3700    *
3701    * @param string $contentPath
3702    *  The path containing content files to validate.
3703    * @param bool $isLibrary
3704    * @return bool TRUE if all files are valid
3705    * TRUE if all files are valid
3706    * FALSE if one or more files fail validation. Error message should be set accordingly by validator.
3707    */
3708   public function validateContentFiles($contentPath, $isLibrary = FALSE) {
3709     if ($this->h5pC->disableFileCheck === TRUE) {
3710       return TRUE;
3711     }
3713     // Scan content directory for files, recurse into sub directories.
3714     $files = array_diff(scandir($contentPath), array('.','..'));
3715     $valid = TRUE;
3716     $whitelist = $this->h5pF->getWhitelist($isLibrary, H5PCore::$defaultContentWhitelist, H5PCore::$defaultLibraryWhitelistExtras);
3718     $wl_regex = '/\.(' . preg_replace('/ +/i', '|', preg_quote($whitelist)) . ')$/i';
3720     foreach ($files as $file) {
3721       $filePath = $contentPath . '/' . $file;
3722       if (is_dir($filePath)) {
3723         $valid = $this->validateContentFiles($filePath, $isLibrary) && $valid;
3724       }
3725       else {
3726         // Snipped from drupal 6 "file_validate_extensions".  Using own code
3727         // to avoid 1. creating a file-like object just to test for the known
3728         // file name, 2. testing against a returned error array that could
3729         // never be more than 1 element long anyway, 3. recreating the regex
3730         // for every file.
3731         // Moodle: the extension mbstring is optional.
3732         /*
3733         if (!extension_loaded('mbstring')) {
3734           $this->h5pF->setErrorMessage($this->h5pF->t('The mbstring PHP extension is not loaded. H5P need this to function properly'), 'mbstring-unsupported');
3735           $valid = FALSE;
3736         }
3737         */
3738         if (!preg_match($wl_regex, \core_text::strtolower($file))) {
3739           $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');
3740           $valid = FALSE;
3741         }
3742       }
3743     }
3744     return $valid;
3745   }
3747   /**
3748    * Validate given value against number semantics
3749    * @param $number
3750    * @param $semantics
3751    */
3752   public function validateNumber(&$number, $semantics) {
3753     // Validate that $number is indeed a number
3754     if (!is_numeric($number)) {
3755       $number = 0;
3756     }
3757     // Check if number is within valid bounds. Move within bounds if not.
3758     if (isset($semantics->min) && $number < $semantics->min) {
3759       $number = $semantics->min;
3760     }
3761     if (isset($semantics->max) && $number > $semantics->max) {
3762       $number = $semantics->max;
3763     }
3764     // Check if number is within allowed bounds even if step value is set.
3765     if (isset($semantics->step)) {
3766       $testNumber = $number - (isset($semantics->min) ? $semantics->min : 0);
3767       $rest = $testNumber % $semantics->step;
3768       if ($rest !== 0) {
3769         $number -= $rest;
3770       }
3771     }
3772     // Check if number has proper number of decimals.
3773     if (isset($semantics->decimals)) {
3774       $number = round($number, $semantics->decimals);
3775     }
3776   }
3778   /**
3779    * Validate given value against boolean semantics
3780    * @param $bool
3781    * @return bool
3782    */
3783   public function validateBoolean(&$bool) {
3784     return is_bool($bool);
3785   }
3787   /**
3788    * Validate select values
3789    * @param $select
3790    * @param $semantics
3791    */
3792   public function validateSelect(&$select, $semantics) {
3793     $optional = isset($semantics->optional) && $semantics->optional;
3794     $strict = FALSE;
3795     if (isset($semantics->options) && !empty($semantics->options)) {
3796       // We have a strict set of options to choose from.
3797       $strict = TRUE;
3798       $options = array();
3800       foreach ($semantics->options as $option) {
3801         // Support optgroup - just flatten options into one
3802         if (isset($option->type) && $option->type === 'optgroup') {
3803           foreach ($option->options as $suboption) {
3804             $options[$suboption->value] = TRUE;
3805           }
3806         }
3807         elseif (isset($option->value)) {
3808           $options[$option->value] = TRUE;
3809         }
3810       }
3811     }
3813     if (isset($semantics->multiple) && $semantics->multiple) {
3814       // Multi-choice generates array of values. Test each one against valid
3815       // options, if we are strict.  First make sure we are working on an
3816       // array.
3817       if (!is_array($select)) {
3818         $select = array($select);
3819       }
3821       foreach ($select as $key => &$value) {
3822         if ($strict && !$optional && !isset($options[$value])) {
3823           $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in multi-select.'));
3824           unset($select[$key]);
3825         }
3826         else {
3827           $select[$key] = htmlspecialchars($value, ENT_QUOTES, 'UTF-8', FALSE);
3828         }
3829       }
3830     }
3831     else {
3832       // Single mode.  If we get an array in here, we chop off the first
3833       // element and use that instead.
3834       if (is_array($select)) {
3835         $select = $select[0];
3836       }
3838       if ($strict && !$optional && !isset($options[$select])) {
3839         $this->h5pF->setErrorMessage($this->h5pF->t('Invalid selected option in select.'));
3840         $select = $semantics->options[0]->value;
3841       }
3842       $select = htmlspecialchars($select, ENT_QUOTES, 'UTF-8', FALSE);
3843     }
3844   }
3846   /**
3847    * Validate given list value against list semantics.
3848    * Will recurse into validating each item in the list according to the type.
3849    * @param $list
3850    * @param $semantics
3851    */
3852   public function validateList(&$list, $semantics) {
3853     $field = $semantics->field;
3854     $function = $this->typeMap[$field->type];
3856     // Check that list is not longer than allowed length. We do this before
3857     // iterating to avoid unnecessary work.
3858     if (isset($semantics->max)) {
3859       array_splice($list, $semantics->max);
3860     }
3862     if (!is_array($list)) {
3863       $list = array();
3864     }
3866     // Validate each element in list.
3867     foreach ($list as $key => &$value) {
3868       if (!is_int($key)) {
3869         array_splice($list, $key, 1);
3870         continue;
3871       }
3872       $this->$function($value, $field);
3873       if ($value === NULL) {
3874         array_splice($list, $key, 1);
3875       }
3876     }
3878     if (count($list) === 0) {
3879       $list = NULL;
3880     }
3881   }
3883   /**
3884    * Validate a file like object, such as video, image, audio and file.
3885    * @param $file
3886    * @param $semantics
3887    * @param array $typeValidKeys
3888    */
3889   private function _validateFilelike(&$file, $semantics, $typeValidKeys = array()) {
3890     // Do not allow to use files from other content folders.
3891     $matches = array();
3892     if (preg_match($this->h5pC->relativePathRegExp, $file->path, $matches)) {
3893       $file->path = $matches[5];
3894     }
3896     // Remove temporary files suffix
3897     if (substr($file->path, -4, 4) === '#tmp') {
3898       $file->path = substr($file->path, 0, strlen($file->path) - 4);
3899     }
3901     // Make sure path and mime does not have any special chars
3902     $file->path = htmlspecialchars($file->path, ENT_QUOTES, 'UTF-8', FALSE);
3903     if (isset($file->mime)) {
3904       $file->mime = htmlspecialchars($file->mime, ENT_QUOTES, 'UTF-8', FALSE);
3905     }
3907     // Remove attributes that should not exist, they may contain JSON escape
3908     // code.
3909     $validKeys = array_merge(array('path', 'mime', 'copyright'), $typeValidKeys);
3910     if (isset($semantics->extraAttributes)) {
3911       $validKeys = array_merge($validKeys, $semantics->extraAttributes); // TODO: Validate extraAttributes
3912     }
3913     $this->filterParams($file, $validKeys);
3915     if (isset($file->width)) {
3916       $file->width = intval($file->width);
3917     }
3919     if (isset($file->height)) {
3920       $file->height = intval($file->height);
3921     }
3923     if (isset($file->codecs)) {
3924       $file->codecs = htmlspecialchars($file->codecs, ENT_QUOTES, 'UTF-8', FALSE);
3925     }
3927     if (isset($file->bitrate)) {
3928       $file->bitrate = intval($file->bitrate);
3929     }
3931     if (isset($file->quality)) {
3932       if (!is_object($file->quality) || !isset($file->quality->level) || !isset($file->quality->label)) {
3933         unset($file->quality);
3934       }
3935       else {
3936         $this->filterParams($file->quality, array('level', 'label'));
3937         $file->quality->level = intval($file->quality->level);
3938         $file->quality->label = htmlspecialchars($file->quality->label, ENT_QUOTES, 'UTF-8', FALSE);
3939       }
3940     }
3942     if (isset($file->copyright)) {
3943       $this->validateGroup($file->copyright, $this->getCopyrightSemantics());
3944     }
3945   }
3947   /**
3948    * Validate given file data
3949    * @param $file
3950    * @param $semantics
3951    */
3952   public function validateFile(&$file, $semantics) {
3953     $this->_validateFilelike($file, $semantics);
3954   }
3956   /**
3957    * Validate given image data
3958    * @param $image
3959    * @param $semantics
3960    */
3961   public function validateImage(&$image, $semantics) {
3962     $this->_validateFilelike($image, $semantics, array('width', 'height', 'originalImage'));
3963   }
3965   /**
3966    * Validate given video data
3967    * @param $video
3968    * @param $semantics
3969    */
3970   public function validateVideo(&$video, $semantics) {
3971     foreach ($video as &$variant) {
3972       $this->_validateFilelike($variant, $semantics, array('width', 'height', 'codecs', 'quality', 'bitrate'));
3973     }
3974   }
3976   /**
3977    * Validate given audio data
3978    * @param $audio
3979    * @param $semantics
3980    */
3981   public function validateAudio(&$audio, $semantics) {
3982     foreach ($audio as &$variant) {
3983       $this->_validateFilelike($variant, $semantics);
3984     }
3985   }
3987   /**
3988    * Validate given group value against group semantics.
3989    * Will recurse into validating each group member.
3990    * @param $group
3991    * @param $semantics
3992    * @param bool $flatten
3993    */
3994   public function validateGroup(&$group, $semantics, $flatten = TRUE) {
3995     // Groups with just one field are compressed in the editor to only output
3996     // the child content. (Exemption for fake groups created by
3997     // "validateBySemantics" above)
3998     $function = null;
3999     $field = null;
4001     $isSubContent = isset($semantics->isSubContent) && $semantics->isSubContent === TRUE;
4003     if (count($semantics->fields) == 1 && $flatten && !$isSubContent) {
4004       $field = $semantics->fields[0];
4005       $function = $this->typeMap[$field->type];