Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 3.11.x will end 14 Nov 2022 (12 months plus 6 months extension).
  • Bug fixes for security issues in 3.11.x will end 13 Nov 2023 (18 months plus 12 months extension).
  • PHP version: minimum PHP 7.3.0 Note: minimum PHP version has increased since Moodle 3.10. PHP 7.4.x is supported too.

Differences Between: [Versions 311 and 401] [Versions 311 and 402] [Versions 311 and 403] [Versions 39 and 311]

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