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