Search moodle.org's
Developer Documentation

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