Search moodle.org's
Developer Documentation

See Release Notes
Long Term Support Release

  • Bug fixes for general core bugs in 4.1.x will end 13 November 2023 (12 months).
  • Bug fixes for security issues in 4.1.x will end 10 November 2025 (36 months).
  • PHP version: minimum PHP 7.4.0 Note: minimum PHP version has increased since Moodle 4.0. PHP 8.0.x is supported too.

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