Current Path : C:/Users/Mahmood/Desktop/moodle/h5p/classes/ |
Current File : C:/Users/Mahmood/Desktop/moodle/h5p/classes/framework.php |
<?php // This file is part of Moodle - http://moodle.org/ // // Moodle is free software: you can redistribute it and/or modify // it under the terms of the GNU General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // Moodle is distributed in the hope that it will be useful, // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU General Public License for more details. // // You should have received a copy of the GNU General Public License // along with Moodle. If not, see <http://www.gnu.org/licenses/>. /** * \core_h5p\framework class * * @package core_h5p * @copyright 2019 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ namespace core_h5p; defined('MOODLE_INTERNAL') || die(); /** * Moodle's implementation of the H5P framework interface. * * @package core_h5p * @copyright 2019 Mihail Geshoski <mihail@moodle.com> * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class framework implements \H5PFrameworkInterface { /** @var string The path to the last uploaded h5p */ private $lastuploadedfolder; /** @var string The path to the last uploaded h5p file */ private $lastuploadedfile; /** @var stored_file The .h5p file */ private $file; /** * Returns info for the current platform. * Implements getPlatformInfo. * * @return array An associative array containing: * - name: The name of the platform, for instance "Moodle" * - version: The version of the platform, for instance "3.8" * - h5pVersion: The version of the H5P component */ public function getPlatformInfo() { global $CFG; return array( 'name' => 'Moodle', 'version' => $CFG->version, 'h5pVersion' => $CFG->version, ); } /** * Fetches a file from a remote server using HTTP GET. * Implements fetchExternalData. * * @param string $url Where you want to get or send data * @param array $data Data to post to the URL * @param bool $blocking Set to 'FALSE' to instantly time out (fire and forget) * @param string $stream Path to where the file should be saved * @return string The content (response body). NULL if something went wrong */ public function fetchExternalData($url, $data = null, $blocking = true, $stream = null) { if ($stream === null) { // Download file. set_time_limit(0); // Get the extension of the remote file. $parsedurl = parse_url($url); $ext = pathinfo($parsedurl['path'], PATHINFO_EXTENSION); // Generate local tmp file path. $fs = new \core_h5p\file_storage(); $localfolder = $fs->getTmpPath(); $stream = $localfolder; // Add the remote file's extension to the temp file. if ($ext) { $stream .= '.' . $ext; } $this->getUploadedH5pFolderPath($localfolder); $this->getUploadedH5pPath($stream); } $response = download_file_content($url, null, $data, true, 300, 20, false, $stream); if (empty($response->error) && ($response->status != '404')) { return $response->results; } else { $this->setErrorMessage($response->error, 'failed-fetching-external-data'); } } /** * Set the tutorial URL for a library. All versions of the library is set. * Implements setLibraryTutorialUrl. * * @param string $libraryname * @param string $url */ public function setLibraryTutorialUrl($libraryname, $url) { // Tutorial url is currently not being used or stored in libraries. } /** * Set an error message. * Implements setErrorMessage. * * @param string $message The error message * @param string $code An optional code */ public function setErrorMessage($message, $code = null) { if ($message !== null) { $this->set_message('error', $message, $code); } } /** * Set an info message. * Implements setInfoMessage. * * @param string $message The info message */ public function setInfoMessage($message) { if ($message !== null) { $this->set_message('info', $message); } } /** * Return messages. * Implements getMessages. * * @param string $type The message type, e.g. 'info' or 'error' * @return string[] Array of messages */ public function getMessages($type) { global $SESSION; // Return and reset messages. $messages = array(); if (isset($SESSION->core_h5p_messages[$type])) { $messages = $SESSION->core_h5p_messages[$type]; unset($SESSION->core_h5p_messages[$type]); if (empty($SESSION->core_h5p_messages)) { unset($SESSION->core_h5p_messages); } } return $messages; } /** * Translation function. * The purpose of this function is to map the strings used in the core h5p methods * and replace them with the translated ones. If a translation for a particular string * is not available, the default message (key) will be returned. * Implements t. * * @param string $message The english string to be translated * @param array $replacements An associative array of replacements to make after translation * @return string Translated string or the english string if a translation is not available */ public function t($message, $replacements = array()) { // Create mapping. $translationsmap = [ 'The file you uploaded is not a valid HTML5 Package (It does not have the .h5p file extension)' => 'noextension', 'The file you uploaded is not a valid HTML5 Package (We are unable to unzip it)' => 'nounzip', 'The main h5p.json file is not valid' => 'nojson', 'Library directory name must match machineName or machineName-majorVersion.minorVersion (from library.json).' . ' (Directory: %directoryName , machineName: %machineName, majorVersion: %majorVersion, minorVersion:' . ' %minorVersion)' => 'librarydirectoryerror', 'A valid content folder is missing' => 'missingcontentfolder', 'A valid main h5p.json file is missing' => 'invalidmainjson', 'Missing required library @library' => 'missinglibrary', "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload new libraries." . ' Contact the site administrator about this.' => 'missinguploadpermissions', 'Invalid library name: %name' => 'invalidlibraryname', 'Could not find library.json file with valid json format for library %name' => 'missinglibraryjson', 'Invalid semantics.json file has been included in the library %name' => 'invalidsemanticsjson', 'Invalid language file %file in library %library' => 'invalidlanguagefile', 'Invalid language file %languageFile has been included in the library %name' => 'invalidlanguagefile2', 'The file "%file" is missing from library: "%name"' => 'missinglibraryfile', 'The system was unable to install the <em>%component</em> component from the package, it requires a newer' . ' version of the H5P plugin. This site is currently running version %current, whereas the required version' . ' is %required or higher. You should consider upgrading and then try again.' => 'missingcoreversion', "Invalid data provided for %property in %library. Boolean expected." => 'invalidlibrarydataboolean', "Invalid data provided for %property in %library" => 'invalidlibrarydata', "Can't read the property %property in %library" => 'invalidlibraryproperty', 'The required property %property is missing from %library' => 'missinglibraryproperty', 'Illegal option %option in %library' => 'invalidlibraryoption', 'Added %new new H5P library and updated %old old one.' => 'addedandupdatedss', 'Added %new new H5P library and updated %old old ones.' => 'addedandupdatedsp', 'Added %new new H5P libraries and updated %old old one.' => 'addedandupdatedps', 'Added %new new H5P libraries and updated %old old ones.' => 'addedandupdatedpp', 'Added %new new H5P library.' => 'addednewlibrary', 'Added %new new H5P libraries.' => 'addednewlibraries', 'Updated %old H5P library.' => 'updatedlibrary', 'Updated %old H5P libraries.' => 'updatedlibraries', 'Missing dependency @dep required by @lib.' => 'missingdependency', 'Provided string is not valid according to regexp in semantics. (value: "%value", regexp: "%regexp")' => 'invalidstring', 'File "%filename" not allowed. Only files with the following extensions are allowed: %files-allowed.' => 'invalidfile', 'Invalid selected option in multi-select.' => 'invalidmultiselectoption', 'Invalid selected option in select.' => 'invalidselectoption', 'H5P internal error: unknown content type "@type" in semantics. Removing content!' => 'invalidsemanticstype', 'Copyright information' => 'copyrightinfo', 'Title' => 'title', 'Author' => 'author', 'Year(s)' => 'years', 'Year' => 'year', 'Source' => 'source', 'License' => 'license', 'Undisclosed' => 'undisclosed', 'General Public License v3' => 'gpl', 'Public Domain' => 'pd', 'Public Domain Dedication and Licence' => 'pddl', 'Public Domain Mark' => 'pdm', 'Public Domain Mark (PDM)' => 'pdm', 'Copyright' => 'copyrightstring', 'The mbstring PHP extension is not loaded. H5P need this to function properly' => 'missingmbstring', 'The version of the H5P library %machineName used in this content is not valid. Content contains %contentLibrary, ' . 'but it should be %semanticsLibrary.' => 'wrongversion', 'The H5P library %library used in the content is not valid' => 'invalidlibrarynamed', 'Fullscreen' => 'fullscreen', 'Disable fullscreen' => 'disablefullscreen', 'Download' => 'download', 'Rights of use' => 'copyright', 'Embed' => 'embed', 'Size' => 'size', 'Show advanced' => 'showadvanced', 'Hide advanced' => 'hideadvanced', 'Include this script on your website if you want dynamic sizing of the embedded content:' => 'resizescript', 'Close' => 'close', 'Thumbnail' => 'thumbnail', 'No copyright information available for this content.' => 'nocopyright', 'Download this content as a H5P file.' => 'downloadtitle', 'View copyright information for this content.' => 'copyrighttitle', 'View the embed code for this content.' => 'embedtitle', 'Visit H5P.org to check out more cool content.' => 'h5ptitle', 'This content has changed since you last used it.' => 'contentchanged', "You'll be starting over." => 'startingover', 'by' => 'by', 'Show more' => 'showmore', 'Show less' => 'showless', 'Sublevel' => 'sublevel', 'Confirm action' => 'confirmdialogheader', 'Please confirm that you wish to proceed. This action is not reversible.' => 'confirmdialogbody', 'Cancel' => 'cancellabel', 'Confirm' => 'confirmlabel', '4.0 International' => 'licenseCC40', '3.0 Unported' => 'licenseCC30', '2.5 Generic' => 'licenseCC25', '2.0 Generic' => 'licenseCC20', '1.0 Generic' => 'licenseCC10', 'General Public License' => 'licenseGPL', 'Version 3' => 'licenseV3', 'Version 2' => 'licenseV2', 'Version 1' => 'licenseV1', 'CC0 1.0 Universal (CC0 1.0) Public Domain Dedication' => 'licenseCC010', 'CC0 1.0 Universal' => 'licenseCC010U', 'License Version' => 'licenseversion', 'Creative Commons' => 'creativecommons', 'Attribution' => 'ccattribution', 'Attribution (CC BY)' => 'ccattribution', 'Attribution-ShareAlike' => 'ccattributionsa', 'Attribution-ShareAlike (CC BY-SA)' => 'ccattributionsa', 'Attribution-NoDerivs' => 'ccattributionnd', 'Attribution-NoDerivs (CC BY-ND)' => 'ccattributionnd', 'Attribution-NonCommercial' => 'ccattributionnc', 'Attribution-NonCommercial (CC BY-NC)' => 'ccattributionnc', 'Attribution-NonCommercial-ShareAlike' => 'ccattributionncsa', 'Attribution-NonCommercial-ShareAlike (CC BY-NC-SA)' => 'ccattributionncsa', 'Attribution-NonCommercial-NoDerivs' => 'ccattributionncnd', 'Attribution-NonCommercial-NoDerivs (CC BY-NC-ND)' => 'ccattributionncnd', 'Public Domain Dedication (CC0)' => 'ccpdd', 'Years (from)' => 'yearsfrom', 'Years (to)' => 'yearsto', "Author's name" => 'authorname', "Author's role" => 'authorrole', 'Editor' => 'editor', 'Licensee' => 'licensee', 'Originator' => 'originator', 'Any additional information about the license' => 'additionallicenseinfo', 'License Extras' => 'licenseextras', 'Changelog' => 'changelog', 'Content Type' => 'contenttype', 'Date' => 'date', 'Changed by' => 'changedby', 'Description of change' => 'changedescription', 'Photo cropped, text changed, etc.' => 'changeplaceholder', 'Author comments' => 'authorcomments', 'Comments for the editor of the content (This text will not be published as a part of copyright info)' => 'authorcommentsdescription', 'Reuse' => 'reuse', 'Reuse Content' => 'reuseContent', 'Reuse this content.' => 'reuseDescription', 'Content is copied to the clipboard' => 'contentCopied', 'Connection lost. Results will be stored and sent when you regain connection.' => 'connectionLost', 'Connection reestablished.' => 'connectionReestablished', 'Attempting to submit stored results.' => 'resubmitScores', 'Your connection to the server was lost' => 'offlineDialogHeader', 'We were unable to send information about your completion of this task. Please check your internet connection.' => 'offlineDialogBody', 'Retrying in :num....' => 'offlineDialogRetryMessage', 'Retry now' => 'offlineDialogRetryButtonLabel', 'Successfully submitted results.' => 'offlineSuccessfulSubmit', 'One of the files inside the package exceeds the maximum file size allowed. (%file %used > %max)' => 'fileExceedsMaxSize', 'The total size of the unpacked files exceeds the maximum size allowed. (%used > %max)' => 'unpackedFilesExceedsMaxSize', 'Unable to read file from the package: %fileName' => 'couldNotReadFileFromZip', 'Unable to parse JSON from the package: %fileName' => 'couldNotParseJSONFromZip', 'A problem with the server write access was detected. Please make sure that your server can write to your data folder.' => 'nowriteaccess', 'H5P hub communication has been disabled because one or more H5P requirements failed.' => 'hubcommunicationdisabled', 'Site could not be registered with the hub. Please contact your site administrator.' => 'sitecouldnotberegistered', 'The H5P Hub has been disabled until this problem can be resolved. You may still upload libraries through the "H5P Libraries" page.' => 'hubisdisableduploadlibraries', 'When you have revised your server setup you may re-enable H5P hub communication in H5P Settings.' => 'reviseserversetupandretry', 'You have been provided a unique key that identifies you with the Hub when receiving new updates. The key is available for viewing in the "H5P Settings" page.' => 'sitekeyregistered', 'Your PHP max post size is quite small. With your current setup, you may not upload files larger than {$a->%number} MB. This might be a problem when trying to upload H5Ps, images and videos. Please consider to increase it to more than 5MB' => 'maxpostsizetoosmall', 'Your PHP max upload size is bigger than your max post size. This is known to cause issues in some installations.' => 'uploadsizelargerthanpostsize', 'Your PHP max upload size is quite small. With your current setup, you may not upload files larger than {$a->%number} MB. This might be a problem when trying to upload H5Ps, images and videos. Please consider to increase it to more than 5MB.' => 'maxuploadsizetoosmall', 'Your PHP version does not support ZipArchive.' => 'noziparchive', 'Your PHP version is outdated. H5P requires version 5.2 to function properly. Version 5.6 or later is recommended.' => 'oldphpversion', 'Your server does not have SSL enabled. SSL should be enabled to ensure a secure connection with the H5P hub.' => 'sslnotenabled', 'Your site was successfully registered with the H5P Hub.' => 'successfullyregisteredwithhub' ]; if (isset($translationsmap[$message])) { return get_string($translationsmap[$message], 'core_h5p', $replacements); } debugging("String translation cannot be found. Please add a string definition for '" . $message . "' in the core_h5p component.", DEBUG_DEVELOPER); return $message; } /** * Get URL to file in the specifimake_pluginfile_urlc library. * Implements getLibraryFileUrl. * * @param string $libraryfoldername The name or path of the library's folder * @param string $filename The file name * @return string URL to file */ public function getLibraryFileUrl($libraryfoldername, $filename) { global $DB; // Remove unnecessary slashes (first and last, if present) from the path to the folder // of the library file. $libraryfilepath = trim($libraryfoldername, '/'); // Get the folder name of the library from the path. // The first element should represent the folder name of the library. $libfoldername = explode('/', $libraryfilepath)[0]; $factory = new \core_h5p\factory(); $core = $factory->get_core(); // The provided folder name of the library must have a valid format (can be parsed). // The folder name is parsed with a purpose of getting the library related information // such as 'machineName', 'majorVersion' and 'minorVersion'. // This information is later used to retrieve the library ID. if (!$libdata = $core->libraryFromString($libfoldername, true)) { debugging('The provided string value "' . $libfoldername . '" is not a valid name for a library folder.', DEBUG_DEVELOPER); return; } $params = array( 'machinename' => $libdata['machineName'], 'majorversion' => $libdata['majorVersion'], 'minorversion' => $libdata['minorVersion'] ); $libraries = $DB->get_records('h5p_libraries', $params, 'patchversion DESC', 'id', 0, 1); if (!$library = reset($libraries)) { debugging('The library "' . $libfoldername . '" does not exist.', DEBUG_DEVELOPER); return; } $context = \context_system::instance(); return \moodle_url::make_pluginfile_url($context->id, 'core_h5p', 'libraries', $library->id, '/' . $libraryfilepath . '/', $filename)->out(); } /** * Get the Path to the last uploaded h5p. * Implements getUploadedH5PFolderPath. * * @param string $setpath The path to the folder of the last uploaded h5p * @return string Path to the folder where the last uploaded h5p for this session is located */ public function getUploadedH5pFolderPath($setpath = null) { if ($setpath !== null) { $this->lastuploadedfolder = $setpath; } if (!isset($this->lastuploadedfolder)) { throw new \coding_exception('Using getUploadedH5pFolderPath() before path is set'); } return $this->lastuploadedfolder; } /** * Get the path to the last uploaded h5p file. * Implements getUploadedH5PPath. * * @param string $setpath The path to the last uploaded h5p * @return string Path to the last uploaded h5p */ public function getUploadedH5pPath($setpath = null) { if ($setpath !== null) { $this->lastuploadedfile = $setpath; } if (!isset($this->lastuploadedfile)) { throw new \coding_exception('Using getUploadedH5pPath() before path is set'); } return $this->lastuploadedfile; } /** * Load addon libraries. * Implements loadAddons. * * @return array The array containing the addon libraries */ public function loadAddons() { global $DB; $addons = array(); $records = $DB->get_records_sql( "SELECT l1.id AS library_id, l1.machinename AS machine_name, l1.majorversion AS major_version, l1.minorversion AS minor_version, l1.patchversion AS patch_version, l1.addto AS add_to, l1.preloadedjs AS preloaded_js, l1.preloadedcss AS preloaded_css FROM {h5p_libraries} l1 LEFT JOIN {h5p_libraries} l2 ON l1.machinename = l2.machinename AND (l1.majorversion < l2.majorversion OR (l1.majorversion = l2.majorversion AND l1.minorversion < l2.minorversion)) WHERE l1.addto IS NOT NULL AND l2.machinename IS NULL"); // NOTE: These are treated as library objects but are missing the following properties: // title, droplibrarycss, fullscreen, runnable, semantics. // Extract num from records. foreach ($records as $addon) { $addons[] = \H5PCore::snakeToCamel($addon); } return $addons; } /** * Load config for libraries. * Implements getLibraryConfig. * * @param array|null $libraries List of libraries * @return array|null The library config if it exists, null otherwise */ public function getLibraryConfig($libraries = null) { global $CFG; return isset($CFG->core_h5p_library_config) ? $CFG->core_h5p_library_config : null; } /** * Get a list of the current installed libraries. * Implements loadLibraries. * * @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() { global $DB; $results = $DB->get_records('h5p_libraries', [], 'title ASC, majorversion ASC, minorversion ASC', 'id, machinename AS machine_name, majorversion AS major_version, minorversion AS minor_version, patchversion AS patch_version, runnable, title'); $libraries = array(); foreach ($results as $library) { $libraries[$library->machine_name][] = $library; } return $libraries; } /** * Returns the URL to the library admin page. * Implements getAdminUrl. * * @return string URL to admin page */ public function getAdminUrl() { // Not supported. } /** * Return the library's ID. * Implements getLibraryId. * * @param string $machinename The librarys machine name * @param string $majorversion Major version number for library (optional) * @param string $minorversion Minor version number for library (optional) * @return int|bool Identifier, or false if non-existent */ public function getLibraryId($machinename, $majorversion = null, $minorversion = null) { global $DB; $params = array( 'machinename' => $machinename ); if ($majorversion !== null) { $params['majorversion'] = $majorversion; } if ($minorversion !== null) { $params['minorversion'] = $minorversion; } $libraries = $DB->get_records('h5p_libraries', $params, 'majorversion DESC, minorversion DESC, patchversion DESC', 'id', 0, 1); // Get the latest version which matches the input parameters. if ($libraries) { $library = reset($libraries); return $library->id ?? false; } return false; } /** * Get file extension whitelist. * Implements getWhitelist. * * 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 A string containing the allowed file extensions separated by whitespace. */ public function getWhitelist($islibrary, $defaultcontentwhitelist, $defaultlibrarywhitelist) { return $defaultcontentwhitelist . ($islibrary ? ' ' . $defaultlibrarywhitelist : ''); } /** * Is the library a patched version of an existing library? * Implements isPatchedLibrary. * * @param array $library An associative array containing: * - machineName: The library machine name * - majorVersion: The librarys major version * - minorVersion: The librarys minor version * - patchVersion: The librarys patch version * @return boolean TRUE if the library is a patched version of an existing library FALSE otherwise */ public function isPatchedLibrary($library) { global $DB; $sql = "SELECT id FROM {h5p_libraries} WHERE machinename = :machinename AND majorversion = :majorversion AND minorversion = :minorversion AND patchversion < :patchversion"; $library = $DB->get_records_sql( $sql, array( 'machinename' => $library['machineName'], 'majorversion' => $library['majorVersion'], 'minorversion' => $library['minorVersion'], 'patchversion' => $library['patchVersion'] ), 0, 1 ); return !empty($library); } /** * Is H5P in development mode? * Implements isInDevMode. * * @return boolean TRUE if H5P development mode is active FALSE otherwise */ public function isInDevMode() { return false; // Not supported (Files in moodle not editable). } /** * Is the current user allowed to update libraries? * Implements mayUpdateLibraries. * * @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 helper::can_update_library($this->get_file()); } /** * Get the .h5p file. * * @return stored_file The .h5p file. */ public function get_file(): \stored_file { if (!isset($this->file)) { throw new \coding_exception('Using get_file() before file is set'); } return $this->file; } /** * Set the .h5p file. * * @param stored_file $file The .h5p file. */ public function set_file(\stored_file $file): void { $this->file = $file; } /** * Store data about a library. * Implements saveLibraryData. * * 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: 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 * @param bool $new Whether it is a new or existing library. */ public function saveLibraryData(&$librarydata, $new = true) { global $DB; // Some special properties needs some checking and converting before they can be saved. $preloadedjs = $this->library_parameter_values_to_csv($librarydata, 'preloadedJs', 'path'); $preloadedcss = $this->library_parameter_values_to_csv($librarydata, 'preloadedCss', 'path'); $droplibrarycss = $this->library_parameter_values_to_csv($librarydata, 'dropLibraryCss', 'machineName'); if (!isset($librarydata['semantics'])) { $librarydata['semantics'] = ''; } if (!isset($librarydata['fullscreen'])) { $librarydata['fullscreen'] = 0; } $embedtypes = ''; if (isset($librarydata['embedTypes'])) { $embedtypes = implode(', ', $librarydata['embedTypes']); } $library = (object) array( 'title' => $librarydata['title'], 'machinename' => $librarydata['machineName'], 'majorversion' => $librarydata['majorVersion'], 'minorversion' => $librarydata['minorVersion'], 'patchversion' => $librarydata['patchVersion'], 'runnable' => $librarydata['runnable'], 'fullscreen' => $librarydata['fullscreen'], 'embedtypes' => $embedtypes, 'preloadedjs' => $preloadedjs, 'preloadedcss' => $preloadedcss, 'droplibrarycss' => $droplibrarycss, 'semantics' => $librarydata['semantics'], 'addto' => isset($librarydata['addTo']) ? json_encode($librarydata['addTo']) : null, 'coremajor' => isset($librarydata['coreApi']['majorVersion']) ? $librarydata['coreApi']['majorVersion'] : null, 'coreminor' => isset($librarydata['coreApi']['majorVersion']) ? $librarydata['coreApi']['minorVersion'] : null, ); if ($new) { // Create new library and keep track of id. $library->id = $DB->insert_record('h5p_libraries', $library); $librarydata['libraryId'] = $library->id; } else { // Update library data. $library->id = $librarydata['libraryId']; // Save library data. $DB->update_record('h5p_libraries', $library); // Remove old dependencies. $this->deleteLibraryDependencies($librarydata['libraryId']); } } /** * Insert new content. * Implements insertContent. * * @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 * - disable: H5P Button display options * - pathnamehash: The pathnamehash linking the record with the entry in the mdl_files table * - contenthash: The contenthash linking the record with the entry in the mdl_files table * @param int $contentmainid Main id for the content if this is a system that supports versions * @return int The ID of the newly inserted content */ public function insertContent($content, $contentmainid = null) { return $this->updateContent($content); } /** * Update old content or insert new content. * Implements updateContent. * * @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 * - disable: H5P Button display options * - pathnamehash: The pathnamehash linking the record with the entry in the mdl_files table * - contenthash: The contenthash linking the record with the entry in the mdl_files table * @param int $contentmainid Main id for the content if this is a system that supports versions * @return int The ID of the newly inserted or updated content */ public function updateContent($content, $contentmainid = null) { global $DB; if (!isset($content['pathnamehash'])) { $content['pathnamehash'] = ''; } if (!isset($content['contenthash'])) { $content['contenthash'] = ''; } // If the libraryid declared in the package is empty, get the latest version. if (empty($content['library']['libraryId'])) { $mainlibrary = $this->get_latest_library_version($content['library']['machineName']); if (empty($mainlibrary)) { // Raise an error if the main library is not defined and the latest version doesn't exist. $message = $this->t('Missing required library @library', ['@library' => $content['library']['machineName']]); $this->setErrorMessage($message, 'missing-required-library'); return false; } $content['library']['libraryId'] = $mainlibrary->id; } $content['disable'] = $content['disable'] ?? null; // Add title to 'params' to use in the editor. if (!empty($content['title'])) { $params = json_decode($content['params']); $params->title = $content['title']; $content['params'] = json_encode($params); } $data = [ 'jsoncontent' => $content['params'], 'displayoptions' => $content['disable'], 'mainlibraryid' => $content['library']['libraryId'], 'timemodified' => time(), 'filtered' => null, 'pathnamehash' => $content['pathnamehash'], 'contenthash' => $content['contenthash'] ]; if (!isset($content['id'])) { $data['timecreated'] = $data['timemodified']; $id = $DB->insert_record('h5p', $data); } else { $id = $data['id'] = $content['id']; $DB->update_record('h5p', $data); } return $id; } /** * Resets marked user data for the given content. * Implements resetContentUserData. * * @param int $contentid The h5p content id */ public function resetContentUserData($contentid) { // Currently, we do not store user data for a content. } /** * Save what libraries a library is depending on. * Implements saveLibraryDependencies. * * @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 The type of dependency */ public function saveLibraryDependencies($libraryid, $dependencies, $dependencytype) { global $DB; foreach ($dependencies as $dependency) { // Find dependency library. $dependencylibrary = $DB->get_record('h5p_libraries', array( 'machinename' => $dependency['machineName'], 'majorversion' => $dependency['majorVersion'], 'minorversion' => $dependency['minorVersion'] ) ); // Create relation. $DB->insert_record('h5p_library_dependencies', array( 'libraryid' => $libraryid, 'requiredlibraryid' => $dependencylibrary->id, 'dependencytype' => $dependencytype )); } } /** * Give an H5P the same library dependencies as a given H5P. * Implements copyLibraryUsage. * * @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 */ public function copyLibraryUsage($contentid, $copyfromid, $contentmainid = null) { // Currently not being called. } /** * Deletes content data. * Implements deleteContentData. * * @param int $contentid Id identifying the content */ public function deleteContentData($contentid) { global $DB; // Remove content. $DB->delete_records('h5p', array('id' => $contentid)); // Remove content library dependencies. $this->deleteLibraryUsage($contentid); } /** * Delete what libraries a content item is using. * Implements deleteLibraryUsage. * * @param int $contentid Content Id of the content we'll be deleting library usage for */ public function deleteLibraryUsage($contentid) { global $DB; $DB->delete_records('h5p_contents_libraries', array('h5pid' => $contentid)); } /** * Saves what libraries the content uses. * Implements saveLibraryUsage. * * @param int $contentid Id identifying the content * @param array $librariesinuse List of libraries the content uses */ public function saveLibraryUsage($contentid, $librariesinuse) { global $DB; $droplibrarycsslist = array(); 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; $DB->insert_record('h5p_contents_libraries', array( 'h5pid' => $contentid, 'libraryid' => $dependency['library']['libraryId'], 'dependencytype' => $dependency['type'], 'dropcss' => $dropcss, 'weight' => $dependency['weight'] )); } } /** * Get number of content/nodes using a library, and the number of dependencies to other libraries. * Implements getLibraryUsage. * * @param int $id Library identifier * @param boolean $skipcontent Optional. Set as true to get number of content instances for library * @return array The array contains two elements, keyed by 'content' and 'libraries'. * Each element contains a number */ public function getLibraryUsage($id, $skipcontent = false) { global $DB; if ($skipcontent) { $content = -1; } else { $sql = "SELECT COUNT(distinct c.id) FROM {h5p_libraries} l JOIN {h5p_contents_libraries} cl ON l.id = cl.libraryid JOIN {h5p} c ON cl.h5pid = c.id WHERE l.id = :libraryid"; $sqlargs = array( 'libraryid' => $id ); $content = $DB->count_records_sql($sql, $sqlargs); } $libraries = $DB->count_records('h5p_library_dependencies', ['requiredlibraryid' => $id]); return array( 'content' => $content, 'libraries' => $libraries, ); } /** * Loads a library. * Implements loadLibrary. * * @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|bool Returns 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: 1 if the library supports fullscreen, 0 otherwise * - embedTypes: list of supported embed types * - preloadedJs: comma separated string with js file paths * - preloadedCss: comma separated sting with css file paths * - dropLibraryCss: list of associative arrays containing: * - machineName: machine name for the librarys that are to drop their css * - semantics: 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 */ public function loadLibrary($machinename, $majorversion, $minorversion) { global $DB; $library = $DB->get_record('h5p_libraries', array( 'machinename' => $machinename, 'majorversion' => $majorversion, 'minorversion' => $minorversion )); if (!$library) { return false; } $librarydata = array( 'libraryId' => $library->id, 'title' => $library->title, 'machineName' => $library->machinename, 'majorVersion' => $library->majorversion, 'minorVersion' => $library->minorversion, 'patchVersion' => $library->patchversion, 'runnable' => $library->runnable, 'fullscreen' => $library->fullscreen, 'embedTypes' => $library->embedtypes, 'preloadedJs' => $library->preloadedjs, 'preloadedCss' => $library->preloadedcss, 'dropLibraryCss' => $library->droplibrarycss, 'semantics' => $library->semantics ); $sql = 'SELECT hl.id, hl.machinename, hl.majorversion, hl.minorversion, hll.dependencytype FROM {h5p_library_dependencies} hll JOIN {h5p_libraries} hl ON hll.requiredlibraryid = hl.id WHERE hll.libraryid = :libraryid ORDER BY hl.id ASC'; $sqlargs = array( 'libraryid' => $library->id ); $dependencies = $DB->get_records_sql($sql, $sqlargs); foreach ($dependencies as $dependency) { $librarydata[$dependency->dependencytype . 'Dependencies'][] = array( 'machineName' => $dependency->machinename, 'majorVersion' => $dependency->majorversion, 'minorVersion' => $dependency->minorversion ); } return $librarydata; } /** * Loads library semantics. * Implements loadLibrarySemantics. * * @param string $name 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($name, $majorversion, $minorversion) { global $DB; $semantics = $DB->get_field('h5p_libraries', 'semantics', array( 'machinename' => $name, 'majorversion' => $majorversion, 'minorversion' => $minorversion ) ); return ($semantics === false ? null : $semantics); } /** * Makes it possible to alter the semantics, adding custom fields, etc. * Implements alterLibrarySemantics. * * @param array $semantics Associative array representing the semantics * @param string $name 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, $name, $majorversion, $minorversion) { global $DB; $library = $DB->get_record('h5p_libraries', array( 'machinename' => $name, 'majorversion' => $majorversion, 'minorversion' => $minorversion, ) ); if ($library) { $library->semantics = json_encode($semantics); $DB->update_record('h5p_libraries', $library); } } /** * Delete all dependencies belonging to given library. * Implements deleteLibraryDependencies. * * @param int $libraryid Library identifier */ public function deleteLibraryDependencies($libraryid) { global $DB; $DB->delete_records('h5p_library_dependencies', array('libraryid' => $libraryid)); } /** * Start an atomic operation against the dependency storage. * Implements lockDependencyStorage. */ public function lockDependencyStorage() { // Library development mode not supported. } /** * Start an atomic operation against the dependency storage. * Implements unlockDependencyStorage. */ public function unlockDependencyStorage() { // Library development mode not supported. } /** * Delete a library from database and file system. * Implements deleteLibrary. * * @param stdClass $library Library object with id, name, major version and minor version */ public function deleteLibrary($library) { $factory = new \core_h5p\factory(); \core_h5p\api::delete_library($factory, $library); } /** * Load content. * Implements loadContent. * * @param int $id Content identifier * @return array Associative array containing: * - id: Identifier for the content * - params: json content as string * - embedType: list of supported embed types * - disable: H5P Button display options * - title: H5P content title * - slug: Human readable content identifier that is unique * - 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 * - metadata: The content's metadata */ public function loadContent($id) { global $DB; $sql = "SELECT hc.id, hc.jsoncontent, hc.displayoptions, hl.id AS libraryid, hl.machinename, hl.title, hl.majorversion, hl.minorversion, hl.fullscreen, hl.embedtypes, hl.semantics, hc.filtered, hc.pathnamehash FROM {h5p} hc JOIN {h5p_libraries} hl ON hl.id = hc.mainlibraryid WHERE hc.id = :h5pid"; $sqlargs = array( 'h5pid' => $id ); $data = $DB->get_record_sql($sql, $sqlargs); // Return null if not found. if ($data === false) { return null; } // Some databases do not support camelCase, so we need to manually // map the values to the camelCase names used by the H5P core. $content = array( 'id' => $data->id, 'params' => $data->jsoncontent, // It has been decided that the embedtype will be always set to 'iframe' (at least for now) because the 'div' // may cause conflicts with CSS and JS in some cases. 'embedType' => 'iframe', 'disable' => $data->displayoptions, 'title' => $data->title, 'slug' => \H5PCore::slugify($data->title) . '-' . $data->id, 'filtered' => $data->filtered, 'libraryId' => $data->libraryid, 'libraryName' => $data->machinename, 'libraryMajorVersion' => $data->majorversion, 'libraryMinorVersion' => $data->minorversion, 'libraryEmbedTypes' => $data->embedtypes, 'libraryFullscreen' => $data->fullscreen, 'metadata' => '', 'pathnamehash' => $data->pathnamehash ); $params = json_decode($data->jsoncontent); if (empty($params->metadata)) { $params->metadata = new \stdClass(); } // Add title to metadata. if (!empty($params->title) && empty($params->metadata->title)) { $params->metadata->title = $params->title; } $content['metadata'] = $params->metadata; $content['params'] = json_encode($params->params ?? $params); return $content; } /** * Load dependencies for the given content of the given type. * Implements loadContentDependencies. * * @param int $id Content identifier * @param int $type The dependency type * @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 * - dependencyType: The dependency type */ public function loadContentDependencies($id, $type = null) { global $DB; $query = "SELECT hcl.id AS unidepid, hl.id AS library_id, hl.machinename AS machine_name, hl.majorversion AS major_version, hl.minorversion AS minor_version, hl.patchversion AS patch_version, hl.preloadedcss AS preloaded_css, hl.preloadedjs AS preloaded_js, hcl.dropcss AS drop_css, hcl.dependencytype as dependency_type FROM {h5p_contents_libraries} hcl JOIN {h5p_libraries} hl ON hcl.libraryid = hl.id WHERE hcl.h5pid = :h5pid"; $queryargs = array( 'h5pid' => $id ); if ($type !== null) { $query .= " AND hcl.dependencytype = :dependencytype"; $queryargs['dependencytype'] = $type; } $query .= " ORDER BY hcl.weight"; $data = $DB->get_records_sql($query, $queryargs); $dependencies = array(); foreach ($data as $dependency) { unset($dependency->unidepid); $dependencies[$dependency->machine_name] = \H5PCore::snakeToCamel($dependency); } return $dependencies; } /** * Get stored setting. * Implements getOption. * * To avoid updating the cache libraries when using the Hub selector, * {@link \H5PEditorAjax::isContentTypeCacheUpdated}, the setting content_type_cache_updated_at * always return the current time. * * @param string $name Identifier for the setting * @param string $default Optional default value if settings is not set * @return mixed Return Whatever has been stored as the setting */ public function getOption($name, $default = false) { if ($name == core::DISPLAY_OPTION_DOWNLOAD || $name == core::DISPLAY_OPTION_EMBED) { // For now, the download and the embed displayoptions are disabled by default, so only will be rendered when // defined in the displayoptions DB field. // This check should be removed if they are added as new H5P settings, to let admins to define the default value. return \H5PDisplayOptionBehaviour::CONTROLLED_BY_AUTHOR_DEFAULT_OFF; } // To avoid update the libraries cache using the Hub selector. if ($name == 'content_type_cache_updated_at') { return time(); } $value = get_config('core_h5p', $name); if ($value === false) { return $default; } return $value; } /** * Stores the given setting. * For example when did we last check h5p.org for updates to our libraries. * Implements setOption. * * @param string $name Identifier for the setting * @param mixed $value Data Whatever we want to store as the setting */ public function setOption($name, $value) { set_config($name, $value, 'core_h5p'); } /** * This will update selected fields on the given content. * Implements updateContentFields(). * * @param int $id Content identifier * @param array $fields Content fields, e.g. filtered */ public function updateContentFields($id, $fields) { global $DB; $content = new \stdClass(); $content->id = $id; foreach ($fields as $name => $value) { // Skip 'slug' as it currently does not exist in the h5p content table. if ($name == 'slug') { continue; } $content->$name = $value; } $DB->update_record('h5p', $content); } /** * Will clear filtered params for all the content that uses the specified. * libraries. This means that the content dependencies will have to be rebuilt and the parameters re-filtered. * Implements clearFilteredParameters(). * * @param array $libraryids Array of library ids */ public function clearFilteredParameters($libraryids) { global $DB; if (empty($libraryids)) { return; } list($insql, $inparams) = $DB->get_in_or_equal($libraryids); $DB->set_field_select('h5p', 'filtered', null, "mainlibraryid $insql", $inparams); } /** * Get number of contents that has to get their content dependencies rebuilt. * and parameters re-filtered. * Implements getNumNotFiltered(). * * @return int The number of contents that has to get their content dependencies rebuilt * and parameters re-filtered */ public function getNumNotFiltered() { global $DB; $sql = "SELECT COUNT(id) FROM {h5p} WHERE " . $DB->sql_compare_text('filtered') . " IS NULL"; return $DB->count_records_sql($sql); } /** * Get number of contents using library as main library. * Implements getNumContent(). * * @param int $libraryid The library ID * @param array $skip The array of h5p content ID's that should be ignored * @return int The number of contents using library as main library */ public function getNumContent($libraryid, $skip = null) { global $DB; $notinsql = ''; $params = array(); if (!empty($skip)) { list($sql, $params) = $DB->get_in_or_equal($skip, SQL_PARAMS_NAMED, 'param', false); $notinsql = " AND id {$sql}"; } $sql = "SELECT COUNT(id) FROM {h5p} WHERE mainlibraryid = :libraryid {$notinsql}"; $params['libraryid'] = $libraryid; return $DB->count_records_sql($sql, $params); } /** * Determines if content slug is used. * Implements isContentSlugAvailable. * * @param string $slug The content slug * @return boolean Whether the content slug is used */ public function isContentSlugAvailable($slug) { // By default the slug should be available as it's currently generated as a unique // value for each h5p content (not stored in the h5p table). return true; } /** * Generates statistics from the event log per library. * Implements getLibraryStats. * * @param string $type Type of event to generate stats for * @return array Number values indexed by library name and version */ public function getLibraryStats($type) { // Event logs are not being stored. } /** * Aggregate the current number of H5P authors. * Implements getNumAuthors. * * @return int The current number of H5P authors */ public function getNumAuthors() { // Currently, H5P authors are not being stored. } /** * 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. * Implements saveCachedAssets. * * @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) { global $DB; foreach ($libraries as $library) { $cachedasset = new \stdClass(); $cachedasset->libraryid = $library['libraryId']; $cachedasset->hash = $key; $DB->insert_record('h5p_libraries_cachedassets', $cachedasset); } } /** * Locate hash keys for given library and delete them. * Used when cache file are deleted. * Implements deleteCachedAssets. * * @param int $libraryid Library identifier * @return array List of hash keys removed */ public function deleteCachedAssets($libraryid) { global $DB; // Get all the keys so we can remove the files. $results = $DB->get_records('h5p_libraries_cachedassets', ['libraryid' => $libraryid]); $hashes = array_map(function($result) { return $result->hash; }, $results); if (!empty($hashes)) { list($sql, $params) = $DB->get_in_or_equal($hashes, SQL_PARAMS_NAMED); // Remove all invalid keys. $DB->delete_records_select('h5p_libraries_cachedassets', 'hash ' . $sql, $params); // Remove also the cachedassets files. $fs = new file_storage(); $fs->deleteCachedAssets($hashes); } return $hashes; } /** * Get the amount of content items associated to a library. * Implements getLibraryContentCount. * * return array The number of content items associated to a library */ public function getLibraryContentCount() { global $DB; $contentcount = array(); $sql = "SELECT h.mainlibraryid, l.machinename, l.majorversion, l.minorversion, COUNT(h.id) AS count FROM {h5p} h LEFT JOIN {h5p_libraries} l ON h.mainlibraryid = l.id GROUP BY h.mainlibraryid, l.machinename, l.majorversion, l.minorversion"; // Count content using the same content type. $res = $DB->get_records_sql($sql); // Extract results. foreach ($res as $lib) { $contentcount["{$lib->machinename} {$lib->majorversion}.{$lib->minorversion}"] = $lib->count; } return $contentcount; } /** * Will trigger after the export file is created. * Implements afterExportCreated. * * @param array $content The content * @param string $filename The file name */ public function afterExportCreated($content, $filename) { // Not being used. } /** * Check whether a user has permissions to execute an action, such as embed H5P content. * Implements hasPermission. * * @param \H5PPermission $permission Permission type * @param int $id Id need by platform to determine permission * @return boolean true if the user can execute the action defined in $permission; false otherwise */ public function hasPermission($permission, $id = null) { // H5P capabilities have not been introduced. } /** * Replaces existing content type cache with the one passed in. * Implements replaceContentTypeCache. * * @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) { // Currently, content type caches are not being stored. } /** * Checks if the given library has a higher version. * Implements libraryHasUpgrade. * * @param array $library An associative array containing: * - machineName: The library machineName * - majorVersion: The library's majorVersion * - minorVersion: The library's minorVersion * @return boolean Whether the library has a higher version */ public function libraryHasUpgrade($library) { global $DB; $sql = "SELECT id FROM {h5p_libraries} WHERE machinename = :machinename AND (majorversion > :majorversion1 OR (majorversion = :majorversion2 AND minorversion > :minorversion))"; $results = $DB->get_records_sql( $sql, array( 'machinename' => $library['machineName'], 'majorversion1' => $library['majorVersion'], 'majorversion2' => $library['majorVersion'], 'minorversion' => $library['minorVersion'] ), 0, 1 ); return !empty($results); } /** * Get current H5P language code. * * @return string Language Code */ public static function get_language() { static $map; if (empty($map)) { // Create mapping for "converting" language codes. $map = array( 'no' => 'nb' ); } // Get current language in Moodle. $language = str_replace('_', '-', strtolower(\current_language())); // Try to map. return $map[$language] ?? $language; } /** * Store messages until they can be printed to the current user. * * @param string $type Type of messages, e.g. 'info', 'error', etc * @param string $newmessage The message * @param string $code The message code */ private function set_message(string $type, string $newmessage = null, string $code = null) { global $SESSION; // We expect to get out an array of strings when getting info // and an array of objects when getting errors for consistency across platforms. // This implementation should be improved for consistency across the data type returned here. if ($type === 'error') { $SESSION->core_h5p_messages[$type][] = (object) array( 'code' => $code, 'message' => $newmessage ); } else { $SESSION->core_h5p_messages[$type][] = $newmessage; } } /** * Convert list of library parameter values to csv. * * @param array $librarydata Library data as found in library.json files * @param string $key Key that should be found in $librarydata * @param string $searchparam The library parameter (Default: 'path') * @return string Library parameter values separated by ', ' */ private function library_parameter_values_to_csv(array $librarydata, string $key, string $searchparam = 'path'): string { if (isset($librarydata[$key])) { $parametervalues = array(); foreach ($librarydata[$key] as $file) { foreach ($file as $index => $value) { if ($index === $searchparam) { $parametervalues[] = $value; } } } return implode(', ', $parametervalues); } return ''; } /** * Get the latest library version. * * @param string $machinename The library's machine name * @return stdClass|null An object with the latest library version */ public function get_latest_library_version(string $machinename): ?\stdClass { global $DB; $libraries = $DB->get_records('h5p_libraries', ['machinename' => $machinename], 'majorversion DESC, minorversion DESC, patchversion DESC', '*', 0, 1); if ($libraries) { return reset($libraries); } return null; } }