Search moodle.org's
Developer Documentation

See Release Notes

  • Bug fixes for general core bugs in 4.3.x will end 7 October 2024 (12 months).
  • Bug fixes for security issues in 4.3.x will end 21 April 2025 (18 months).
  • PHP version: minimum PHP 8.0.0 Note: minimum PHP version has increased since Moodle 4.1. PHP 8.2.x is supported too.

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

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