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