Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 3.9.x will end* 10 May 2021 (12 months).
  • Bug fixes for security issues in 3.9.x will end* 8 May 2023 (36 months).
  • PHP version: minimum PHP 7.2.0 Note: minimum PHP version has increased since Moodle 3.8. PHP 7.3.x and 7.4.x are supported too.

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

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