Differences Between: [Versions 310 and 402] [Versions 311 and 402] [Versions 39 and 402] [Versions 400 and 402] [Versions 401 and 402]
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. 13 // 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/>. 16 17 /** 18 * Contains API class for the H5P area. 19 * 20 * @package core_h5p 21 * @copyright 2020 Sara Arjona <sara@moodle.com> 22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 23 */ 24 25 namespace core_h5p; 26 27 use core\lock\lock_config; 28 use Moodle\H5PCore; 29 30 /** 31 * Contains API class for the H5P area. 32 * 33 * @copyright 2020 Sara Arjona <sara@moodle.com> 34 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later 35 */ 36 class api { 37 38 /** 39 * Delete a library and also all the libraries depending on it and the H5P contents using it. For the H5P content, only the 40 * database entries in {h5p} are removed (the .h5p files are not removed in order to let users to deploy them again). 41 * 42 * @param factory $factory The H5P factory. 43 * @param \stdClass $library The library to delete. 44 */ 45 public static function delete_library(factory $factory, \stdClass $library): void { 46 global $DB; 47 48 // Get the H5P contents using this library, to remove them from DB. The .h5p files won't be removed 49 // so they will be displayed by the player next time a user with the proper permissions accesses it. 50 $sql = 'SELECT DISTINCT hcl.h5pid 51 FROM {h5p_contents_libraries} hcl 52 WHERE hcl.libraryid = :libraryid'; 53 $params = ['libraryid' => $library->id]; 54 $h5pcontents = $DB->get_records_sql($sql, $params); 55 foreach ($h5pcontents as $h5pcontent) { 56 $factory->get_framework()->deleteContentData($h5pcontent->h5pid); 57 } 58 59 $fs = $factory->get_core()->fs; 60 $framework = $factory->get_framework(); 61 // Delete the library from the file system. 62 $fs->delete_library(array('libraryId' => $library->id)); 63 // Delete also the cache assets to rebuild them next time. 64 $framework->deleteCachedAssets($library->id); 65 66 // Remove library data from database. 67 $DB->delete_records('h5p_library_dependencies', array('libraryid' => $library->id)); 68 $DB->delete_records('h5p_libraries', array('id' => $library->id)); 69 70 // Remove the library from the cache. 71 $libscache = \cache::make('core', 'h5p_libraries'); 72 $libarray = [ 73 'machineName' => $library->machinename, 74 'majorVersion' => $library->majorversion, 75 'minorVersion' => $library->minorversion, 76 ]; 77 $libstring = H5PCore::libraryToString($libarray); 78 $librarykey = helper::get_cache_librarykey($libstring); 79 $libscache->delete($librarykey); 80 81 // Remove the libraries using this library. 82 $requiredlibraries = self::get_dependent_libraries($library->id); 83 foreach ($requiredlibraries as $requiredlibrary) { 84 self::delete_library($factory, $requiredlibrary); 85 } 86 } 87 88 /** 89 * Get all the libraries using a defined library. 90 * 91 * @param int $libraryid The library to get its dependencies. 92 * @return array List of libraryid with all the libraries required by a defined library. 93 */ 94 public static function get_dependent_libraries(int $libraryid): array { 95 global $DB; 96 97 $sql = 'SELECT * 98 FROM {h5p_libraries} 99 WHERE id IN (SELECT DISTINCT hl.id 100 FROM {h5p_library_dependencies} hld 101 JOIN {h5p_libraries} hl ON hl.id = hld.libraryid 102 WHERE hld.requiredlibraryid = :libraryid)'; 103 $params = ['libraryid' => $libraryid]; 104 105 return $DB->get_records_sql($sql, $params); 106 } 107 108 /** 109 * Get a library from an identifier. 110 * 111 * @param int $libraryid The library identifier. 112 * @return \stdClass The library object having the library identifier defined. 113 * @throws dml_exception A DML specific exception is thrown if the libraryid doesn't exist. 114 */ 115 public static function get_library(int $libraryid): \stdClass { 116 global $DB; 117 118 return $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST); 119 } 120 121 /** 122 * Returns a library as an object with properties that correspond to the fetched row's field names. 123 * 124 * @param array $params An associative array with the values of the machinename, majorversion and minorversion fields. 125 * @param bool $configurable A library that has semantics so it can be configured in the editor. 126 * @param string $fields Library attributes to retrieve. 127 * 128 * @return \stdClass|null An object with one attribute for each field name in $fields param. 129 */ 130 public static function get_library_details(array $params, bool $configurable, string $fields = ''): ?\stdClass { 131 global $DB; 132 133 $select = "machinename = :machinename 134 AND majorversion = :majorversion 135 AND minorversion = :minorversion"; 136 137 if ($configurable) { 138 $select .= " AND semantics IS NOT NULL"; 139 } 140 141 $fields = $fields ?: '*'; 142 143 $record = $DB->get_record_select('h5p_libraries', $select, $params, $fields); 144 145 return $record ?: null; 146 } 147 148 /** 149 * Get all the H5P content type libraries versions. 150 * 151 * @param string|null $fields Library fields to return. 152 * 153 * @return array An array with an object for each content type library installed. 154 */ 155 public static function get_contenttype_libraries(?string $fields = ''): array { 156 global $DB; 157 158 $libraries = []; 159 $fields = $fields ?: '*'; 160 $select = "runnable = :runnable 161 AND semantics IS NOT NULL"; 162 $params = ['runnable' => 1]; 163 $sort = 'title, majorversion DESC, minorversion DESC'; 164 165 $records = $DB->get_records_select('h5p_libraries', $select, $params, $sort, $fields); 166 167 $added = []; 168 foreach ($records as $library) { 169 // Remove unique index. 170 unset($library->id); 171 172 // Convert snakes to camels. 173 $library->majorVersion = (int) $library->majorversion; 174 unset($library->major_version); 175 $library->minorVersion = (int) $library->minorversion; 176 unset($library->minorversion); 177 $library->metadataSettings = json_decode($library->metadatasettings ?? ''); 178 179 // If we already add this library means that it is an old version,as the previous query was sorted by version. 180 if (isset($added[$library->name])) { 181 $library->isOld = true; 182 } else { 183 $added[$library->name] = true; 184 } 185 186 // Add new library. 187 $libraries[] = $library; 188 } 189 190 return $libraries; 191 } 192 193 /** 194 * Get the H5P DB instance id for a H5P pluginfile URL. If it doesn't exist, it's not created. 195 * 196 * @param string $url H5P pluginfile URL. 197 * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions 198 * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they 199 * might be controlled before calling this method. 200 * 201 * @return array of [file, stdClass|false]: 202 * - file local file for this $url. 203 * - stdClass is an H5P object or false if there isn't any H5P with this URL. 204 */ 205 public static function get_content_from_pluginfile_url(string $url, bool $preventredirect = true, 206 bool $skipcapcheck = false): array { 207 208 global $DB; 209 210 // Deconstruct the URL and get the pathname associated. 211 if ($skipcapcheck || self::can_access_pluginfile_hash($url, $preventredirect)) { 212 $pathnamehash = self::get_pluginfile_hash($url); 213 } 214 215 if (!$pathnamehash) { 216 return [false, false]; 217 } 218 219 // Get the file. 220 $fs = get_file_storage(); 221 $file = $fs->get_file_by_hash($pathnamehash); 222 if (!$file) { 223 return [false, false]; 224 } 225 226 $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); 227 return [$file, $h5p]; 228 } 229 230 /** 231 * Get the original file and H5P DB instance for a given H5P pluginfile URL. If it doesn't exist, it's not created. 232 * If the file has been added as a reference, this method will return the original linked file. 233 * 234 * @param string $url H5P pluginfile URL. 235 * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions. 236 * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they 237 * might be controlled before calling this method. 238 * 239 * @return array of [\stored_file|false, \stdClass|false, \stored_file|false]: 240 * - \stored_file: original local file for the given url (if it has been added as a reference, this method 241 * will return the linked file) or false if there isn't any H5P file with this URL. 242 * - \stdClass: an H5P object or false if there isn't any H5P with this URL. 243 * - \stored_file: file associated to the given url (if it's different from original) or false when both files 244 * (original and file) are the same. 245 * @since Moodle 4.0 246 */ 247 public static function get_original_content_from_pluginfile_url(string $url, bool $preventredirect = true, 248 bool $skipcapcheck = false): array { 249 250 $file = false; 251 list($originalfile, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck); 252 if ($originalfile) { 253 if ($reference = $originalfile->get_reference()) { 254 $file = $originalfile; 255 // If the file has been added as a reference to any other file, get it. 256 $fs = new \file_storage(); 257 $referenced = \file_storage::unpack_reference($reference); 258 $originalfile = $fs->get_file( 259 $referenced['contextid'], 260 $referenced['component'], 261 $referenced['filearea'], 262 $referenced['itemid'], 263 $referenced['filepath'], 264 $referenced['filename'] 265 ); 266 $h5p = self::get_content_from_pathnamehash($originalfile->get_pathnamehash()); 267 if (empty($h5p)) { 268 $h5p = false; 269 } 270 } 271 } 272 273 return [$originalfile, $h5p, $file]; 274 } 275 276 /** 277 * Check if the user can edit an H5P file. It will return true in the following situations: 278 * - The user is the author of the file. 279 * - The component is different from user (i.e. private files). 280 * - If the component is contentbank, the user can edit this file (calling the ContentBank API). 281 * - If the component is mod_xxx or block_xxx, the user has the addinstance capability. 282 * - If the component implements the can_edit_content in the h5p\canedit class and the callback to this method returns true. 283 * 284 * @param \stored_file $file The H5P file to check. 285 * 286 * @return boolean Whether the user can edit or not the given file. 287 * @since Moodle 4.0 288 */ 289 public static function can_edit_content(\stored_file $file): bool { 290 global $USER; 291 292 list($type, $component) = \core_component::normalize_component($file->get_component()); 293 294 // Private files. 295 $currentuserisauthor = $file->get_userid() == $USER->id; 296 $isuserfile = $component === 'user'; 297 if ($currentuserisauthor && $isuserfile) { 298 // The user can edit the content because it's a private user file and she is the owner. 299 return true; 300 } 301 302 // Check if the plugin where the file belongs implements the custom can_edit_content method and call it if that's the case. 303 $classname = '\\' . $file->get_component() . '\\h5p\\canedit'; 304 $methodname = 'can_edit_content'; 305 if (method_exists($classname, $methodname)) { 306 return $classname::{$methodname}($file); 307 } 308 309 // For mod/block files, check if the user has the addinstance capability of the component where the file belongs. 310 if ($type === 'mod' || $type === 'block') { 311 // For any other component, check whether the user can add/edit them. 312 $context = \context::instance_by_id($file->get_contextid()); 313 $plugins = \core_component::get_plugin_list($type); 314 $isvalid = array_key_exists($component, $plugins); 315 if ($isvalid && has_capability("$type/$component:addinstance", $context)) { 316 // The user can edit the content because she has the capability for creating instances where the file belongs. 317 return true; 318 } 319 } 320 321 // For contentbank files, use the API to check if the user has access. 322 if ($component == 'contentbank') { 323 $cb = new \core_contentbank\contentbank(); 324 $content = $cb->get_content_from_id($file->get_itemid()); 325 $contenttype = $content->get_content_type_instance(); 326 if ($contenttype instanceof \contenttype_h5p\contenttype) { 327 // Only H5P contenttypes should be considered here. 328 if ($contenttype->can_edit($content)) { 329 // The user has permissions to edit the H5P in the content bank. 330 return true; 331 } 332 } 333 } 334 335 return false; 336 } 337 338 /** 339 * Create, if it doesn't exist, the H5P DB instance id for a H5P pluginfile URL. If it exists: 340 * - If the content is not the same, remove the existing content and re-deploy the H5P content again. 341 * - If the content is the same, returns the H5P identifier. 342 * 343 * @param string $url H5P pluginfile URL. 344 * @param stdClass $config Configuration for H5P buttons. 345 * @param factory $factory The \core_h5p\factory object 346 * @param stdClass $messages The error, exception and info messages, raised while preparing and running an H5P content. 347 * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions 348 * @param bool $skipcapcheck Whether capabilities should be checked or not to get the pluginfile URL because sometimes they 349 * might be controlled before calling this method. 350 * 351 * @return array of [file, h5pid]: 352 * - file local file for this $url. 353 * - h5pid is the H5P identifier or false if there isn't any H5P with this URL. 354 */ 355 public static function create_content_from_pluginfile_url(string $url, \stdClass $config, factory $factory, 356 \stdClass &$messages, bool $preventredirect = true, bool $skipcapcheck = false): array { 357 global $USER; 358 359 $core = $factory->get_core(); 360 list($file, $h5p) = self::get_content_from_pluginfile_url($url, $preventredirect, $skipcapcheck); 361 362 if (!$file) { 363 $core->h5pF->setErrorMessage(get_string('h5pfilenotfound', 'core_h5p')); 364 return [false, false]; 365 } 366 367 $contenthash = $file->get_contenthash(); 368 if ($h5p && $h5p->contenthash != $contenthash) { 369 // The content exists and it is different from the one deployed previously. The existing one should be removed before 370 // deploying the new version. 371 self::delete_content($h5p, $factory); 372 $h5p = false; 373 } 374 375 $context = \context::instance_by_id($file->get_contextid()); 376 if ($h5p) { 377 // The H5P content has been deployed previously. 378 379 // If the main library for this H5P content is disabled, the content won't be displayed. 380 $mainlibrary = (object) ['id' => $h5p->mainlibraryid]; 381 if (!self::is_library_enabled($mainlibrary)) { 382 $core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p')); 383 return [$file, false]; 384 } else { 385 $displayoptions = helper::get_display_options($core, $config); 386 // Check if the user can set the displayoptions. 387 if ($displayoptions != $h5p->displayoptions && has_capability('moodle/h5p:setdisplayoptions', $context)) { 388 // If displayoptions has changed and user has permission to modify it, update this information in DB. 389 $core->h5pF->updateContentFields($h5p->id, ['displayoptions' => $displayoptions]); 390 } 391 return [$file, $h5p->id]; 392 } 393 } else { 394 // The H5P content hasn't been deployed previously. 395 396 // Check if the user uploading the H5P content is "trustable". If the file hasn't been uploaded by a user with this 397 // capability, the content won't be deployed and an error message will be displayed. 398 if (!helper::can_deploy_package($file)) { 399 $core->h5pF->setErrorMessage(get_string('nopermissiontodeploy', 'core_h5p')); 400 return [$file, false]; 401 } 402 403 // The H5P content can be only deployed if the author of the .h5p file can update libraries or if all the 404 // content-type libraries exist, to avoid users without the h5p:updatelibraries capability upload malicious content. 405 $onlyupdatelibs = !helper::can_update_library($file); 406 407 // Start lock to prevent synchronous access to save the same H5P. 408 $lockfactory = lock_config::get_lock_factory('core_h5p'); 409 $lockkey = 'core_h5p_' . $file->get_pathnamehash(); 410 if ($lock = $lockfactory->get_lock($lockkey, 10)) { 411 try { 412 // Validate and store the H5P content before displaying it. 413 $h5pid = helper::save_h5p($factory, $file, $config, $onlyupdatelibs, false); 414 } finally { 415 $lock->release(); 416 } 417 } else { 418 $core->h5pF->setErrorMessage(get_string('lockh5pdeploy', 'core_h5p')); 419 return [$file, false]; 420 }; 421 422 if (!$h5pid && $file->get_userid() != $USER->id && has_capability('moodle/h5p:updatelibraries', $context)) { 423 // The user has permission to update libraries but the package has been uploaded by a different 424 // user without this permission. Check if there is some missing required library error. 425 $missingliberror = false; 426 $messages = helper::get_messages($messages, $factory); 427 if (!empty($messages->error)) { 428 foreach ($messages->error as $error) { 429 if ($error->code == 'missing-required-library') { 430 $missingliberror = true; 431 break; 432 } 433 } 434 } 435 if ($missingliberror) { 436 // The message about the permissions to upload libraries should be removed. 437 $infomsg = "Note that the libraries may exist in the file you uploaded, but you're not allowed to upload " . 438 "new libraries. Contact the site administrator about this."; 439 if (($key = array_search($infomsg, $messages->info)) !== false) { 440 unset($messages->info[$key]); 441 } 442 443 // No library will be installed and an error will be displayed, because this content is not trustable. 444 $core->h5pF->setInfoMessage(get_string('notrustablefile', 'core_h5p')); 445 } 446 return [$file, false]; 447 448 } 449 return [$file, $h5pid]; 450 } 451 } 452 453 /** 454 * Delete an H5P package. 455 * 456 * @param stdClass $content The H5P package to delete with, at least content['id]. 457 * @param factory $factory The \core_h5p\factory object 458 */ 459 public static function delete_content(\stdClass $content, factory $factory): void { 460 $h5pstorage = $factory->get_storage(); 461 462 // Add an empty slug to the content if it's not defined, because the H5P library requires this field exists. 463 // It's not used when deleting a package, so the real slug value is not required at this point. 464 $content->slug = $content->slug ?? ''; 465 $h5pstorage->deletePackage( (array) $content); 466 } 467 468 /** 469 * Delete an H5P package deployed from the defined $url. 470 * 471 * @param string $url pluginfile URL of the H5P package to delete. 472 * @param factory $factory The \core_h5p\factory object 473 */ 474 public static function delete_content_from_pluginfile_url(string $url, factory $factory): void { 475 global $DB; 476 477 // Get the H5P to delete. 478 $pathnamehash = self::get_pluginfile_hash($url); 479 $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); 480 if ($h5p) { 481 self::delete_content($h5p, $factory); 482 } 483 } 484 485 /** 486 * If user can access pathnamehash from an H5P internal URL. 487 * 488 * @param string $url H5P pluginfile URL poiting to an H5P file. 489 * @param bool $preventredirect Set to true in scripts that can not redirect (CLI, RSS feeds, etc.), throws exceptions 490 * 491 * @return bool if user can access pluginfile hash. 492 * @throws \moodle_exception 493 * @throws \coding_exception 494 * @throws \require_login_exception 495 */ 496 protected static function can_access_pluginfile_hash(string $url, bool $preventredirect = true): bool { 497 global $USER, $CFG; 498 499 // Decode the URL before start processing it. 500 $url = new \moodle_url(urldecode($url)); 501 502 // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors. 503 $url->remove_params(array_keys($url->params())); 504 $path = $url->out_as_local_url(); 505 506 // We only need the slasharguments. 507 $path = substr($path, strpos($path, '.php/') + 5); 508 $parts = explode('/', $path); 509 510 // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey. 511 if (strpos($url, '/tokenpluginfile.php')) { 512 array_shift($parts); 513 } 514 515 // Get the contextid, component and filearea. 516 $contextid = array_shift($parts); 517 $component = array_shift($parts); 518 $filearea = array_shift($parts); 519 520 // Get the context. 521 try { 522 list($context, $course, $cm) = get_context_info_array($contextid); 523 } catch (\moodle_exception $e) { 524 throw new \moodle_exception('invalidcontextid', 'core_h5p'); 525 } 526 527 // For CONTEXT_USER, such as the private files, raise an exception if the owner of the file is not the current user. 528 if ($context->contextlevel == CONTEXT_USER && $USER->id !== $context->instanceid) { 529 throw new \moodle_exception('h5pprivatefile', 'core_h5p'); 530 } 531 532 if (!is_siteadmin($USER)) { 533 // For CONTEXT_COURSECAT No login necessary - unless login forced everywhere. 534 if ($context->contextlevel == CONTEXT_COURSECAT) { 535 if ($CFG->forcelogin) { 536 require_login(null, true, null, false, true); 537 } 538 } 539 540 // For CONTEXT_BLOCK. 541 if ($context->contextlevel == CONTEXT_BLOCK) { 542 if ($context->get_course_context(false)) { 543 // If block is in course context, then check if user has capability to access course. 544 require_course_login($course, true, null, false, true); 545 } else if ($CFG->forcelogin) { 546 // No login necessary - unless login forced everywhere. 547 require_login(null, true, null, false, true); 548 } else { 549 // Get parent context and see if user have proper permission. 550 $parentcontext = $context->get_parent_context(); 551 if ($parentcontext->contextlevel === CONTEXT_COURSECAT) { 552 // Check if category is visible and user can view this category. 553 if (!\core_course_category::get($parentcontext->instanceid, IGNORE_MISSING)) { 554 send_file_not_found(); 555 } 556 } else if ($parentcontext->contextlevel === CONTEXT_USER && $parentcontext->instanceid != $USER->id) { 557 // The block is in the context of a user, it is only visible to the user who it belongs to. 558 send_file_not_found(); 559 } 560 if ($filearea !== 'content') { 561 send_file_not_found(); 562 } 563 } 564 } 565 566 // For CONTEXT_MODULE and CONTEXT_COURSE check if the user is enrolled in the course. 567 // And for CONTEXT_MODULE has permissions view this .h5p file. 568 if ($context->contextlevel == CONTEXT_MODULE || 569 $context->contextlevel == CONTEXT_COURSE) { 570 // Require login to the course first (without login to the module). 571 require_course_login($course, true, null, !$preventredirect, $preventredirect); 572 573 // Now check if module is available OR it is restricted but the intro is shown on the course page. 574 if ($context->contextlevel == CONTEXT_MODULE) { 575 $cminfo = \cm_info::create($cm); 576 if (!$cminfo->uservisible) { 577 if (!$cm->showdescription || !$cminfo->is_visible_on_course_page()) { 578 // Module intro is not visible on the course page and module is not available, show access error. 579 require_course_login($course, true, $cminfo, !$preventredirect, $preventredirect); 580 } 581 } 582 } 583 } 584 } 585 586 return true; 587 } 588 589 /** 590 * Get the pathnamehash from an H5P internal URL. 591 * 592 * @param string $url H5P pluginfile URL poiting to an H5P file. 593 * 594 * @return string|false pathnamehash for the file in the internal URL. 595 * 596 * @throws \moodle_exception 597 */ 598 protected static function get_pluginfile_hash(string $url) { 599 600 // Decode the URL before start processing it. 601 $url = new \moodle_url(urldecode($url)); 602 603 // Remove params from the URL (such as the 'forcedownload=1'), to avoid errors. 604 $url->remove_params(array_keys($url->params())); 605 $path = $url->out_as_local_url(); 606 607 // We only need the slasharguments. 608 $path = substr($path, strpos($path, '.php/') + 5); 609 $parts = explode('/', $path); 610 $filename = array_pop($parts); 611 612 // If the request is made by tokenpluginfile.php we need to avoid userprivateaccesskey. 613 if (strpos($url, '/tokenpluginfile.php')) { 614 array_shift($parts); 615 } 616 617 // Get the contextid, component and filearea. 618 $contextid = array_shift($parts); 619 $component = array_shift($parts); 620 $filearea = array_shift($parts); 621 622 // Ignore draft files, because they are considered temporary files, so shouldn't be displayed. 623 if ($filearea == 'draft') { 624 return false; 625 } 626 627 // Get the context. 628 try { 629 list($context, $course, $cm) = get_context_info_array($contextid); 630 } catch (\moodle_exception $e) { 631 throw new \moodle_exception('invalidcontextid', 'core_h5p'); 632 } 633 634 // Some components, such as mod_page or mod_resource, add the revision to the URL to prevent caching problems. 635 // So the URL contains this revision number as itemid but a 0 is always stored in the files table. 636 // In order to get the proper hash, a callback should be done (looking for those exceptions). 637 $pathdata = null; 638 if ($context->contextlevel == CONTEXT_MODULE || $context->contextlevel == CONTEXT_BLOCK) { 639 $pathdata = component_callback($component, 'get_path_from_pluginfile', [$filearea, $parts], null); 640 } 641 if (null === $pathdata) { 642 // Look for the components and fileareas which have empty itemid defined in xxx_pluginfile. 643 $hasnullitemid = false; 644 $hasnullitemid = $hasnullitemid || ($component === 'user' && ($filearea === 'private' || $filearea === 'profile')); 645 $hasnullitemid = $hasnullitemid || (substr($component, 0, 4) === 'mod_' && $filearea === 'intro'); 646 $hasnullitemid = $hasnullitemid || ($component === 'course' && 647 ($filearea === 'summary' || $filearea === 'overviewfiles')); 648 $hasnullitemid = $hasnullitemid || ($component === 'coursecat' && $filearea === 'description'); 649 $hasnullitemid = $hasnullitemid || ($component === 'backup' && 650 ($filearea === 'course' || $filearea === 'activity' || $filearea === 'automated')); 651 if ($hasnullitemid) { 652 $itemid = 0; 653 } else { 654 $itemid = array_shift($parts); 655 } 656 657 if (empty($parts)) { 658 $filepath = '/'; 659 } else { 660 $filepath = '/' . implode('/', $parts) . '/'; 661 } 662 } else { 663 // The itemid and filepath have been returned by the component callback. 664 [ 665 'itemid' => $itemid, 666 'filepath' => $filepath, 667 ] = $pathdata; 668 } 669 670 $fs = get_file_storage(); 671 $pathnamehash = $fs->get_pathname_hash($contextid, $component, $filearea, $itemid, $filepath, $filename); 672 return $pathnamehash; 673 } 674 675 /** 676 * Returns the H5P content object corresponding to an H5P content file. 677 * 678 * @param string $pathnamehash The pathnamehash of the file associated to an H5P content. 679 * 680 * @return null|\stdClass H5P content object or null if not found. 681 */ 682 public static function get_content_from_pathnamehash(string $pathnamehash): ?\stdClass { 683 global $DB; 684 685 $h5p = $DB->get_record('h5p', ['pathnamehash' => $pathnamehash]); 686 687 return ($h5p) ? $h5p : null; 688 } 689 690 /** 691 * Return the H5P export information file when the file has been deployed. 692 * Otherwise, return null if H5P file: 693 * i) has not been deployed. 694 * ii) has changed the content. 695 * 696 * The information returned will be: 697 * - filename, filepath, mimetype, filesize, timemodified and fileurl. 698 * 699 * @param int $contextid ContextId of the H5P activity. 700 * @param factory $factory The \core_h5p\factory object. 701 * @param string $component component 702 * @param string $filearea file area 703 * @return array|null Return file info otherwise null. 704 */ 705 public static function get_export_info_from_context_id(int $contextid, 706 factory $factory, 707 string $component, 708 string $filearea): ?array { 709 710 $core = $factory->get_core(); 711 $fs = get_file_storage(); 712 $files = $fs->get_area_files($contextid, $component, $filearea, 0, 'id', false); 713 $file = reset($files); 714 715 if ($h5p = self::get_content_from_pathnamehash($file->get_pathnamehash())) { 716 if ($h5p->contenthash == $file->get_contenthash()) { 717 $content = $core->loadContent($h5p->id); 718 $slug = $content['slug'] ? $content['slug'] . '-' : ''; 719 $filename = "{$slug}{$content['id']}.h5p"; 720 $deployedfile = helper::get_export_info($filename, null, $factory); 721 722 return $deployedfile; 723 } 724 } 725 726 return null; 727 } 728 729 /** 730 * Enable or disable a library. 731 * 732 * @param int $libraryid The id of the library to enable/disable. 733 * @param bool $isenabled True if the library should be enabled; false otherwise. 734 */ 735 public static function set_library_enabled(int $libraryid, bool $isenabled): void { 736 global $DB; 737 738 $library = $DB->get_record('h5p_libraries', ['id' => $libraryid], '*', MUST_EXIST); 739 if ($library->runnable) { 740 // For now, only runnable libraries can be enabled/disabled. 741 $record = [ 742 'id' => $libraryid, 743 'enabled' => $isenabled, 744 ]; 745 $DB->update_record('h5p_libraries', $record); 746 } 747 } 748 749 /** 750 * Check whether a library is enabled or not. When machinename is passed, it will return false if any of the versions 751 * for this machinename is disabled. 752 * If the library doesn't exist, it will return true. 753 * 754 * @param \stdClass $librarydata Supported fields for library: 'id' and 'machichename'. 755 * @return bool 756 * @throws \moodle_exception 757 */ 758 public static function is_library_enabled(\stdClass $librarydata): bool { 759 global $DB; 760 761 $params = []; 762 if (property_exists($librarydata, 'machinename')) { 763 $params['machinename'] = $librarydata->machinename; 764 } 765 if (property_exists($librarydata, 'id')) { 766 $params['id'] = $librarydata->id; 767 } 768 769 if (empty($params)) { 770 throw new \moodle_exception("Missing 'machinename' or 'id' in librarydata parameter"); 771 } 772 773 $libraries = $DB->get_records('h5p_libraries', $params); 774 775 // If any of the libraries with these values have been disabled, return false. 776 foreach ($libraries as $id => $library) { 777 if (!$library->enabled) { 778 return false; 779 } 780 } 781 782 return true; 783 } 784 785 /** 786 * Check whether an H5P package is valid or not. 787 * 788 * @param \stored_file $file The file with the H5P content. 789 * @param bool $onlyupdatelibs Whether new libraries can be installed or only the existing ones can be updated 790 * @param bool $skipcontent Should the content be skipped (so only the libraries will be saved)? 791 * @param factory|null $factory The \core_h5p\factory object 792 * @param bool $deletefiletree Should the temporary files be deleted before returning? 793 * @return bool True if the H5P file is valid (expected format, valid libraries...); false otherwise. 794 */ 795 public static function is_valid_package(\stored_file $file, bool $onlyupdatelibs, bool $skipcontent = false, 796 ?factory $factory = null, bool $deletefiletree = true): bool { 797 798 // This may take a long time. 799 \core_php_time_limit::raise(); 800 801 $isvalid = false; 802 803 if (empty($factory)) { 804 $factory = new factory(); 805 } 806 $core = $factory->get_core(); 807 $h5pvalidator = $factory->get_validator(); 808 809 // Set the H5P file path. 810 $core->h5pF->set_file($file); 811 $path = $core->fs->getTmpPath(); 812 $core->h5pF->getUploadedH5pFolderPath($path); 813 // Add manually the extension to the file to avoid the validation fails. 814 $path .= '.h5p'; 815 $core->h5pF->getUploadedH5pPath($path); 816 // Copy the .h5p file to the temporary folder. 817 $file->copy_content_to($path); 818 819 if ($h5pvalidator->isValidPackage($skipcontent, $onlyupdatelibs)) { 820 if ($skipcontent) { 821 $isvalid = true; 822 } else if (!empty($h5pvalidator->h5pC->mainJsonData['mainLibrary'])) { 823 $mainlibrary = (object) ['machinename' => $h5pvalidator->h5pC->mainJsonData['mainLibrary']]; 824 if (self::is_library_enabled($mainlibrary)) { 825 $isvalid = true; 826 } else { 827 // If the main library of the package is disabled, the H5P content will be considered invalid. 828 $core->h5pF->setErrorMessage(get_string('mainlibrarydisabled', 'core_h5p')); 829 } 830 } 831 } 832 833 if ($deletefiletree) { 834 // Remove temp content folder. 835 H5PCore::deleteFileTree($path); 836 } 837 838 return $isvalid; 839 } 840 }
title
Description
Body
title
Description
Body
title
Description
Body
title
Body