You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

1756 lines
58 KiB

<?php
// (c) Copyright by authors of the Tiki Wiki CMS Groupware Project
//
// All Rights Reserved. See copyright.txt for details and a complete list of authors.
// Licensed under the GNU LESSER GENERAL PUBLIC LICENSE. See license.txt for details.
// $Id$
/**
* Class H5P_H5PTiki
*
* Main wrapper class around the H5P library
*
*/
class H5P_H5PTiki implements H5PFrameworkInterface
{
// properties for table objects
private $tiki_h5p_contents = null;
private $tiki_h5p_contents_libraries = null;
private $tiki_h5p_libraries = null;
private $tiki_h5p_libraries_cachedassets = null;
private $tiki_h5p_libraries_libraries = null;
private $tiki_h5p_libraries_languages = null;
private $tiki_h5p_results = null;
private $tiki_h5p_libraries_hub_cache = null;
public $isSaving = false;
public static $h5p_path;
public function __construct()
{
// Initialise the main H5P Tiki wrapper
$tikiDb = TikiDb::get();
$this->tiki_h5p_contents = $tikiDb->table('tiki_h5p_contents');
$this->tiki_h5p_contents_libraries = $tikiDb->table('tiki_h5p_contents_libraries');
$this->tiki_h5p_libraries = $tikiDb->table('tiki_h5p_libraries');
$this->tiki_h5p_libraries_cachedassets = $tikiDb->table('tiki_h5p_libraries_cachedassets');
$this->tiki_h5p_libraries_libraries = $tikiDb->table('tiki_h5p_libraries_libraries');
$this->tiki_h5p_libraries_languages = $tikiDb->table('tiki_h5p_libraries_languages');
$this->tiki_h5p_results = $tikiDb->table('tiki_h5p_results');
$this->tiki_h5p_libraries_hub_cache = $tikiDb->table('tiki_h5p_libraries_hub_cache');
global $tikidomainslash;
self::$h5p_path = "storage/{$tikidomainslash}public/h5p";
if (! is_writable(self::$h5p_path)) {
\Feedback::error(tr("H5P directory is not writable: %0", self::$h5p_path));
return;
}
foreach (['cachedassets','content','exports','libraries','temp'] as $dir) {
$path = self::$h5p_path . "/" . $dir;
if (! is_dir($path)) {
mkdir($path);
} elseif (! is_writable($path)) {
\Feedback::error(tr("H5P directory is not writable: %0", $path));
}
}
if ($this->getOption('cron_last_run') < time() - 86400 && ! empty($_SERVER['HTTP_HOST'])) {
// Cron not run in >24h, trigger it
// Determine full URL
$cronUrl = (isset($_SERVER['HTTPS']) ? 'https' : 'http') . "://{$_SERVER['HTTP_HOST']}/" .
TikiLib::lib('service')->getUrl(['controller' => 'h5p', 'action' => 'cron']);
// Use token to prevent unauthorized use
$token = $this->getOption('cron_token');
if ($token === null) {
// Create new token
$token = uniqid();
$this->setOption('cron_token', $token);
}
$this->fetchExternalData($cronUrl, ['token' => $token], false);
}
}
/**
* Get the different instances of the core components.
*
* @param string $component
*
* @return \H5PCore|\H5PContentValidator|\H5PExport|\H5PStorage|\H5PValidator|\H5P_H5PTiki
*/
public static function get_h5p_instance($component)
{
static $interface, $core;
global $prefs, $tikiroot, $tikipath;
if (! function_exists('curl_init')) {
throw new Exception(tr('H5P requires the CURL extension to be installed in PHP'));
}
if (is_null($interface)) {
// Setup Core and Interface components that are always needed
$interface = new \H5P_H5PTiki();
$core = new \H5PCore(
$interface,
$tikipath . self::$h5p_path, // Where the extracted content files will be stored
$tikiroot . self::$h5p_path, // URL of the previous option
$prefs['language'], // TODO: Map proper language code from Tiki to H5P langs
true // each time an h5p is saved it exports the reult into the file gallery to keep it up to date
);
// Will combine all JavaScript and all CSS files to reduce the total number of requests
$core->aggregateAssets = true;
}
// Determine which component to return
switch ($component) {
case 'validator':
return new \H5PValidator($interface, $core);
case 'storage':
return new \H5PStorage($interface, $core);
case 'contentvalidator':
return new \H5PContentValidator($interface, $core);
case 'export':
return new \H5PExport($interface, $core);
case 'interface':
return $interface;
case 'core':
return $core;
}
}
/**
* Returns info for the current platform
*
* @return array
* An associative array containing:
* - name: The name of the platform, for instance "Wordpress"
* - version: The version of the platform, for instance "4.0"
* - h5pVersion: The version of the H5P plugin/module
*/
public function getPlatformInfo()
{
$TWV = new TWVersion();
return [
'name' => 'Tiki',
'version' => $TWV->version,
'h5pVersion' => '1.0.0', // TODO: Use variable? (\H5PLib not loaded)
];
}
/**
* Fetches a file from a remote server using HTTP GET
*
* @param $url
* @param $data
* @param bool $blocking
* @param null $stream
*
* @return string The content (response body). null if something went wrong
*/
public function fetchExternalData($url, $data = null, $blocking = true, $stream = null, $fullData = false, $headers = array(), $files = array(), $method = 'POST')
{
$handle = curl_init($url);
if (! empty($data)) {
curl_setopt($handle, CURLOPT_POST, true);
curl_setopt($handle, CURLOPT_POSTFIELDS, $data);
}
curl_setopt($handle, CURLOPT_RETURNTRANSFER, 1);
if (! $blocking) {
curl_setopt($handle, CURLOPT_CONNECTTIMEOUT, 0.01);
}
if (is_string($stream)) {
$filePointer = fopen($stream, "wb");
if ($filePointer == false) {
H5PCore::ajaxError(
tr('Failed to write to temp file %0.', $stream),
'DOWNLOAD_FAILED',
null,
null
);
return false;
}
curl_setopt($handle, CURLOPT_FILE, $filePointer);
}
$response = curl_exec($handle);
curl_close($handle);
if (! $response) {
$error = curl_error($handle);
// Print error?
}
if ($blocking) {
return $response;
}
}
/**
* Set the tutorial URL for a library. All versions of the library is set
*
* @param string $machineName
* @param string $tutorialUrl
*/
public function setLibraryTutorialUrl($machineName, $tutorialUrl)
{
$this->tiki_h5p_libraries->update(
[
'tutorial_url' => $tutorialUrl,
],
['name' => $machineName]
);
}
/**
* Show the user an error message
*
* @param string $message
* The error message
*/
public function setErrorMessage($message, $code = null)
{
if (Perms::get()->h5p_edit) {
// needs 'session' as the method param if the error happens asychronously
Feedback::error(tra($message) . tr(' (code=%0)', $code));
}
}
/**
* Show the user an information message
*
* @param string $message
* The error message
*/
public function setInfoMessage($message)
{
if (Perms::get()->h5p_edit) {
Feedback::success(tra($message));
}
}
/**
* Translation function
*
* @param string $message
* The english string to be translated.
* @param array $replacements
* An associative array of replacements to make after translation. Incidences
* of any key in this array are replaced with the corresponding value. Based
* on the first character of the key, the value is escaped and/or themed:
* - !variable: inserted as is
* - @variable: escape plain text to HTML
* - %variable: escape text and theme as a placeholder for user-submitted
* content
*
* @return string Translated string
* Translated string
*/
public function t($message, $replacements = [])
{
$args = [];
$counter = 0;
foreach ($replacements as $key => $val) {
$args[] = $val;
$message = str_replace($key, "%$counter", $message);
}
return tra($message, '', false, $args);
}
/**
* Get the Path to the last uploaded h5p
*
* @param string $setDir
* Set the dir insted of using an auto generated one.
*
* @return string
* Path to the folder where the last uploaded h5p for this session is located.
*/
public function getUploadedH5pFolderPath($setDir = null)
{
static $dir;
if ($setDir !== null) {
$dir = $setDir;
}
if (is_null($dir)) {
$core = self::get_h5p_instance('core');
$dir = $core->fs->getTmpPath();
}
return $dir;
}
/**
* Get the path to the last uploaded h5p file
*
* @param string $setPath
* Set the path insted of using an auto generated one.
*
* @return string
* Path to the last uploaded h5p
*/
public function getUploadedH5pPath($setPath = null)
{
static $path;
if ($setPath !== null) {
$path = $setPath;
}
if (is_null($path)) {
$core = self::get_h5p_instance('core');
$path = $core->fs->getTmpPath() . '.h5p';
}
return $path;
}
/**
* Get a list of the current installed libraries
*
* @return array
* Associative array containing one entry per machine name.
* For each machineName there is a list of libraries(with different versions)
*/
public function loadLibraries()
{
$res = $this->tiki_h5p_libraries->fetchAll(
['id', 'name', 'title', 'major_version', 'minor_version', 'patch_version', 'runnable', 'restricted'],
[],
-1,
0,
['title' => 'ASC', 'major_version' => 'ASC', 'minor_version' => 'ASC']
);
$libraries = [];
foreach ($res as $library) {
$libraries[$library['name']][] = (object)$library;
}
return $libraries;
}
/**
* Returns the URL to the library admin page
*
* @return string
* URL to admin page
*/
public function getAdminUrl()
{
// TODO: What is this for? This will be needed when the Library Managment page is implemented
return TikiLib::tikiUrl('tiki-admin.php?page=h5p');
}
/**
* Get id to an existing library.
* If version number is not specified, the newest version will be returned.
*
* @param string $machineName
* The librarys machine name
* @param int $majorVersion
* Optional major version number for library
* @param int $minorVersion
* Optional minor version number for library
*
* @return int
* The id of the specified library or FALSE
*/
public function getLibraryId($machineName, $majorVersion = null, $minorVersion = null)
{
$conditions = [
'name' => $machineName,
'major_version' => $majorVersion,
'minor_version' => $minorVersion,
];
$orderby = [];
if ($majorVersion !== null) {
$conditions['major_version'] = $majorVersion;
$orderby[] = ['major_version' => 'desc'];
}
if ($minorVersion !== null) {
$conditions['minor_version'] = $minorVersion;
$orderby[] = ['minor_version' => 'desc'];
}
$orderby[] = ['patch_version' => 'desc'];
return $this->tiki_h5p_libraries->fetchOne(
'id',
$conditions
);
}
/**
* Get file extension whitelist
*
* The default extension list is part of h5p, but admins should be allowed to modify it
*
* @param boolean $isLibrary
* TRUE if this is the whitelist for a library. FALSE if it is the whitelist
* for the content folder we are getting
* @param string $defaultContentWhitelist
* A string of file extensions separated by whitespace
* @param string $defaultLibraryWhitelist
* A string of file extensions separated by whitespace
*
* @return string
*/
public function getWhitelist($isLibrary, $defaultContentWhitelist, $defaultLibraryWhitelist)
{
global $prefs;
return $prefs['h5p_whitelist'] . ($isLibrary ? ' ' . $defaultLibraryWhitelist : '');
}
/**
* Is the library a patched version of an existing library?
*
* @param object $library
* An associative array containing:
* - machineName: The library machineName
* - majorVersion: The librarys majorVersion
* - minorVersion: The librarys minorVersion
* - patchVersion: The librarys patchVersion
*
* @return boolean
* TRUE if the library is a patched version of an existing library
* FALSE otherwise
*/
public function isPatchedLibrary($library)
{
$operator = $this->isInDevMode() ? '<=' : '<';
$result = $this->tiki_h5p_libraries->fetchCount(
[
'name' => $library['machineName'],
'major_version' => $library['majorVersion'],
'minor_version' => $library['minorVersion'],
'patch_version' => $this->tiki_h5p_libraries->expr("$$ $operator ?", [$library['patchVersion']]),
]
);
return ! empty($result);
}
/**
* Is H5P in development mode?
*
* @return boolean
* TRUE if H5P development mode is active
* FALSE otherwise
*/
public function isInDevMode()
{
global $prefs;
return $prefs['h5p_dev_mode'] === 'y';
}
/**
* Is the current user allowed to update libraries?
*
* @return boolean
* TRUE if the user is allowed to update libraries
* FALSE if the user is not allowed to update libraries
*/
public function mayUpdateLibraries()
{
return Perms::get()->h5p_admin; // Do we need a separate perm for update? Or h5p_edit maybe?
}
/**
* Store data about a library
*
* Also fills in the libraryId in the libraryData object if the object is new
*
* @param array $libraryData
* Associative array containing:
* - libraryId: The id of the library if it is an existing library.
* - title: The library's name
* - machineName: The library machineName
* - majorVersion: The library's majorVersion
* - minorVersion: The library's minorVersion
* - patchVersion: The library's patchVersion
* - runnable: 1 if the library is a content type, 0 otherwise
* - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
* - embedTypes(optional): list of supported embed types
* - preloadedJs(optional): list of associative arrays containing:
* - path: path to a js file relative to the library root folder
* - preloadedCss(optional): list of associative arrays containing:
* - path: path to css file relative to the library root folder
* - dropLibraryCss(optional): list of associative arrays containing:
* - machineName: machine name for the librarys that are to drop their css
* - semantics(optional): Json describing the content structure for the library
* - language(optional): associative array containing:
* - languageCode: Translation in json format
* @param bool $new
*/
public function saveLibraryData(&$libraryData, $new = true)
{
$preloadedJs = $this->pathsToCsv($libraryData, 'preloadedJs');
$preloadedCss = $this->pathsToCsv($libraryData, 'preloadedCss');
$dropLibraryCss = '';
if (isset($libraryData['dropLibraryCss'])) {
$libs = [];
foreach ($libraryData['dropLibraryCss'] as $lib) {
$libs[] = $lib['machineName'];
}
$dropLibraryCss = implode(', ', $libs);
}
$embedTypes = '';
if (isset($libraryData['embedTypes'])) {
$embedTypes = implode(', ', $libraryData['embedTypes']);
}
if (! isset($libraryData['semantics'])) {
$libraryData['semantics'] = '';
}
if (! isset($libraryData['fullscreen'])) {
$libraryData['fullscreen'] = 0;
}
if (! isset($library['hasIcon'])) {
$library['hasIcon'] = 0;
}
if ($new) {
$libraryId = $this->tiki_h5p_libraries->insert(
[
'name' => $libraryData['machineName'],
'title' => $libraryData['title'],
'major_version' => $libraryData['majorVersion'],
'minor_version' => $libraryData['minorVersion'],
'patch_version' => $libraryData['patchVersion'],
'runnable' => $libraryData['runnable'],
'fullscreen' => $libraryData['fullscreen'],
'embed_types' => $embedTypes,
'preloaded_js' => $preloadedJs,
'preloaded_css' => $preloadedCss,
'drop_library_css' => $dropLibraryCss,
'semantics' => $libraryData['semantics'],
'tutorial_url' => '',
'has_icon' => $libraryData['hasIcon'] ? 1 : 0,
'metadata_settings' => $libraryData['metadataSettings'],
'add_to' => isset($libraryData['addTo']) ? json_encode($libraryData['addTo']) : null,
]
);
$libraryData['libraryId'] = $libraryId;
} else {
$this->tiki_h5p_libraries->update(
[
'title' => $libraryData['title'],
'patch_version' => $libraryData['patchVersion'],
'runnable' => $libraryData['runnable'],
'fullscreen' => $libraryData['fullscreen'],
'embed_types' => $embedTypes,
'preloaded_js' => $preloadedJs,
'preloaded_css' => $preloadedCss,
'drop_library_css' => $dropLibraryCss,
'semantics' => $libraryData['semantics'],
'has_icon' => $libraryData['hasIcon'] ? 1 : 0,
'metadata_settings' => $libraryData['metadataSettings'],
'add_to' => isset($libraryData['addTo']) ? json_encode($libraryData['addTo']) : null,
],
['id' => $libraryData['libraryId']]
);
$this->deleteLibraryDependencies($libraryData['libraryId']);
}
// Log library successfully installed/upgraded
new H5P_Event(
'library',
($new ? 'create' : 'update'),
null,
null,
$libraryData['machineName'],
$libraryData['majorVersion'] . '.' . $libraryData['minorVersion']
);
$this->tiki_h5p_libraries_languages->deleteMultiple(['library_id' => $libraryData['libraryId']]);
if (isset($libraryData['language'])) {
foreach ($libraryData['language'] as $languageCode => $languageJson) {
$id = $this->tiki_h5p_libraries_languages->insert(
[
'library_id' => $libraryData['libraryId'],
'language_code' => $languageCode,
'translation' => $languageJson
]
);
}
}
}
/**
* Convert list of file paths to csv (from the WP implementation)
*
* @param array $libraryData
* Library data as found in library.json files
* @param string $key
* Key that should be found in $libraryData
*
* @return string
* file paths separated by ', '
*/
private function pathsToCsv($libraryData, $key)
{
if (isset($libraryData[$key])) {
$paths = [];
foreach ($libraryData[$key] as $file) {
$paths[] = $file['path'];
}
return implode(', ', $paths);
}
return '';
}
/**
* Insert new content.
*
* @param array $content
* An associative array containing:
* - id: The content id
* - params: The content in json format
* - library: An associative array containing:
* - libraryId: The id of the main library for this content
* @param int $contentMainId
* Main id for the content if this is a system that supports versions
*
* @return mixed
*/
public function insertContent($content, $contentMainId = null)
{
return $this->updateContent($content, $contentMainId);
}
/**
* Update old content.
*
* @param array $content
* An associative array containing:
* - id: The content id
* - params: The content in json format
* - library: An associative array containing:
* - libraryId: The id of the main library for this content
* @param int $contentMainId
* Main id for the content if this is a system that supports versions
* ** In Tiki this is the fileId **
*
* @return int the content id
*/
public function updateContent($content, $contentMainId = null)
{
global $user;
if (empty($content['title'])) {
$title = TikiLib::lib('filegal')->get_file_label($contentMainId);
} else {
$title = $content['title'];
}
$metadata = (array) $content['metadata'];
$params = json_decode($content['params'], true);
if (isset($params['metadata'])) { // when editing metadata is inside params
$metadata = array_merge($metadata, $params['metadata']);
}
if (isset($params['params'])) { // when editing params are inside params
$content['params'] = json_encode($params['params']);
}
$metadataArray = \H5PMetadata::toDBArray($metadata, true, true, $types);
$data = array_merge(
$metadataArray,
[
'updated_at' => date("Y-m-d H:i:s", TikiLib::lib('tiki')->now),
'title' => $title,
'parameters' => isset($content['params']) ? $content['params'] : '',
'embed_type' => 'div', // TODO: Determine from library?
'library_id' => $content['library']['libraryId'],
'filtered' => '',
'slug' => '',
'disable' => isset($content['disable']) ? $content['disable'] : 0,
'file_id' => $contentMainId,
]
);
if (! isset($content['id'])) {
// Insert new content
$data['created_at'] = $data['updated_at'];
$data['user_id'] = TikiLib::lib('tiki')->get_user_id($user);
$content['id'] = $this->tiki_h5p_contents->insert($data);
$event_type = 'create';
} else {
// Update existing content
$this->tiki_h5p_contents->update(
$data,
['id' => $content['id']]
);
$event_type = 'update';
}
// Log content create/update/upload
if (! empty($content['uploaded'])) {
$event_type .= ' upload';
}
new H5P_Event(
'content',
$event_type,
$content['id'],
$content['title'],
$content['library']['machineName'],
$content['library']['majorVersion'] . '.' . $content['library']['minorVersion']
);
return $content['id'];
}
/**
* Resets marked user data for the given content.
*
* @param int $contentId
*/
public function resetContentUserData($contentId)
{
// TODO: Implement resetContentUserData() method.
}
/**
* Save what libraries a library is depending on
*
* @param int $libraryId
* Library Id for the library we're saving dependencies for
* @param array $dependencies
* List of dependencies as associative arrays containing:
* - machineName: The library machineName
* - majorVersion: The library's majorVersion
* - minorVersion: The library's minorVersion
* @param string $dependencyType
* What type of dependency this is, the following values are allowed:
* - editor
* - preloaded
* - dynamic
*/
public function saveLibraryDependencies($libraryId, $dependencies, $dependencyType)
{
foreach ($dependencies as $dependency) {
$lh = $this->tiki_h5p_libraries->fetchOne(
'id',
[
'name' => $dependency['machineName'],
'major_version' => $dependency['majorVersion'],
'minor_version' => $dependency['minorVersion'],
]
);
$this->tiki_h5p_libraries_libraries->insert(
[
'library_id' => $libraryId,
'required_library_id' => $lh,
'dependency_type' => $dependencyType,
]
);
}
}
/**
* Give an H5P the same library dependencies as a given H5P
*
* @param int $contentId
* Id identifying the content
* @param int $copyFromId
* Id identifying the content to be copied
* @param int $contentMainId
* Main id for the content, typically used in frameworks
* That supports versions. (In this case the content id will typically be
* the version id, and the contentMainId will be the frameworks content id
*/
public function copyLibraryUsage($contentId, $copyFromId, $contentMainId = null)
{
$hcl = $this->tiki_h5p_contents_libraries->fetchRow(
[
'library_id',
'dependency_type',
'weight',
'drop_css',
],
[
'content_id' => $copyFromId,
]
);
$this->tiki_h5p_contents_libraries->insert(
[
'content_id' => $contentId,
'library_id' => $hcl['library_id'],
'dependency_type' => $hcl['dependency_type'],
'weight' => $hcl['weight'],
'drop_css' => $hcl['drop_css'],
]
);
}
/**
* Deletes content data
*
* @param int $contentId
* Id identifying the content
*/
public function deleteContentData($contentId)
{
// Remove content data and library usage
$this->tiki_h5p_contents->delete(['id' => $contentId]);
$this->deleteLibraryUsage($contentId);
// Remove results (really?)
$this->tiki_h5p_results->delete(['content_id' => $contentId]);
$tiki_user_preferences = TikiDb::get()->table('tiki_user_preferences', false);
$tikilib = TikiLib::lib('tiki');
// Remove contents user/usage data
$users = $tiki_user_preferences->fetchColumn(
'user',
['prefName' => "h5p_content_$contentId"]
);
foreach ($users as $u) {
$tikilib->set_user_preference($u, "h5p_content_$contentId", ''); // no delete userpref?
}
// tidy up
$tiki_user_preferences->deleteMultiple(
['prefName' => "h5p_content_$contentId"]
);
}
/**
* Try to connect with H5P.org and look for updates to our libraries.
* Can be disabled through settings
*
*/
public function getLibraryUpdates()
{
global $prefs;
if ($prefs['h5p_hub_is_enabled'] === 'y' || $prefs['h5p_send_usage_statistics'] === 'y') {
$core = self::get_h5p_instance('core');
$core->fetchLibrariesMetadata();
}
}
/**
* Delete what libraries a content item is using
*
* @param int $contentId
* Content Id of the content we'll be deleting library usage for
*/
public function deleteLibraryUsage($contentId)
{
$this->tiki_h5p_contents_libraries->deleteMultiple(['content_id' => $contentId]);
}
/**
* Saves what libraries the content uses
*
* @param int $contentId
* Id identifying the content
* @param array $librariesInUse
* List of libraries the content uses. Libraries consist of associative arrays with:
* - library: Associative array containing:
* - dropLibraryCss(optional): comma-separated list of machineNames
* - machineName: Machine name for the library
* - libraryId: Id of the library
* - type: The dependency type. Allowed values:
* - editor
* - dynamic
* - preloaded
*/
public function saveLibraryUsage($contentId, $librariesInUse)
{
$dropLibraryCssList = [];
foreach ($librariesInUse as $dependency) {
if (! empty($dependency['library']['dropLibraryCss'])) {
$dropLibraryCssList = array_merge($dropLibraryCssList, explode(', ', $dependency['library']['dropLibraryCss']));
}
}
foreach ($librariesInUse as $dependency) {
$dropCss = in_array($dependency['library']['machineName'], $dropLibraryCssList) ? 1 : 0;
$this->tiki_h5p_contents_libraries->insert(
[
'content_id' => $contentId,
'library_id' => $dependency['library']['id'],
'dependency_type' => $dependency['type'],
'drop_css' => $dropCss,
'weight' => $dependency['weight'],
]
);
}
}
/**
* Get number of content/nodes using a library, and the number of
* dependencies to other libraries
*
* @param int $libraryId
* Library identifier
*
* @return array
* Associative array containing:
* - content: Number of content using the library
* - libraries: Number of libraries depending on the library
*/
public function getLibraryUsage($libraryId, $skipContent = false)
{
$usage = [
'libraries' => $this->tiki_h5p_libraries_libraries->fetchCount(['required_library_id' => $libraryId]),
];
if ($skipContent) {
$usage['content'] = -1;
} else {
$usage['content'] = (int)TikiDb::get()->query(
'SELECT COUNT(DISTINCT c.`id`)
FROM `tiki_h5p_libraries` l
JOIN `tiki_h5p_contents_libraries` cl ON l.`id` = cl.`library_id`
JOIN `tiki_h5p_contents` c ON cl.content_id = c.id
WHERE l.id = ?',
$libraryId
);
}
return $usage;
}
/**
* Loads a library
*
* @param string $machineName
* The library's machine name
* @param int $majorVersion
* The library's major version
* @param int $minorVersion
* The library's minor version
*
* @return array|FALSE
* FALSE if the library does not exist.
* Otherwise an associative array containing:
* - libraryId: The id of the library if it is an existing library.
* - title: The library's name
* - machineName: The library machineName
* - majorVersion: The library's majorVersion
* - minorVersion: The library's minorVersion
* - patchVersion: The library's patchVersion
* - runnable: 1 if the library is a content type, 0 otherwise
* - fullscreen(optional): 1 if the library supports fullscreen, 0 otherwise
* - embedTypes(optional): list of supported embed types
* - preloadedJs(optional): comma-separated string with js file paths
* - preloadedCss(optional): comma-separated sting with css file paths
* - dropLibraryCss(optional): list of associative arrays containing:
* - machineName: machine name for the librarys that are to drop their css
* - semantics(optional): Json describing the content structure for the library
* - preloadedDependencies(optional): list of associative arrays containing:
* - machineName: Machine name for a library this library is depending on
* - majorVersion: Major version for a library this library is depending on
* - minorVersion: Minor for a library this library is depending on
* - dynamicDependencies(optional): list of associative arrays containing:
* - machineName: Machine name for a library this library is depending on
* - majorVersion: Major version for a library this library is depending on
* - minorVersion: Minor for a library this library is depending on
* - editorDependencies(optional): list of associative arrays containing:
* - machineName: Machine name for a library this library is depending on
* - majorVersion: Major version for a library this library is depending on
* - minorVersion: Minor for a library this library is depending on
*/
public function loadLibrary($machineName, $majorVersion, $minorVersion)
{
$library = $this->tiki_h5p_libraries->fetchRow(
[
'id',
'name',
'title',
'major_version',
'minor_version',
'patch_version',
'embed_types',
'preloaded_js',
'preloaded_css',
'drop_library_css',
'fullscreen',
'runnable',
'semantics',
'tutorial_url',
'has_icon',
],
[
'name' => $machineName,
'major_version' => $majorVersion,
'minor_version' => $minorVersion,
]
);
if ($library === false) {
return false;
}
$library = H5PCore::snakeToCamel($library);
$library['machineName'] = $library['name'];
$library['libraryId'] = $library['id'];
$result = TikiDb::get()->query(
'SELECT hl.`name`, hl.`major_version` AS major, hl.`minor_version` AS minor, hll.`dependency_type` AS type
FROM `tiki_h5p_libraries_libraries` hll
JOIN `tiki_h5p_libraries` hl ON hll.`required_library_id` = hl.`id`
WHERE hll.`library_id` = ?',
$library['id']
);
foreach ($result->result as $dependency) {
$library[$dependency['type'] . 'Dependencies'][] = [
'machineName' => $dependency['name'],
'majorVersion' => $dependency['major'],
'minorVersion' => $dependency['minor'],
];
}
if ($this->isInDevMode()) {
$semantics = $this->getSemanticsFromFile($library['machineName'], $library['majorVersion'], $library['minorVersion']);
if ($semantics) {
$library['semantics'] = $semantics;
}
}
return $library;
}
private function getSemanticsFromFile($name, $majorVersion, $minorVersion)
{
$semanticsPath = self::$h5p_path . '/libraries/' . $name . '-' . $majorVersion . '.' . $minorVersion . '/semantics.json';
if (file_exists($semanticsPath)) {
$semantics = file_get_contents($semanticsPath);
if (! json_decode($semantics, true)) {
$this->setErrorMessage($this->t('Invalid json in semantics for %library', ['%library' => $name]));
}
return $semantics;
}
return false;
}
/**
* Loads library semantics.
*
* @param string $machineName
* Machine name for the library
* @param int $majorVersion
* The library's major version
* @param int $minorVersion
* The library's minor version
*
* @return string
* The library's semantics as json
*/
public function loadLibrarySemantics($machineName, $majorVersion, $minorVersion)
{
if ($this->isInDevMode()) {
$semantics = $this->getSemanticsFromFile($machineName, $majorVersion, $minorVersion);
} else {
$semantics = $this->tiki_h5p_libraries->fetchOne(
'semantics',
[
'name' => $machineName,
'major_version' => $majorVersion,
'minor_version' => $minorVersion,
]
);
}
return (empty($semantics) ? null : $semantics);
}
/**
* Makes it possible to alter the semantics, adding custom fields, etc.
*
* @param array $semantics
* Associative array representing the semantics
* @param string $machineName
* The library's machine name
* @param int $majorVersion
* The library's major version
* @param int $minorVersion
* The library's minor version
*/
public function alterLibrarySemantics(&$semantics, $machineName, $majorVersion, $minorVersion)
{
// TODO: Implement alterLibrarySemantics() method.
// find an equivalent of do_action_ref_array or drupal_alter('h5p_semantics', $semantics, $name, $majorVersion, $minorVersion);
// Not sure if this will be needed in Tiki.
// I guess it would be implemented as firing a new event that functions may bind to in events.php.
}
/**
* Delete all dependencies belonging to given library
*
* @param int $libraryId
* Library identifier
*/
public function deleteLibraryDependencies($libraryId)
{
$this->tiki_h5p_libraries_libraries->deleteMultiple(['library_id' => $libraryId]);
}
/**
* Start an atomic operation against the dependency storage
*/
public function lockDependencyStorage()
{
TikiDb::get()->query('LOCK TABLES `tiki_h5p_libraries_libraries` write, `tiki_h5p_libraries` as hl read');
}
/**
* Stops an atomic operation against the dependency storage
*/
public function unlockDependencyStorage()
{
TikiDb::get()->query('UNLOCK TABLES');
}
/**
* Delete a library from database and file system
*
* @param stdClass $library
* Library object with id, name, major version and minor version.
*/
public function deleteLibrary($library)
{
/* might be an int according to drupal
$library = $this->tiki_h5p_libraries->fetchRow(
$this->tiki_h5p_libraries->all(),
['library_id' => $libraryId]
);*/
// Delete files
H5PCore::deleteFileTree(
self::$h5p_path . '/libraries/' . $library->machine_name . '-' . $library->major_version . '.' . $library->minor_version
);
// Delete data in database (won't delete content)
$this->tiki_h5p_libraries_libraries->deleteMultiple(['library_id', $library->id]);
$this->tiki_h5p_libraries_languages->deleteMultiple(['library_id', $library->id]);
$this->tiki_h5p_libraries->deleteMultiple(['id', $library->id]);
}
/**
* Load content.
*
* @param int $id
* Content identifier
*
* @return array
* Associative array containing:
* - id: Identifier for the content
* - file_id: Tiki specific fileId (of the original h5p file in the galleries)
* - params: json content as string
* - embedType: csv of embed types
* - title: The contents title
* - language: Language code for the content
* - libraryId: Id for the main library
* - libraryName: The library machine name
* - libraryMajorVersion: The library's majorVersion
* - libraryMinorVersion: The library's minorVersion
* - libraryEmbedTypes: CSV of the main library's embed types
* - libraryFullscreen: 1 if fullscreen is supported. 0 otherwise.
*/
public function loadContent($id)
{
$content = TikiDb::get()->query(
'SELECT hc.id,
hc.file_id,
hc.title,
hc.parameters AS params,
hc.filtered,
hc.slug AS slug,
hc.user_id,
hc.embed_type AS embedType,
hc.disable,
hl.id AS libraryId,
hl.name AS libraryName,
hl.major_version AS libraryMajorVersion,
hl.minor_version AS libraryMinorVersion,
hl.embed_types AS libraryEmbedTypes,
hl.fullscreen AS libraryFullscreen,
hc.authors AS authors,
hc.source AS source,
hc.year_from AS yearFrom,
hc.year_to AS yearTo,
hc.license AS license,
hc.license_version AS licenseVersion,
hc.license_extras AS licenseExtras,
hc.author_comments AS authorComments,
hc.changes AS changes,
hc.default_language AS defaultLanguage,
hc.a11y_title AS a11yTitle
FROM `tiki_h5p_contents` hc
JOIN `tiki_h5p_libraries` hl ON hl.id = hc.library_id
WHERE hc.id =?',
$id
);
$row = $content->fetchRow();
$row['metadata'] = [];
$metadata_structure = [
'title', 'authors', 'source', 'yearFrom', 'yearTo',
'license', 'licenseVersion', 'licenseExtras',
'authorComments', 'changes', 'defaultLanguage', 'a11yTitle',
];
foreach ($metadata_structure as $property) {
if (! empty($row[$property])) {
if ($property === 'authors' || $property === 'changes') {
$row['metadata'][$property] = json_decode($row[$property]);
} else {
$row['metadata'][$property] = $row[$property];
}
if ($property !== 'title') {
unset($row[$property]); // Unset all except title
}
}
}
return $row;
}
/**
* Load dependencies for the given content of the given type.
*
* @param int $id
* Content identifier
* @param int $type
* Dependency types. Allowed values:
* - editor
* - preloaded
* - dynamic
*
* @return array
* List of associative arrays containing:
* - libraryId: The id of the library if it is an existing library.
* - machineName: The library machineName
* - majorVersion: The library's majorVersion
* - minorVersion: The library's minorVersion
* - patchVersion: The library's patchVersion
* - preloadedJs(optional): comma-separated string with js file paths
* - preloadedCss(optional): comma-separated sting with css file paths
* - dropCss(optional): csv of machine names
*/
public function loadContentDependencies($id, $type = null)
{
$query = 'SELECT hl.`id`, hl.`name` AS machineName, hl.`major_version` AS majorVersion, hl.`minor_version` AS minorVersion,
hl.`patch_version` AS patchVersion, hl.`preloaded_css` AS preloadedCss, hl.`preloaded_js` AS preloadedJs,
hcl.`drop_css` AS dropCss, hcl.`dependency_type` AS dependencyType
FROM `tiki_h5p_contents_libraries` hcl
JOIN `tiki_h5p_libraries` hl ON hcl.`library_id` = hl.`id`
WHERE hcl.content_id = ?';
$queryArgs = [$id];
if ($type !== null) {
$query .= " AND hcl.`dependency_type` = ?";
$queryArgs[] = $type;
}
$query .= ' ORDER BY hcl.`weight`';
$result = TikiDb::get()->query($query, $queryArgs);
return $result->result;
}
/**
* Get stored setting.
*
* @param string $name
* Identifier for the setting
* @param string $default
* Optional default value if settings is not set
*
* @return mixed
* Whatever has been stored as the setting
*/
public function getOption($name, $default = null)
{
global $prefs;
$prefName = 'h5p_' . $name;
return isset($prefs[$prefName]) ? $prefs[$prefName] : $default;
}
/**
* Stores the given setting.
* For example when did we last check h5p.org for updates to our libraries.
*
* @param string $name
* Identifier for the setting
* @param mixed $value Data
* Whatever we want to store as the setting
*/
public function setOption($name, $value)
{
TikiLib::lib('tiki')->set_preference('h5p_' . $name, $value);
}
/**
* This will update selected fields on the given content.
*
* @param int $id Content identifier
* @param array $fields Content fields, e.g. filtered or slug.
*/
public function updateContentFields($id, $fields)
{
$processedFields = [];
foreach ($fields as $name => $value) {
$processedFields[self::camelToString($name)] = $value;
}
$this->tiki_h5p_contents->update(
$processedFields,
['id' => $id]
);
}
/**
* Convert variables to fit our DB.
*/
private static function camelToString($input)
{
$input = preg_replace('/[a-z0-9]([A-Z])[a-z0-9]/', '_$1', $input);
return strtolower($input);
}
/**
* Will clear filtered params for all the content that uses the specified
* library. This means that the content dependencies will have to be rebuilt,
* and the parameters re-filtered.
*
* @param array $library_ids
*/
public function clearFilteredParameters($library_ids)
{
$this->tiki_h5p_contents->update(
['filtered' => null],
['library_id' => $this->tiki_h5p_contents->in($library_ids)]
);
}
/**
* Get number of contents that has to get their content dependencies rebuilt
* and parameters re-filtered.
*
* @return int
*/
public function getNumNotFiltered()
{
return $this->tiki_h5p_contents->fetchCount(['filtered' => '']);
}
/**
* Get number of contents using library as main library.
*
* @param int $libraryId
*
* @return int
*/
public function getNumContent($libraryId, $skip = null)
{
return $this->tiki_h5p_contents->fetchCount(['library_id' => $libraryId]);
}
/**
* Determines if content slug is used.
*
* @param string $slug
*
* @return boolean
*/
public function isContentSlugAvailable($slug)
{
return empty($this->tiki_h5p_contents->fetchOne('slug', ['slug' => $slug]));
}
/**
* Generates statistics from the event log per library
*
* @param string $type Type of event to generate stats for
*
* @return array Number values indexed by library name and version
*/
public function getLibraryStats($type)
{
return [];
/*
$count = [];
$tiki_h5p_counters = TikiDb::get()->table('tiki_h5p_counters');
$results = $tiki_h5p_counters->fetchAll(
['library_name', 'library_version', 'num'],
['type' => $type]
);
// Extract results
foreach ($results as $library) {
$count[$library['library_name'] . ' ' . $library['library_version']] = $library['num'];
}
return $count;
*/
}
/**
* Aggregate the current number of H5P authors
*
* @return int
*/
public function getNumAuthors()
{
return $this->tiki_h5p_contents->fetchOne(
$this->tiki_h5p_contents->expr('COUNT(DISTINCT `user_id`)'),
[]
);
}
/**
* Stores hash keys for cached assets, aggregated JavaScripts and
* stylesheets, and connects it to libraries so that we know which cache file
* to delete when a library is updated.
*
* @param string $key
* Hash key for the given libraries
* @param array $libraries
* List of dependencies(libraries) used to create the key
*/
public function saveCachedAssets($key, $libraries)
{
foreach ($libraries as $library) {
$libraryId = isset($library['id']) ? $library['id'] : $library['libraryId'];
if (! $this->tiki_h5p_libraries_cachedassets->fetchCount(['library_id' => $libraryId])) {
$this->tiki_h5p_libraries_cachedassets->insert(
[
'library_id' => $libraryId,
'hash' => $key,
]
);
} else {
$this->tiki_h5p_libraries_cachedassets->update(
['hash' => $key],
['library_id' => $libraryId]
);
}
}
}
/**
* Locate hash keys for given library and delete them.
* Used when cache file are deleted.
*
* @param int $library_id
* Library identifier
*
* @return array
* List of hash keys removed
*/
public function deleteCachedAssets($library_id)
{
// Get all the keys so we can remove the files
$results = $this->tiki_h5p_libraries_cachedassets->fetchAll(
['hash'],
['library_id' => $library_id]
);
// Remove all invalid keys
$hashes = [];
foreach ($results as $row) {
$hashes[] = $row['hash'];
$this->tiki_h5p_libraries_cachedassets->deleteMultiple(
['hash' => $row['hash']]
);
}
return $hashes;
}
/**
* Get the amount of content items associated to a library
* return int
*/
public function getLibraryContentCount()
{
$count = [];
// Find number of content per library
$results = TikiDb::get()->query('
SELECT l.`name`, l.`major_version`, l.`minor_version`, COUNT(*) AS count
FROM `tiki_h5p_contents` c, `tiki_h5p_libraries` l
WHERE c.`library_id` = l.`id`
GROUP BY l.`name`, l.`major_version`, l.`minor_version`');
// Extract results
foreach ($results->result as $library) {
$count[$library['name'] . ' ' . $library['major_version'] . '.' . $library['minor_version']] = $library['count'];
}
return $count;
}
/**
* Will trigger after the export file is created.
*/
public function afterExportCreated($content, $filename)
{
global $prefs, $user;
$exportedFile = H5P_H5PTiki::$h5p_path . '/exports/' . $filename;
if (! file_exists($exportedFile)) {
Feedback::error(tr('Exporting H5P content %0 failed', $content['id']));
}
$this->isSaving = true;
$file = Tiki\FileGallery\File::id($content['file_id']);
$file->replace(file_get_contents($exportedFile), 'application/zip', $content['title'], TikiLib::remove_non_word_characters_and_accents($content['title']) . '.h5p');
$file->setParam('user', $user);
$file->setParam('author', $user);
$result = $file->replace(file_get_contents($exportedFile), 'application/zip', $content['title'], TikiLib::remove_non_word_characters_and_accents($content['title']) . '.h5p');
$this->isSaving = false;
if (! $result) {
Feedback::error(tr('Saving H5P content %0 (fileId %1) failed', $content['id'], $content['file_id']));
}
}
/**
* Check if current user can edit H5P
*
* @method currentUserCanEdit
* @param int $contentUserId
*
* @return boolean
*/
private static function currentUserCanEdit($contentUserId)
{
global $user;
return $user === $contentUserId || Perms::get()->h5p_edit;
}
/**
* Check if user has permissions to an action
*
* @method hasPermission
* @param [H5PPermission] $permission Permission type, ref H5PPermission
* @param [int] $id Id need by platform to determine permission
*
* @return boolean
*/
public function hasPermission($permission, $contentUserId = null)
{
switch ($permission) {
case H5PPermission::DOWNLOAD_H5P:
case H5PPermission::EMBED_H5P:
return self::currentUserCanEdit($contentUserId);
case H5PPermission::CREATE_RESTRICTED:
case H5PPermission::UPDATE_LIBRARIES:
return Perms::get()->h5p_admin;
case H5PPermission::INSTALL_RECOMMENDED:
return Perms::get()->h5p_admin;
}
return false;
}
/**
* Get URL to file in the specific library
*
* @param string $libraryFolderName
* @param string $fileName
*
* @return string URL to file
*/
public function getLibraryFileUrl($libraryFolderName, $fileName)
{
return H5P_H5PTiki::$h5p_path . '/libraries/' . $libraryFolderName . '/' . $fileName;
}
/**
* Replaces existing content type cache with the one passed in
*
* @param object $contentTypeCache Json with an array called 'libraries'
* containing the new content type cache that should replace the old one.
*/
public function replaceContentTypeCache($contentTypeCache)
{
TikiDb::get()->query("TRUNCATE TABLE `tiki_h5p_libraries_hub_cache`");
foreach ($contentTypeCache->contentTypes as $ct) {
// Insert into db
$this->tiki_h5p_libraries_hub_cache->insert(
[
'machine_name' => $ct->id,
'major_version' => $ct->version->major,
'minor_version' => $ct->version->minor,
'patch_version' => $ct->version->patch,
'h5p_major_version' => $ct->coreApiVersionNeeded->major,
'h5p_minor_version' => $ct->coreApiVersionNeeded->minor,
'title' => $ct->title,
'summary' => $ct->summary,
'description' => $ct->description,
'icon' => $ct->icon,
'created_at' => self::dateTimeToTime($ct->createdAt),
'updated_at' => self::dateTimeToTime($ct->updatedAt),
'is_recommended' => $ct->isRecommended === true ? 1 : 0,
'popularity' => $ct->popularity,
'screenshots' => json_encode($ct->screenshots),
'license' => json_encode(isset($ct->license) ? $ct->license : []),
'example' => $ct->example,
'tutorial' => isset($ct->tutorial) ? $ct->tutorial : '',
'keywords' => json_encode(isset($ct->keywords) ? $ct->keywords : []),
'categories' => json_encode(isset($ct->categories) ? $ct->categories : []),
'owner' => $ct->owner,
]
);
}
}
/**
* Convert datetime string to unix timestamp
*
* @param string $datetime
*
* @return int unix timestamp
*/
public static function dateTimeToTime($datetime)
{
$dt = new DateTime($datetime);
return $dt->getTimestamp();
}
/**
* Return messages
*
* @param string $type 'info' or 'error'
*
* @return string[]
*/
public function getMessages($type)
{
// TODO: Implement getMessages() method.
}
/**
* Load addon libraries
*
* @return array
*/
public function loadAddons()
{
// Load addons
// If there are several versions of the same addon, pick the newest one
$result = TikiDb::get()->query(
"SELECT l1.id as libraryId, l1.name as machineName,
l1.major_version as majorVersion, l1.minor_version as minorVersion,
l1.patch_version as patchVersion, l1.add_to as addTo,
l1.preloaded_js as preloadedJs, l1.preloaded_css as preloadedCss
FROM tiki_h5p_libraries AS l1
LEFT JOIN tiki_h5p_libraries AS l2
ON l1.name = l2.name AND
(l1.major_version < l2.major_version OR
(l1.major_version = l2.major_version AND
l1.minor_version < l2.minor_version))
WHERE l1.add_to IS NOT NULL AND l2.name IS NULL"
);
return $result->fetchRow();
}
/**
* Load config for libraries
*
* @param array $libraries
*
* @return array
*/
public function getLibraryConfig($libraries = null)
{
return defined('H5P_LIBRARY_CONFIG') ? H5P_LIBRARY_CONFIG : null;
}
/**
* Checks if the given library has a higher version.
*
* @param array $library
*
* @return boolean
*/
public function libraryHasUpgrade($library)
{
return ! empty(
TikiDb::get()->query(
'SELECT `id` FROM `tiki_h5p_libraries` WHERE `name` = ?
AND (`major_version` > ? OR (`major_version` = ? AND `minor_version` > ?)) LIMIT 1',
[
$library['machineName'],
$library['majorVersion'],
$library['majorVersion'],
$library['minorVersion'],
]
)
);
}
public function replaceContentHubMetadataCache($metadata, $lang)
{
// TODO: Implement replaceContentHubMetadataCache() method.
}
public function getContentHubMetadataCache($lang = 'en')
{
// TODO: Implement getContentHubMetadataCache() method.
}
public function getContentHubMetadataChecked($lang = 'en')
{
// TODO: Implement getContentHubMetadataChecked() method.
}
public function setContentHubMetadataChecked($time, $lang = 'en')
{
// TODO: Implement setContentHubMetadataChecked() method.
}
}