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