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. } }