MDL-81717 h5p: Improve robustness content type fetching
[moodle.git] / h5p / classes / core.php
blob6bedab9b7e2ca16c885c02de5aeb1137debd0d59
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 namespace core_h5p;
19 defined('MOODLE_INTERNAL') || die();
21 require_once("$CFG->libdir/filelib.php");
23 use Moodle\H5PCore;
24 use Moodle\H5PFrameworkInterface;
25 use Moodle\H5PHubEndpoints;
26 use stdClass;
27 use moodle_url;
28 use core_h5p\local\library\autoloader;
30 // phpcs:disable moodle.NamingConventions.ValidFunctionName.LowercaseMethod
31 // phpcs:disable moodle.NamingConventions.ValidVariableName.VariableNameLowerCase
33 /**
34 * H5P core class, containing functions and storage shared by the other H5P classes.
36 * @package core_h5p
37 * @copyright 2019 Sara Arjona <sara@moodle.com>
38 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
40 class core extends H5PCore {
42 /** @var array The array containing all the present libraries */
43 protected $libraries;
45 /**
46 * Constructor for core_h5p/core.
48 * @param H5PFrameworkInterface $framework The frameworks implementation of the H5PFrameworkInterface
49 * @param string|H5PFileStorage $path The H5P file storage directory or class
50 * @param string $url The URL to the file storage directory
51 * @param string $language The language code. Defaults to english
52 * @param boolean $export Whether export is enabled
54 public function __construct(H5PFrameworkInterface $framework, $path, string $url, string $language = 'en',
55 bool $export = false) {
57 parent::__construct($framework, $path, $url, $language, $export);
59 // Aggregate the assets by default.
60 $this->aggregateAssets = true;
63 /**
64 * Get the path to the dependency.
66 * @param array $dependency An array containing the information of the requested dependency library
67 * @return string The path to the dependency library
69 protected function getDependencyPath(array $dependency): string {
70 $library = $this->find_library($dependency);
72 return "libraries/{$library->id}/" . H5PCore::libraryToFolderName($dependency);
75 /**
76 * Get the paths to the content dependencies.
78 * @param int $id The H5P content ID
79 * @return array An array containing the path of each content dependency
81 public function get_dependency_roots(int $id): array {
82 $roots = [];
83 $dependencies = $this->h5pF->loadContentDependencies($id);
84 $context = \context_system::instance();
85 foreach ($dependencies as $dependency) {
86 $library = $this->find_library($dependency);
87 $roots[self::libraryToFolderName($dependency)] = (moodle_url::make_pluginfile_url(
88 $context->id,
89 'core_h5p',
90 'libraries',
91 $library->id,
92 "/" . self::libraryToFolderName($dependency),
94 ))->out(false);
97 return $roots;
101 * Get a particular dependency library.
103 * @param array $dependency An array containing information of the dependency library
104 * @return stdClass|null The library object if the library dependency exists, null otherwise
106 protected function find_library(array $dependency): ?\stdClass {
107 global $DB;
108 if (null === $this->libraries) {
109 $this->libraries = $DB->get_records('h5p_libraries');
112 $major = $dependency['majorVersion'];
113 $minor = $dependency['minorVersion'];
114 $patch = $dependency['patchVersion'];
116 foreach ($this->libraries as $library) {
117 if ($library->machinename !== $dependency['machineName']) {
118 continue;
121 if ($library->majorversion != $major) {
122 continue;
124 if ($library->minorversion != $minor) {
125 continue;
127 if ($library->patchversion != $patch) {
128 continue;
131 return $library;
134 return null;
138 * Get the list of JS scripts to include on the page.
140 * @return array The array containg urls of the core JavaScript files
142 public static function get_scripts(): array {
143 global $PAGE;
145 $jsrev = $PAGE->requires->get_jsrev();
146 $urls = [];
147 foreach (self::$scripts as $script) {
148 $urls[] = autoloader::get_h5p_core_library_url($script, [
149 'ver' => $jsrev,
152 $urls[] = new moodle_url("/h5p/js/h5p_overrides.js", [
153 'ver' => $jsrev,
156 return $urls;
160 * Fetch and install the latest H5P content types libraries from the official H5P repository.
161 * If the latest version of a content type library is present in the system, nothing is done for that content type.
163 * @return stdClass
165 public function fetch_latest_content_types(): ?\stdClass {
167 $contenttypes = $this->get_latest_content_types();
168 if (!empty($contenttypes->error)) {
169 return $contenttypes;
172 $typesinstalled = [];
174 $factory = new factory();
175 $framework = $factory->get_framework();
177 foreach ($contenttypes->contentTypes as $type) {
178 // Don't fetch content types if any of the versions is disabled.
179 $librarydata = (object) ['machinename' => $type->id];
180 if (!api::is_library_enabled($librarydata)) {
181 continue;
183 // Don't fetch content types that require a higher H5P core API version.
184 if (!$this->is_required_core_api($type->coreApiVersionNeeded)) {
185 continue;
188 $library = [
189 'machineName' => $type->id,
190 'majorVersion' => $type->version->major,
191 'minorVersion' => $type->version->minor,
192 'patchVersion' => $type->version->patch,
194 // Add example and tutorial to the library, to store this information too.
195 if (isset($type->example)) {
196 $library['example'] = $type->example;
198 if (isset($type->tutorial)) {
199 $library['tutorial'] = $type->tutorial;
202 $shoulddownload = true;
203 if ($framework->getLibraryId($type->id, $type->version->major, $type->version->minor)) {
204 if (!$framework->isPatchedLibrary($library)) {
205 $shoulddownload = false;
209 if ($shoulddownload) {
210 $installed['id'] = $this->fetch_content_type($library);
211 if ($installed['id']) {
212 $installed['name'] = H5PCore::libraryToString($library);
213 $typesinstalled[] = $installed;
218 $result = new stdClass();
219 $result->error = '';
220 $result->typesinstalled = $typesinstalled;
222 return $result;
226 * Given an H5P content type machine name, fetch and install the required library from the official H5P repository.
228 * @param array $library Library machineName, majorversion and minorversion.
229 * @return int|null Returns the id of the content type library installed, null otherwise.
231 public function fetch_content_type(array $library): ?int {
232 global $DB;
234 $factory = new factory();
236 $fs = get_file_storage();
238 // Delete any existing file, if it was not deleted during a previous download.
239 $existing = $fs->get_file(
240 (\context_system::instance())->id,
241 'core_h5p',
242 'library_sources',
244 '/',
245 $library['machineName']
247 if ($existing) {
248 $existing->delete();
251 // Download the latest content type from the H5P official repository.
252 $file = $fs->create_file_from_url(
253 (object) [
254 'component' => 'core_h5p',
255 'filearea' => 'library_sources',
256 'itemid' => 0,
257 'contextid' => (\context_system::instance())->id,
258 'filepath' => '/',
259 'filename' => $library['machineName'],
261 $this->get_api_endpoint($library['machineName']),
262 null,
263 true
266 if (!$file) {
267 return null;
270 helper::save_h5p($factory, $file, (object) [], false, true);
272 $file->delete();
274 $librarykey = static::libraryToString($library);
276 if (is_null($factory->get_storage()->h5pC->librariesJsonData)) {
277 // There was an error fetching the content type.
278 debugging('Error fetching content type: ' . $librarykey);
279 return null;
282 $libraryjson = $factory->get_storage()->h5pC->librariesJsonData[$librarykey];
283 if (is_null($libraryjson) || !array_key_exists('libraryId', $libraryjson)) {
284 // There was an error fetching the content type.
285 debugging('Error fetching content type: ' . $librarykey);
286 return null;
289 $libraryid = $libraryjson['libraryId'];
291 // Update example and tutorial (if any of them are defined in $library).
292 $params = ['id' => $libraryid];
293 if (array_key_exists('example', $library)) {
294 $params['example'] = $library['example'];
296 if (array_key_exists('tutorial', $library)) {
297 $params['tutorial'] = $library['tutorial'];
299 if (count($params) > 1) {
300 $DB->update_record('h5p_libraries', $params);
303 return $libraryid;
307 * Get H5P endpoints.
309 * If $endpoint = 'content' and $library is null, moodle_url is the endpoint of the latest version of the H5P content
310 * types; however, if $library is the machine name of a content type, moodle_url is the endpoint to download the content type.
311 * The SITES endpoint ($endpoint = 'site') may be use to get a site UUID or send site data.
313 * @param string|null $library The machineName of the library whose endpoint is requested.
314 * @param string $endpoint The endpoint required. Valid values: "site", "content".
315 * @return moodle_url The endpoint moodle_url object.
317 public function get_api_endpoint(?string $library = null, string $endpoint = 'content'): moodle_url {
318 if ($endpoint == 'site') {
319 $h5purl = H5PHubEndpoints::createURL(H5PHubEndpoints::SITES );
320 } else if ($endpoint == 'content') {
321 $h5purl = H5PHubEndpoints::createURL(H5PHubEndpoints::CONTENT_TYPES ) . $library;
324 return new moodle_url($h5purl);
328 * Get the latest version of the H5P content types available in the official repository.
330 * @return stdClass An object with 2 properties:
331 * - string error: error message when there is any problem, empty otherwise
332 * - array contentTypes: an object for each H5P content type with its information
334 public function get_latest_content_types(): \stdClass {
335 global $CFG;
337 $siteuuid = $this->get_site_uuid() ?? md5($CFG->wwwroot);
338 $postdata = ['uuid' => $siteuuid];
340 // Get the latest content-types json.
341 $endpoint = $this->get_api_endpoint();
342 $request = download_file_content($endpoint, null, $postdata, true);
344 if (!empty($request->error) || $request->status != '200' || empty($request->results)) {
345 if (empty($request->error)) {
346 $request->error = get_string('fetchtypesfailure', 'core_h5p');
348 return $request;
351 $contenttypes = json_decode($request->results);
352 $contenttypes->error = '';
354 return $contenttypes;
358 * Get the site UUID. If site UUID is not defined, try to register the site.
360 * return $string The site UUID, null if it is not set.
362 public function get_site_uuid(): ?string {
363 // Check if the site_uuid is already set.
364 $siteuuid = get_config('core_h5p', 'site_uuid');
366 if (!$siteuuid) {
367 $siteuuid = $this->register_site();
370 return $siteuuid;
374 * Get H5P generated site UUID.
376 * return ?string Returns H5P generated site UUID, null if can't get it.
378 private function register_site(): ?string {
379 $endpoint = $this->get_api_endpoint(null, 'site');
380 $siteuuid = download_file_content($endpoint, null, '');
382 // Successful UUID retrieval from H5P.
383 if ($siteuuid) {
384 $json = json_decode($siteuuid);
385 if (isset($json->uuid)) {
386 set_config('site_uuid', $json->uuid, 'core_h5p');
387 return $json->uuid;
391 return null;
395 * Checks that the required H5P core API version or higher is installed.
397 * @param stdClass $coreapi Object with properties major and minor for the core API version required.
398 * @return bool True if the required H5P core API version is installed. False if not.
400 public function is_required_core_api($coreapi): bool {
401 if (isset($coreapi) && !empty($coreapi)) {
402 if (($coreapi->major > H5PCore::$coreApi['majorVersion']) ||
403 (($coreapi->major == H5PCore::$coreApi['majorVersion']) && ($coreapi->minor > H5PCore::$coreApi['minorVersion']))) {
404 return false;
407 return true;
411 * Get the library string from a DB library record.
413 * @param stdClass $record The DB library record.
414 * @param bool $foldername If true, use hyphen instead of space in returned string.
415 * @return string The string name on the form {machineName} {majorVersion}.{minorVersion}.
417 public static function record_to_string(stdClass $record, bool $foldername = false): string {
418 if ($foldername) {
419 return static::libraryToFolderName([
420 'machineName' => $record->machinename,
421 'majorVersion' => $record->majorversion,
422 'minorVersion' => $record->minorversion,
424 } else {
425 return static::libraryToString([
426 'machineName' => $record->machinename,
427 'majorVersion' => $record->majorversion,
428 'minorVersion' => $record->minorversion,
435 * Small helper for getting the library's ID.
436 * This method is rewritten to use MUC (instead of an static variable which causes some problems with PHPUnit).
438 * @param array $library
439 * @param string $libString
440 * @return int Identifier, or FALSE if non-existent
442 public function getLibraryId($library, $libString = null) {
443 if (!$libString) {
444 $libString = self::libraryToString($library);
447 // Check if this information has been saved previously into the cache.
448 $libcache = \cache::make('core', 'h5p_libraries');
449 $librarykey = helper::get_cache_librarykey($libString);
450 $libraryId = $libcache->get($librarykey);
451 if ($libraryId === false) {
452 $libraryId = $this->h5pF->getLibraryId($library['machineName'], $library['majorVersion'], $library['minorVersion']);
453 $libcache->set($librarykey, $libraryId);
456 return $libraryId;