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.
 
 
 
 
 
 

744 lines
25 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$
use Tiki\FileGallery\File as TikiFile;
class H5PLib
{
/**
* Lib version, used for cache-busting of style and script file references.
* Keeping track of the DB version.
*
* @since 1.0.0
* @var string
*/
const VERSION = '1.6.0';
private $H5PTiki = null;
private static $settings = null;
/**
* H5PLib constructor.
* @throws Exception
*/
public function __construct()
{
if (! extension_loaded('zip')) {
throw new Exception(tr('PHP Zip extension not found, reqiuired for Feature H5P'));
}
$this->H5PTiki = \H5P_H5PTiki::get_h5p_instance('interface');
}
public function __destruct()
{
}
/**
* Triggered by the tiki.file.create event from filegallib
*
* @param array $args containing:
* - 'type' => 'file'
* - 'object' => $fileId
* - 'user' => $GLOBALS['user']
* - 'galleryId' => $galleryId
* - 'filetype' => $type
*
*/
public function handle_fileCreation($args)
{
if (! $this->H5PTiki->isSaving && $metadata = $this->getRequestMetadata($args)) {
$validator = H5P_H5PTiki::get_h5p_instance('validator');
if ($validator->isValidPackage()) {
$storage = H5P_H5PTiki::get_h5p_instance('storage');
$storage->savePackage(null, $args['object']);
} else {
// TODO: What to do if the file isn't a valid H5P? Seems a bit drastic to delete the file – but then again, why would we host broken files?
// @unlink($interface->getUploadedH5pPath());
Feedback::error(Feedback::get());
}
}
}
/**
* Triggered by the tiki.file.update event from filegallib
*
* @param array $args containing:
* - 'type' => 'file'
* - 'object' => $fileId
* - 'user' => $GLOBALS['user']
* - 'galleryId' => $galleryId
* - 'filetype' => $type
*
*/
public function handle_fileUpdate($args)
{
if (! $this->H5PTiki->isSaving && isset($args['object']) && $metadata = $this->getRequestMetadata($args)) {
$validator = H5P_H5PTiki::get_h5p_instance('validator');
if ($validator->isValidPackage()) {
$content = $this->loadContentFromFileId($args['object']);
$storage = H5P_H5PTiki::get_h5p_instance('storage');
$storage->savePackage($content, $args['object']);
}
}
}
/**
* Triggered by the tiki.file.delete event from filegallib
*
* @param array $args containing:
* - 'type' => 'file'
* - 'object' => $fileId
* - 'user' => $GLOBALS['user']
* - 'galleryId' => $galleryId
* - 'filetype' => $type
*
*/
public function handle_fileDelete($args)
{
if (isset($args['object']) && $args['type'] === 'file') {
$id = $this->getContentIdFromFileId($args['object']);
if ($id) {
// Remove the h5p contents
$this->H5PTiki->deleteContentData($id);
}
}
}
/**
* Get H5P content row from the Tiki fileId
*
* @param int $fileId
*
* @return array|bool
*/
public function loadContentFromFileId($fileId)
{
global $prefs;
$id = $this->getContentIdFromFileId($fileId);
if ($id) {// Try to find content with $id.
$core = \H5P_H5PTiki::get_h5p_instance('core');
$content = $core->loadContent($id); // TODO: Is it possible to pass $fileId directly here to reduce the number of queries?
if (is_array($content) && ! empty($content)) {
// no error
$content['language'] = substr($prefs['language'], 0, 2); // TODO better
}
return $content;
} else {
return false;
}
}
/**
* @param int $fileId
* @return bool|mixed
*/
public function getContentIdFromFileId($fileId)
{
$tiki_h5p_contents = TikiDb::get()->table('tiki_h5p_contents');
return $tiki_h5p_contents->fetchOne('id', ['file_id' => $fileId]);
}
private function getRequestMetadata($args)
{
$metadata = null;
if ($this->isZipFile($args) && $zip = $this->getZipFile($args['object'])) {
if ($manifest = $this->getH5PManifest($zip)) {
$metadata = $this->getMetadata($manifest);
}
$zip->close();
}
return $metadata;
}
private function isZipFile($args)
{
if (! isset($args['filetype'])) {
return false;
}
return in_array($args['filetype'], ['application/zip', 'application/x-zip', 'application/x-zip-compressed']);
}
private function getZipFile($fileId)
{
global $prefs, $tikipath;
if (! class_exists('ZipArchive')) {
Feedback::error(tra('PHP Class "ZipArchive" not found'));
}
$file = TikiFile::id($fileId);
// make a copy of the h5p file for the validator to unpack (and eventually delete)
$dest = $tikipath . 'temp/' . $file->filename;
file_put_contents($dest, $file->getContents());
/** @var ZipArchive $zip */
$zip = new ZipArchive();
$interface = H5P_H5PTiki::get_h5p_instance('interface');
$filepath = $interface->getUploadedH5pPath($dest);
if ($zip->open($filepath) === true) {
return $zip;
}
}
/**
* @param ZipArchive $zip
* @return mixed
*/
private function getH5PManifest($zip)
{
return $zip->getFromName('h5p.json');
}
private function getMetadata($manifest)
{
return json_decode($manifest, false);
}
/**
* Include settings and assets for the given content.
*
* @param array $content
* @param boolean $no_cache
* @return string Embed code
*/
public function addAssets($content, $no_cache = false)
{
// Add core assets
$this->addCoreAssets();
// Detemine embed type
$embed = H5PCore::determineEmbedType($content['embedType'], $content['library']['embedTypes']);
// Make sure content isn't added twice
$cid = 'cid-' . $content['id'];
if (! isset(self::$settings['contents'][$cid])) {
self::$settings['contents'][$cid] = $this->getContentSettings($content);
$core = \H5P_H5PTiki::get_h5p_instance('core');
// Get assets for this content
$preloaded_dependencies = $core->loadContentDependencies($content['id'], 'preloaded');
$files = $core->getDependenciesFiles($preloaded_dependencies);
$this->alter_assets($files, $preloaded_dependencies, $embed);
if ($embed === 'div') {
$this->enqueueAssets($files);
} elseif ($embed === 'iframe') {
self::$settings['contents'][$cid]['scripts'] = $core->getAssetsUrls($files['scripts']);
self::$settings['contents'][$cid]['styles'] = $core->getAssetsUrls($files['styles']);
}
}
// Tiki JB note: I had to add this here to get the js files to be included,
// the WP plugin doesn't use this here so i must be missing something else...
$this->printSettings(self::$settings);
if ($embed === 'div') {
return '<div class="h5p-content" data-content-id="' . $content['id'] . '"></div>';
} else {
return '<div class="h5p-iframe-wrapper"><iframe id="h5p-iframe-' . $content['id'] . '" class="h5p-iframe" data-content-id="' . $content['id'] . '" style="height:1px" src="about:blank" frameBorder="0" scrolling="no"></iframe></div>';
}
}
/**
* Add the js and css files specified in the prefs
*
* @param array $dependencies
* @param array $files scripts & styles
* @param string $embed type
*/
public function alter_assets(&$files, &$dependencies, $embed)
{
global $prefs, $base_url;
// refactor dependency list
$libraries = [];
foreach ($dependencies as $dependency) {
$libraries[$dependency['machineName']] = [
'majorVersion' => $dependency['majorVersion'],
'minorVersion' => $dependency['minorVersion'],
];
}
$prefsMap = [
'h5p_custom_js_files' => 'scripts',
'h5p_custom_css_files' => 'styles',
];
foreach ($prefsMap as $pref => $type) {
if (! empty($prefs[$pref])) {
$additions = explode("\n", $prefs[$pref]);
foreach ($additions as & $line) {
$line = explode(',', $line);
}
foreach ($additions as $addition) {
if ($addition[0] === '*' || isset($libraries[$addition[0]])) {
$version = $addition[2] ?? '1.0.0';
$filepath = $addition[1];
if (strpos(parse_url($filepath, PHP_URL_PATH), '/') !== 0) {
// relative path needs to be absolute to tiki base
$filepath = $base_url . $filepath;
}
$files[$type][] = (object) [
'path' => $filepath,
'version' => "?ver=$version", // Cache buster
];
}
}
}
}
}
/**
* Set core JavaScript settings and add core assets.
*/
public function addCoreAssets()
{
if (! empty(self::$settings)) {
return; // Already added
}
self::$settings = $this->getCoreSettings();
self::$settings['core'] = [
'styles' => [],
'scripts' => [],
];
self::$settings['loadedJs'] = [];
self::$settings['loadedCss'] = [];
$TWV = new TWVersion();
$cache_buster = '?ver=' . $TWV->version;
$lib_url = 'vendor_bundled/vendor/h5p/h5p-core/';
// Add core stylesheets
foreach (H5PCore::$styles as $style) {
self::$settings['core']['styles'][] = $lib_url . $style . $cache_buster;
TikiLib::lib('header')->add_cssfile($lib_url . $style);
}
// Add core JavaScript
foreach (H5PCore::$scripts as $script) {
self::$settings['core']['scripts'][] = $lib_url . $script . $cache_buster;
TikiLib::lib('header')->add_jsfile($lib_url . $script);
}
}
/**
* Get generic h5p settings
*/
public function getCoreSettings()
{
global $user, $base_url, $prefs;
$userId = TikiLib::lib('tiki')->get_user_id($user);
$core = H5P_H5PTiki::get_h5p_instance('core');
$h5p = H5P_H5PTiki::get_h5p_instance('interface');
$settings = [
'baseUrl' => $base_url,
'url' => $base_url . \H5P_H5PTiki::$h5p_path,
'postUserStatistics' => ($prefs['h5p_track_user'] === 'y') && $userId,
'ajax' => [
'setFinished' => 'tiki-ajax_services.php?controller=h5p&action=results',
'contentUserData' => 'tiki-ajax_services.php?controller=h5p&action=userdata&contentId=:contentId&dataType=:dataType&subContentId=:subContentId',
],
'saveFreq' => $prefs['h5p_save_content_state'] === 'y' ? $prefs['h5p_save_content_frequency'] : false,
'siteUrl' => $base_url,
'l10n' => [
'H5P' => $core->getLocalization(),
],
'hubIsEnabled' => $prefs['h5p_hub_is_enabled'] === 'y',
'reportingIsEnabled' => $prefs['h5p_enable_lrs_content_types'] === 'y',
'libraryConfig' => $h5p->getLibraryConfig(),
'crossorigin' => defined('H5P_CROSSORIGIN') ? H5P_CROSSORIGIN : null,
];
if ($userId) {
$settings['user'] = [
'name' => $user,
//'mail' => $userId->user_email, // TODO: Used in xAPI statements to uniquely identify the user across systems, i.e. if an LRS is used.
];
}
return $settings;
}
/**
* Enqueue assets for content embedded by div.
*
* @param array $assets
*/
public function enqueueAssets(&$assets)
{
$rel_url = \H5P_H5PTiki::$h5p_path;
foreach ($assets['scripts'] as $script) {
$url = $rel_url . $script->path . $script->version;
if (! in_array($url, self::$settings['loadedJs'])) {
self::$settings['loadedJs'][] = $url;
TikiLib::lib('header')->add_jsfile($rel_url . $script->path);
}
}
foreach ($assets['styles'] as $style) {
$url = $rel_url . $style->path . $style->version;
if (! in_array($url, self::$settings['loadedCss'])) {
self::$settings['loadedCss'][] = $url;
TikiLib::lib('header')->add_cssfile($rel_url . $style->path);
}
}
}
/**
* Add H5P JavaScript settings to the bottom of the page.
*/
public function addSettings()
{
if (self::$settings !== null) {
$this->printSettings(self::$settings);
}
}
/**
* JSON encode and print the given H5P JavaScript settings.
*
* @param array $settings
*/
public function printSettings($settings, $obj_name = 'H5PIntegration')
{
$json_settings = json_encode($settings);
if ($json_settings !== false) {
$headerlib = TikiLib::lib('header');
if (! empty($headerlib->js['5h5p'])) {
$headerlib->js['5h5p'] = []; // clear previous plugins js as they are accumulative
}
$headerlib->add_js('var ' . $obj_name . ' = ' . $json_settings . ";\n", '5h5p');
}
}
/**
* Get added JavaScript settings.
*
* @return array
*/
public function getSettings()
{
return self::$settings;
}
/**
* Get settings for given content
*
* @since 1.5.0
* @param array $content
* @return array
*/
public function getContentSettings($content)
{
global $prefs, $user;
$core = \H5P_H5PTiki::get_h5p_instance('core');
// Add global disable settings - odd, not found?
//$content['disable'] |= $core->getGlobalDisable();
$safe_parameters = $core->filterParameters($content);
/* if (has_action('h5p_alter_filtered_parameters')) {
// Parse the JSON parameters
$decoded_parameters = json_decode($safe_parameters);
/**
* Allows you to alter the H5P content parameters after they have been
* filtered. This hook only fires before view.
*
* @since 1.5.3
*
* @param object &$parameters
* @param string $libraryName
* @param int $libraryMajorVersion
* @param int $libraryMinorVersion
* /
do_action_ref_array('h5p_alter_filtered_parameters', array(&$decoded_parameters, $content['library']['name'], $content['library']['majorVersion'], $content['library']['minorVersion']));
// Stringify the JSON parameters
$safe_parameters = json_encode($decoded_parameters);
}
*/
$smarty = TikiLib::lib('smarty');
$smarty->loadPlugin('smarty_function_service');
$embedUrl = smarty_function_service([
'controller' => 'h5p',
'action' => 'embed',
'fileId' => $content['file_id'],
], $smarty->getEmptyInternalTemplate());
// Getting author's user id
$userId = TikiLib::lib('tiki')->get_user_id($user);
$author_id = (is_array($content) ? $content['user_id'] : $userId);
// Add JavaScript settings for this content
$settings = [
'library' => H5PCore::libraryToString($content['library']),
'jsonContent' => $safe_parameters,
'fullScreen' => $content['library']['fullscreen'],
'exportUrl' => ($prefs['h5p_export'] === 'y' ? 'tiki-download_file.php?fileId=' . $content['file_id'] : ''),
'embedCode' => '<iframe src="' . $embedUrl . '" width=":w" height=":h" frameborder="0" allowfullscreen="allowfullscreen"></iframe>',
'resizeCode' => '<script src="vendor_bundled/vendor/h5p/h5p-core/js/h5p-resizer.js" charset="UTF-8"></script>',
'url' => $embedUrl,
'title' => $content['title'],
'displayOptions' => $core->getDisplayOptionsForView($content['disable'], $author_id),
'metadata' => $content['metadata'],
'contentUserData' => [
0 => [
'state' => '{}',
],
],
];
// Get preloaded user data for the current user
if ($prefs['h5p_save_content_state'] === 'y' && $userId) {
$results = json_decode(TikiLib::lib('user')->get_user_preference($user, "h5p_content_{$content['id']}"), true);
if (! empty($results['preload'])) {
$settings['contentUserData'][$results['subContentId']][$results['dataType']] = json_encode($results['data']);
}
}
return $settings;
}
/**
* Add assets and JavaScript settings for the editor.
*
* @param int $id optional content identifier
*/
public function addEditorAssets($id = null)
{
global $tikiroot, $tikipath, $prefs;
// Add core assets
$this->addCoreAssets();
// Use jQuery and styles from core.
$assets = [
'css' => self::$settings['core']['styles'],
'js' => self::$settings['core']['scripts']
];
// Use relative URL to support both http and https.
$editorpath = 'vendor_bundled/vendor/h5p/h5p-editor/';
$url = $tikiroot . $editorpath;
// Make sure files are reloaded for new versions
$TWV = new TWVersion();
$cachebuster = '?ver=' . $TWV->version;
// Add editor styles
foreach (H5peditor::$styles as $style) {
$assets['css'][] = $url . $style . $cachebuster;
}
// Add editor JavaScript
foreach (H5peditor::$scripts as $script) {
// We do not want the creator of the iframe inside the iframe
if ($script !== 'scripts/h5peditor-editor.js') {
$assets['js'][] = $url . $script . $cachebuster;
}
}
// Add JavaScript with library framework integration (editor part)
TikiLib::lib('header')->add_jsfile($editorpath . 'scripts/h5peditor-editor.js');
TikiLib::lib('header')->add_jsfile('lib/core/H5P/editor.js');
// Add translation
$languagescript = $editorpath . 'language/' . substr($prefs['language'], 0, 2) . '.js';
if (! file_exists($tikipath . $languagescript)) {
$languagescript = $editorpath . 'language/en.js';
}
TikiLib::lib('header')->add_jsfile($languagescript);
// needs to be non-sefurl version so h5p can append the action and params
$ajaxPath = 'tiki-ajax_services.php?controller=h5p&action=';
// Add JavaScript settings
$contentvalidator = \H5P_H5PTiki::get_h5p_instance('contentvalidator');
self::$settings['editor'] = [
'filesPath' => $tikiroot . \H5P_H5PTiki::$h5p_path . '/editor',
'fileIcon' => [
'path' => $url . 'images/binary-file.png',
'width' => 50,
'height' => 50,
],
'ajaxPath' => $ajaxPath,
'libraryUrl' => $url,
'copyrightSemantics' => $contentvalidator->getCopyrightSemantics(),
'metadataSemantics' => $contentvalidator->getMetadataSemantics(),
'assets' => $assets,
'apiVersion' => H5PCore::$coreApi,
];
if ($id !== null) {
self::$settings['editor']['nodeVersionId'] = $id;
}
$this->printSettings(self::$settings);
}
/**
* Add assets and JavaScript settings for the editor.
* @param array $content H5P content
* @param JitFilter $input
* @return bool|int
*/
public function saveContent(&$content, $input)
{
$core = \H5P_H5PTiki::get_h5p_instance('core');
$oldLibrary = empty($content['library']) ? null : $content['library'];
$oldParams = empty($content['params']) ? null : $content['params'];
// Check parameters input
$content['params'] = $input->parameters->xss();
if (empty($content['params'])) {
$core->h5pF->setErrorMessage(tr('Missing content parameters.'));
return false;
}
// Decode parameters input
$params = json_decode($content['params']);
if ($params === null) {
$core->h5pF->setErrorMessage(tr('Invalid content parameters.'));
return false;
}
$content['params'] = json_encode($params->params);
$content['metadata'] = json_encode($params->metadata);
// Trim title and check length
$content['title'] = $input->title->text();
$trimmed_title = empty($content['title']) ? '' : trim($content['title']);
if ($trimmed_title === '') {
$core->h5pF->setErrorMessage(tr('Missing title.'));
return false;
}
if (strlen($trimmed_title) > 255) {
$core->h5pF->setErrorMessage(tr('Title is too long. Must be 256 letters or shorter.'));
return false;
}
$content['title'] = $trimmed_title;
// Get content type chosen in editor
$content['library'] = $core->libraryFromString($input->library->text());
if (! $content['library']) {
$core->h5pF->setErrorMessage(tr('Invalid content type.'));
return false;
}
// Check if content type exists
$content['library']['libraryId'] = $core->h5pF->getLibraryId($content['library']['machineName'], $content['library']['majorVersion'], $content['library']['minorVersion']);
if (! $content['library']['libraryId']) {
$core->h5pF->setErrorMessage(tr("The chosen content type isn't installed."));
return false;
}
// Set disabled features
// TODO: Implement
// create the file gallery file to attach this to
global $prefs, $user;
$fileId = $input->fileId->int();
if (! $fileId) {
// Prevent extracting and inserting the file we're creating
$this->H5PTiki->isSaving = true;
$file = new TikiFile([
'galleryId' => $prefs['h5p_filegal_id'],
'description' => tr('Created by H5P'),
'user' => $user,
]);
$fileId = $file->replace('', 'application/zip', $content['title'], TikiLib::remove_non_word_characters_and_accents($content['title']) . '.h5p');
$this->H5PTiki->isSaving = false;
}
// Save new content
$content['id'] = $core->saveContent($content, $fileId);
// Move images to parmanent storage and find all required content dependencies
$editor = \H5P_EditorTikiStorage::get_h5peditor_instance();
$editor->processParameters($content['id'], $content['library'], $params->params, $oldLibrary, $oldParams);
// export the project into the new file gallery file
$content['file_id'] = $fileId;
$core->filterParameters($content); // rebuild content
// trigger the update event if not creating a new file
if (empty($file)) {
$file = TikiFile::id($fileId);
TikiLib::events()->trigger(
'tiki.file.update',
[
'type' => 'file',
'object' => $fileId,
'user' => $user,
'galleryId' => $file->galleryId,
'filetype' => $file->filetype,
]
);
}
return $fileId;
}
public function removeOldTmpFiles()
{
$older_than = time() - 86400;
// Locate files
$result = TikiDb::get()->query(
'SELECT tf.`path` FROM `tiki_h5p_tmpfiles` tf WHERE tf.`created_at` < ?',
$older_than
);
// Delete files from file system
foreach ($result->result as $file) {
@unlink($file['path']);
}
// Remove from tmpfiles table
TikiDb::get()->query(
'DELETE FROM `tiki_h5p_tmpfiles` WHERE `created_at` < ?',
$older_than
);
}
}