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